use crate::common::*; use pretty_assertions::assert_eq; macro_rules! test { ( name: $name:ident, $(justfile: $justfile:expr,)? $(args: ($($arg:tt),*),)? $(env: { $($env_key:literal : $env_value:literal,)* },)? $(stdin: $stdin:expr,)? $(stdout: $stdout:expr,)? $(stderr: $stderr:expr,)? $(status: $status:expr,)? $(shell: $shell:expr,)? ) => { #[test] fn $name() { let test = crate::test::Test::new(); $($(let test = test.arg($arg);)*)? $($(let test = test.env($env_key, $env_value);)*)? $(let test = test.justfile($justfile);)? $(let test = test.shell($shell);)? $(let test = test.status($status);)? $(let test = test.stderr($stderr);)? $(let test = test.stdin($stdin);)? $(let test = test.stdout($stdout);)? test.run(); } } } pub(crate) struct Test { pub(crate) args: Vec, pub(crate) current_dir: PathBuf, pub(crate) env: BTreeMap, pub(crate) justfile: Option, pub(crate) shell: bool, pub(crate) status: i32, pub(crate) stderr: String, pub(crate) stderr_regex: Option, pub(crate) stdin: String, pub(crate) stdout: String, pub(crate) suppress_dotenv_load_warning: bool, pub(crate) tempdir: TempDir, } impl Test { pub(crate) fn new() -> Self { Self::with_tempdir(tempdir()) } pub(crate) fn with_tempdir(tempdir: TempDir) -> Self { Self { args: Vec::new(), current_dir: PathBuf::new(), env: BTreeMap::new(), justfile: Some(String::new()), shell: true, status: EXIT_SUCCESS, stderr: String::new(), stderr_regex: None, stdin: String::new(), stdout: String::new(), suppress_dotenv_load_warning: true, tempdir, } } pub(crate) fn arg(mut self, val: &str) -> Self { self.args.push(val.to_owned()); self } pub(crate) fn args(mut self, args: &[&str]) -> Self { for arg in args { self = self.arg(arg); } self } pub(crate) fn current_dir(mut self, path: impl AsRef) -> Self { self.current_dir = path.as_ref().to_owned(); self } pub(crate) fn env(mut self, key: &str, val: &str) -> Self { self.env.insert(key.to_string(), val.to_string()); self } pub(crate) fn justfile(mut self, justfile: impl Into) -> Self { self.justfile = Some(justfile.into()); self } pub(crate) fn justfile_path(&self) -> PathBuf { self.tempdir.path().join("justfile") } pub(crate) fn no_justfile(mut self) -> Self { self.justfile = None; self } pub(crate) fn shell(mut self, shell: bool) -> Self { self.shell = shell; self } pub(crate) fn status(mut self, exit_status: i32) -> Self { self.status = exit_status; self } pub(crate) fn stderr(mut self, stderr: impl Into) -> Self { self.stderr = stderr.into(); self } pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef) -> Self { self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap()); self } pub(crate) fn stdin(mut self, stdin: impl Into) -> Self { self.stdin = stdin.into(); self } pub(crate) fn stdout(mut self, stdout: impl Into) -> Self { self.stdout = stdout.into(); self } pub(crate) fn suppress_dotenv_load_warning(mut self, suppress_dotenv_load_warning: bool) -> Self { self.suppress_dotenv_load_warning = suppress_dotenv_load_warning; self } pub(crate) fn tree(self, mut tree: Tree) -> Self { tree.map(|_name, content| unindent(content)); tree.instantiate(&self.tempdir.path()).unwrap(); self } } impl Test { pub(crate) fn run(self) -> TempDir { if let Some(justfile) = &self.justfile { let justfile = unindent(justfile); fs::write(self.justfile_path(), justfile).unwrap(); } let stdout = unindent(&self.stdout); let stderr = unindent(&self.stderr); let mut dotenv_path = self.tempdir.path().to_path_buf(); dotenv_path.push(".env"); fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap(); let mut command = Command::new(&executable_path("just")); if self.shell { command.args(&["--shell", "bash"]); } let mut child = command .args(self.args) .envs(&self.env) .env( "JUST_SUPPRESS_DOTENV_LOAD_WARNING", if self.suppress_dotenv_load_warning { "1" } else { "0" }, ) .current_dir(self.tempdir.path().join(self.current_dir)) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("just invocation failed"); { let mut stdin_handle = child.stdin.take().expect("failed to unwrap stdin handle"); stdin_handle .write_all(self.stdin.as_bytes()) .expect("failed to write stdin to just process"); } let output = child .wait_with_output() .expect("failed to wait for just process"); fn compare(name: &str, have: T, want: T) -> bool { let equal = have == want; if !equal { eprintln!("Bad {}: {}", name, Comparison::new(&have, &want)); } equal } let output_stderr = str::from_utf8(&output.stderr).unwrap(); if let Some(ref stderr_regex) = self.stderr_regex { if !stderr_regex.is_match(output_stderr) { panic!( "Stderr regex mismatch: {} !~= /{}/", output_stderr, stderr_regex ); } } if !compare("status", output.status.code().unwrap(), self.status) | !compare("stdout", str::from_utf8(&output.stdout).unwrap(), &stdout) | (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr)) { panic!("Output mismatch."); } if self.status == EXIT_SUCCESS { test_round_trip(self.tempdir.path()); } self.tempdir } } #[derive(PartialEq, Debug)] struct Output<'a> { stdout: &'a str, stderr: &'a str, status: i32, } fn test_round_trip(tmpdir: &Path) { println!("Reparsing..."); let output = Command::new(&executable_path("just")) .current_dir(tmpdir) .arg("--dump") .output() .expect("just invocation failed"); if !output.status.success() { panic!("dump failed: {}", output.status); } let dumped = String::from_utf8(output.stdout).unwrap(); let reparsed_path = tmpdir.join("reparsed.just"); fs::write(&reparsed_path, &dumped).unwrap(); let output = Command::new(&executable_path("just")) .current_dir(tmpdir) .arg("--justfile") .arg(&reparsed_path) .arg("--dump") .output() .expect("just invocation failed"); if !output.status.success() { panic!("reparse failed: {}", output.status); } let reparsed = String::from_utf8(output.stdout).unwrap(); assert_eq!(reparsed, dumped, "reparse mismatch"); }