From cdf104bf8cce5faa1e8649856cf747d6584d6e36 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 1 Dec 2024 16:37:08 -0800 Subject: [PATCH] Add `--request` subcommand for testing (#2498) --- src/config.rs | 18 ++++++++++++++++++ src/config_error.rs | 2 ++ src/error.rs | 6 +++--- src/lib.rs | 11 +++++++++-- src/request.rs | 13 +++++++++++++ src/subcommand.rs | 24 +++++++++++++++++++----- tests/constants.rs | 9 ++++----- tests/lib.rs | 3 ++- tests/request.rs | 29 +++++++++++++++++++++++++++++ tests/test.rs | 16 ++++++++++++++++ 10 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 src/request.rs create mode 100644 tests/request.rs diff --git a/src/config.rs b/src/config.rs index 6eb8d23b1d..86ec4bfeb5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,6 +53,7 @@ mod cmd { pub(crate) const INIT: &str = "INIT"; pub(crate) const LIST: &str = "LIST"; pub(crate) const MAN: &str = "MAN"; + pub(crate) const REQUEST: &str = "REQUEST"; pub(crate) const SHOW: &str = "SHOW"; pub(crate) const SUMMARY: &str = "SUMMARY"; pub(crate) const VARIABLES: &str = "VARIABLES"; @@ -69,6 +70,7 @@ mod cmd { INIT, LIST, MAN, + REQUEST, SHOW, SUMMARY, VARIABLES, @@ -517,6 +519,17 @@ impl Config { .help("Print man page") .help_heading(cmd::HEADING), ) + .arg( + Arg::new(cmd::REQUEST) + .long("request") + .action(ArgAction::Set) + .hide(true) + .help( + "Execute . For internal testing purposes only. May be changed or removed at \ + any time.", + ) + .help_heading(cmd::REQUEST), + ) .arg( Arg::new(cmd::SHOW) .short('s') @@ -696,6 +709,11 @@ impl Config { } } else if matches.get_flag(cmd::MAN) { Subcommand::Man + } else if let Some(request) = matches.get_one::(cmd::REQUEST) { + Subcommand::Request { + request: serde_json::from_str(request) + .map_err(|source| ConfigError::RequestParse { source })?, + } } else if let Some(path) = matches.get_many::(cmd::SHOW) { Subcommand::Show { path: Self::parse_module_path(path)?, diff --git a/src/config_error.rs b/src/config_error.rs index 6935b81cd5..3023346386 100644 --- a/src/config_error.rs +++ b/src/config_error.rs @@ -12,6 +12,8 @@ pub(crate) enum ConfigError { Internal { message: String }, #[snafu(display("Invalid module path `{}`", path.join(" ")))] ModulePath { path: Vec }, + #[snafu(display("Failed to parse request: {source}"))] + RequestParse { source: serde_json::Error }, #[snafu(display( "Path-prefixed recipes may not be used with `--working-directory` or `--justfile`." ))] diff --git a/src/error.rs b/src/error.rs index b8774a2515..a07c4efd62 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,7 +81,7 @@ pub(crate) enum Error<'src> { }, DotenvRequired, DumpJson { - serde_json_error: serde_json::Error, + source: serde_json::Error, }, EditorInvoke { editor: OsString, @@ -359,8 +359,8 @@ impl ColorDisplay for Error<'_> { DotenvRequired => { write!(f, "Dotenv file not found")?; } - DumpJson { serde_json_error } => { - write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?; + DumpJson { source } => { + write!(f, "Failed to dump JSON to stdout: {source}")?; } EditorInvoke { editor, io_error } => { let editor = editor.to_string_lossy(); diff --git a/src/lib.rs b/src/lib.rs index 799cc37832..cda797c7c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ pub(crate) use { regex::Regex, serde::{ ser::{SerializeMap, SerializeSeq}, - Serialize, Serializer, + Deserialize, Serialize, Serializer, }, snafu::{ResultExt, Snafu}, std::{ @@ -76,9 +76,12 @@ pub(crate) use crate::{node::Node, tree::Tree}; pub use crate::run::run; +#[doc(hidden)] +use request::Request; + // Used in integration tests. #[doc(hidden)] -pub use unindent::unindent; +pub use {request::Response, unindent::unindent}; type CompileResult<'a, T = ()> = Result>; type ConfigResult = Result; @@ -106,6 +109,10 @@ pub mod fuzzing; #[doc(hidden)] pub mod summary; +// Used for testing with the `--request` subcommand. +#[doc(hidden)] +pub mod request; + mod alias; mod analyzer; mod argument_parser; diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000000..a84a84c2fa --- /dev/null +++ b/src/request.rs @@ -0,0 +1,13 @@ +use super::*; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum Request { + EnvironmentVariable(String), +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum Response { + EnvironmentVariable(Option), +} diff --git a/src/subcommand.rs b/src/subcommand.rs index cc47d879a1..8d34578845 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -35,6 +35,9 @@ pub(crate) enum Subcommand { path: ModulePath, }, Man, + Request { + request: Request, + }, Run { arguments: Vec, overrides: BTreeMap, @@ -71,10 +74,6 @@ impl Subcommand { let justfile = &compilation.justfile; match self { - Run { - arguments, - overrides, - } => Self::run(config, loader, search, compilation, arguments, overrides)?, Choose { overrides, chooser } => { Self::choose(config, justfile, &search, overrides, chooser.as_deref())?; } @@ -85,6 +84,11 @@ impl Subcommand { Format => Self::format(config, &search, compilation)?, Groups => Self::groups(config, justfile), List { path } => Self::list(config, justfile, path)?, + Request { request } => Self::request(request)?, + Run { + arguments, + overrides, + } => Self::run(config, loader, search, compilation, arguments, overrides)?, Show { path } => Self::show(config, justfile, path)?, Summary => Self::summary(config, justfile), Variables => Self::variables(justfile), @@ -280,7 +284,7 @@ impl Subcommand { match config.dump_format { DumpFormat::Json => { serde_json::to_writer(io::stdout(), &compilation.justfile) - .map_err(|serde_json_error| Error::DumpJson { serde_json_error })?; + .map_err(|source| Error::DumpJson { source })?; println!(); } DumpFormat::Just => print!("{}", compilation.root_ast()), @@ -402,6 +406,16 @@ impl Subcommand { Ok(()) } + fn request(request: &Request) -> RunResult<'static> { + let response = match request { + Request::EnvironmentVariable(key) => Response::EnvironmentVariable(env::var_os(key)), + }; + + serde_json::to_writer(io::stdout(), &response).map_err(|source| Error::DumpJson { source })?; + + Ok(()) + } + fn list(config: &Config, mut module: &Justfile, path: &ModulePath) -> RunResult<'static> { for name in &path.path { module = module diff --git a/tests/constants.rs b/tests/constants.rs index c6a3a85329..5c30109d8a 100644 --- a/tests/constants.rs +++ b/tests/constants.rs @@ -48,14 +48,13 @@ fn constants_can_be_redefined() { fn constants_are_not_exported() { Test::new() .justfile( - " + r#" set export foo: - echo $HEXUPPER - ", + @'{{just_executable()}}' --request '{"environment-variable": "HEXUPPER"}' + "#, ) - .stderr_regex(".*HEXUPPER: unbound variable.*") - .status(127) + .response(Response::EnvironmentVariable(None)) .run(); } diff --git a/tests/lib.rs b/tests/lib.rs index 675e1fe048..3ee9c3801f 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -6,7 +6,7 @@ pub(crate) use { test::{assert_eval_eq, Output, Test}, }, executable_path::executable_path, - just::unindent, + just::{unindent, Response}, libc::{EXIT_FAILURE, EXIT_SUCCESS}, pretty_assertions::Comparison, regex::Regex, @@ -99,6 +99,7 @@ mod quote; mod readme; mod recursion_limit; mod regexes; +mod request; mod run; mod script; mod search; diff --git a/tests/request.rs b/tests/request.rs new file mode 100644 index 0000000000..da8e144fa2 --- /dev/null +++ b/tests/request.rs @@ -0,0 +1,29 @@ +use super::*; + +#[test] +fn environment_variable_set() { + Test::new() + .justfile( + r#" + export BAR := 'baz' + + @foo: + '{{just_executable()}}' --request '{"environment-variable": "BAR"}' + "#, + ) + .response(Response::EnvironmentVariable(Some("baz".into()))) + .run(); +} + +#[test] +fn environment_variable_missing() { + Test::new() + .justfile( + r#" + @foo: + '{{just_executable()}}' --request '{"environment-variable": "FOO_BAR_BAZ"}' + "#, + ) + .response(Response::EnvironmentVariable(None)) + .run(); +} diff --git a/tests/test.rs b/tests/test.rs index cd7c0cceb7..e02b5a85b6 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -50,6 +50,7 @@ pub(crate) struct Test { pub(crate) env: BTreeMap, pub(crate) expected_files: BTreeMap>, pub(crate) justfile: Option, + pub(crate) response: Option, pub(crate) shell: bool, pub(crate) status: i32, pub(crate) stderr: String, @@ -74,6 +75,7 @@ impl Test { env: BTreeMap::new(), expected_files: BTreeMap::new(), justfile: Some(String::new()), + response: None, shell: true, status: EXIT_SUCCESS, stderr: String::new(), @@ -139,6 +141,11 @@ impl Test { self } + pub(crate) fn response(mut self, response: Response) -> Self { + self.response = Some(response); + self.stdout_regex(".*") + } + pub(crate) fn shell(mut self, shell: bool) -> Self { self.shell = shell; self @@ -293,6 +300,15 @@ impl Test { panic!("Output mismatch."); } + if let Some(ref response) = self.response { + assert_eq!( + &serde_json::from_str::(output_stdout) + .expect("failed to deserialize stdout as response"), + response, + "response mismatch" + ); + } + for (path, expected) in &self.expected_files { let actual = fs::read(self.tempdir.path().join(path)).unwrap(); assert_eq!(