// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::cell::RefCell; use std::collections::HashMap; use std::collections::HashSet; use std::ffi::OsStr; use std::ffi::OsString; use std::io::Read; use std::io::Write; use std::ops::Deref; use std::ops::DerefMut; use std::path::Path; use std::path::PathBuf; use std::process::Child; use std::process::Command; use std::process::Stdio; use std::rc::Rc; use os_pipe::pipe; use crate::assertions::assert_wildcard_match; use crate::assertions::assert_wildcard_match_with_logger; use crate::deno_exe_path; use crate::denort_exe_path; use crate::env_vars_for_jsr_tests; use crate::env_vars_for_npm_tests; use crate::fs::PathRef; use crate::http_server; use crate::jsr_registry_unset_url; use crate::lsp::LspClientBuilder; use crate::nodejs_org_mirror_unset_url; use crate::npm_registry_unset_url; use crate::pty::Pty; use crate::strip_ansi_codes; use crate::testdata_path; use crate::tests_path; use crate::HttpServerGuard; use crate::TempDir; // Gives the developer a nice error message if they have a deno configuration // file that will be auto-discovered by the tests and cause a lot of failures. static HAS_DENO_JSON_IN_WORKING_DIR_ERR: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { let testdata_path = testdata_path(); let mut current_dir = testdata_path.as_path(); let deno_json_names = ["deno.json", "deno.jsonc"]; loop { for name in deno_json_names { let deno_json_path = current_dir.join(name); if deno_json_path.exists() { return Some(format!( concat!( "Found deno configuration file at {}. The test suite relies on ", "a deno.json not existing in any ancestor directory. Please ", "delete this file so the tests won't auto-discover it.", ), deno_json_path.display(), )); } } if let Some(parent) = current_dir.parent() { current_dir = parent; } else { break; } } None }); #[derive(Default, Clone)] struct DiagnosticLogger(Option>>>); impl DiagnosticLogger { pub fn writeln(&self, text: impl AsRef) { match &self.0 { Some(logger) => { let mut logger = logger.borrow_mut(); logger.write_all(text.as_ref().as_bytes()).unwrap(); logger.write_all(b"\n").unwrap(); } #[allow(clippy::print_stderr)] None => eprintln!("{}", text.as_ref()), } } } #[derive(Default)] pub struct TestContextBuilder { diagnostic_logger: DiagnosticLogger, use_http_server: bool, use_temp_cwd: bool, use_symlinked_temp_dir: bool, use_canonicalized_temp_dir: bool, /// Copies the files at the specified directory in the "testdata" directory /// to the temp folder and runs the test from there. This is useful when /// the test creates files in the testdata directory (ex. a node_modules folder) copy_temp_dir: Option, temp_dir_path: Option, cwd: Option, envs: HashMap, } impl TestContextBuilder { pub fn new() -> Self { Self::default().add_compile_env_vars() } pub fn for_npm() -> Self { Self::new().use_http_server().add_npm_env_vars() } pub fn for_jsr() -> Self { Self::new().use_http_server().add_jsr_env_vars() } pub fn logging_capture(mut self, logger: Rc>>) -> Self { self.diagnostic_logger = DiagnosticLogger(Some(logger)); self } pub fn temp_dir_path(mut self, path: impl AsRef) -> Self { self.temp_dir_path = Some(path.as_ref().to_path_buf()); self } pub fn use_http_server(mut self) -> Self { self.use_http_server = true; self } pub fn use_temp_cwd(mut self) -> Self { self.use_temp_cwd = true; self } /// Causes the temp directory to be symlinked to a target directory /// which is useful for debugging issues that only show up on the CI. /// /// Note: This method is not actually deprecated, it's just the CI /// does this by default so there's no need to check in any code that /// uses this into the repo. This is just for debugging purposes. #[deprecated] pub fn use_symlinked_temp_dir(mut self) -> Self { self.use_symlinked_temp_dir = true; self } /// Causes the temp directory to go to its canonicalized path instead /// of being in a symlinked temp dir on the CI. /// /// Note: This method is not actually deprecated. It's just deprecated /// to discourage its use. Use it sparingly and document why you're using /// it. You better have a good reason other than being lazy! /// /// If your tests are failing because the temp dir is symlinked on the CI, /// then it likely means your code doesn't properly handle when Deno is running /// in a symlinked directory. That's a bug and you should fix it without using /// this. #[deprecated] pub fn use_canonicalized_temp_dir(mut self) -> Self { self.use_canonicalized_temp_dir = true; self } /// Copies the files at the specified directory in the "testdata" directory /// to the temp folder and runs the test from there. This is useful when /// the test creates files in the testdata directory (ex. a node_modules folder) pub fn use_copy_temp_dir(mut self, dir: impl AsRef) -> Self { self.copy_temp_dir = Some(dir.as_ref().to_string()); self } pub fn cwd(mut self, cwd: impl AsRef) -> Self { self.cwd = Some(cwd.as_ref().to_string()); self } pub fn envs(self, vars: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { let mut this = self; for (key, value) in vars { this = this.env(key, value); } this } pub fn env(mut self, key: impl AsRef, value: impl AsRef) -> Self { self .envs .insert(key.as_ref().to_string(), value.as_ref().to_string()); self } pub fn add_npm_env_vars(mut self) -> Self { for (key, value) in env_vars_for_npm_tests() { self = self.env(key, value); } self } pub fn add_compile_env_vars(mut self) -> Self { // The `denort` binary is in the same artifact directory as the `deno` binary. let denort_bin = denort_exe_path(); self = self.env("DENORT_BIN", denort_bin.to_string()); self } pub fn add_jsr_env_vars(mut self) -> Self { for (key, value) in env_vars_for_jsr_tests() { self = self.env(key, value); } self } pub fn build(&self) -> TestContext { if let Some(err) = &*HAS_DENO_JSON_IN_WORKING_DIR_ERR { panic!("{}", err); } let temp_dir_path = PathRef::new( self .temp_dir_path .clone() .unwrap_or_else(std::env::temp_dir), ); let temp_dir_path = if self.use_canonicalized_temp_dir { temp_dir_path.canonicalize() } else { temp_dir_path }; let deno_dir = TempDir::new_in(temp_dir_path.as_path()); let temp_dir = TempDir::new_in(temp_dir_path.as_path()); let temp_dir = if self.use_symlinked_temp_dir { assert!(!self.use_canonicalized_temp_dir); // code doesn't handle using both of these TempDir::new_symlinked(temp_dir) } else { temp_dir }; if let Some(temp_copy_dir) = &self.copy_temp_dir { let test_data_path = testdata_path().join(temp_copy_dir); let temp_copy_dir = temp_dir.path().join(temp_copy_dir); temp_copy_dir.create_dir_all(); test_data_path.copy_to_recursive(&temp_copy_dir); } let deno_exe = deno_exe_path(); let http_server_guard = if self.use_http_server { Some(Rc::new(http_server())) } else { None }; let cwd = if self.use_temp_cwd || self.copy_temp_dir.is_some() { temp_dir.path().to_owned() } else { testdata_path().clone() }; let cwd = match &self.cwd { Some(specified_cwd) => cwd.join(specified_cwd), None => cwd, }; TestContext { cwd, deno_exe, envs: self.envs.clone(), diagnostic_logger: self.diagnostic_logger.clone(), _http_server_guard: http_server_guard, deno_dir, temp_dir, } } } #[derive(Clone)] pub struct TestContext { deno_exe: PathRef, diagnostic_logger: DiagnosticLogger, envs: HashMap, cwd: PathRef, _http_server_guard: Option>, deno_dir: TempDir, temp_dir: TempDir, } impl Default for TestContext { fn default() -> Self { TestContextBuilder::default().build() } } impl TestContext { pub fn with_http_server() -> Self { TestContextBuilder::new().use_http_server().build() } pub fn deno_dir(&self) -> &TempDir { &self.deno_dir } pub fn temp_dir(&self) -> &TempDir { &self.temp_dir } pub fn new_command(&self) -> TestCommandBuilder { TestCommandBuilder::new(self.deno_dir.clone()) .set_diagnostic_logger(self.diagnostic_logger.clone()) .envs(self.envs.clone()) .current_dir(&self.cwd) } pub fn new_lsp_command(&self) -> LspClientBuilder { let mut builder = LspClientBuilder::new_with_dir(self.deno_dir.clone()) .deno_exe(&self.deno_exe) .set_root_dir(self.temp_dir.path().clone()); for (key, value) in &self.envs { builder = builder.env(key, value); } builder } pub fn run_npm(&self, args: impl AsRef) { self .new_command() .name("npm") .args(args) .run() .skip_output_check(); } pub fn get_jsr_package_integrity(&self, sub_path: &str) -> String { fn get_checksum(bytes: &[u8]) -> String { use sha2::Digest; let mut hasher = sha2::Sha256::new(); hasher.update(bytes); format!("{:x}", hasher.finalize()) } let url = url::Url::parse(self.envs.get("JSR_URL").unwrap()).unwrap(); let url = url.join(&format!("{}_meta.json", sub_path)).unwrap(); let bytes = sync_fetch(url); get_checksum(&bytes) } } fn sync_fetch(url: url::Url) -> bytes::Bytes { let runtime = tokio::runtime::Builder::new_current_thread() .enable_io() .enable_time() .build() .unwrap(); runtime.block_on(async move { let client = reqwest::Client::new(); let response = client.get(url).send().await.unwrap(); assert!(response.status().is_success()); response.bytes().await.unwrap() }) } /// We can't clone an stdio, so if someone clones a DenoCmd, /// we want to set this to `Cloned` and show the user a helpful /// panic message. enum StdioContainer { Cloned, Inner(RefCell>), } impl Clone for StdioContainer { fn clone(&self) -> Self { Self::Cloned } } impl StdioContainer { pub fn new(stdio: Stdio) -> Self { Self::Inner(RefCell::new(Some(stdio))) } pub fn take(&self) -> Stdio { match self { StdioContainer::Cloned => panic!("Cannot run a command after it was cloned. You need to reset the stdio value."), StdioContainer::Inner(inner) => { match inner.borrow_mut().take() { Some(value) => value, None => panic!("Cannot run a command that was previously run. You need to reset the stdio value between runs."), } }, } } } #[derive(Clone)] pub struct TestCommandBuilder { deno_dir: TempDir, diagnostic_logger: DiagnosticLogger, stdin: Option, stdout: Option, stderr: Option, stdin_text: Option, command_name: String, cwd: Option, envs: HashMap, envs_remove: HashSet, env_clear: bool, args_text: String, args_vec: Vec, split_output: bool, show_output: bool, } impl TestCommandBuilder { pub fn new(deno_dir: TempDir) -> Self { Self { deno_dir, diagnostic_logger: Default::default(), stdin: None, stdout: None, stderr: None, stdin_text: None, split_output: false, cwd: None, envs: Default::default(), envs_remove: Default::default(), env_clear: false, command_name: "deno".to_string(), args_text: "".to_string(), args_vec: Default::default(), show_output: false, } } pub fn name(mut self, name: impl AsRef) -> Self { self.command_name = name.as_ref().to_string_lossy().to_string(); self } pub fn args(mut self, args: impl AsRef) -> Self { self.args_text = args.as_ref().to_string(); self } pub fn args_vec(mut self, args: I) -> Self where I: IntoIterator, S: AsRef, { self.args_vec.extend( args .into_iter() .map(|s| s.as_ref().to_string_lossy().to_string()), ); self } pub fn arg(mut self, arg: S) -> Self where S: AsRef, { self .args_vec .push(arg.as_ref().to_string_lossy().to_string()); self } pub fn env_clear(mut self) -> Self { self.env_clear = true; self } pub fn envs(self, vars: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { let mut this = self; for (key, value) in vars { this = this.env(key, value); } this } pub fn env(mut self, key: K, val: V) -> Self where K: AsRef, V: AsRef, { self.envs.insert( key.as_ref().to_string_lossy().to_string(), val.as_ref().to_string_lossy().to_string(), ); self } pub fn env_remove(mut self, key: K) -> Self where K: AsRef, { self .envs_remove .insert(key.as_ref().to_string_lossy().to_string()); self } /// Set this to enable streaming the output of the command to stderr. /// /// Not deprecated, this is just here so you don't accidentally /// commit code with this enabled. #[deprecated] pub fn show_output(mut self) -> Self { self.show_output = true; self } pub fn stdin>(mut self, cfg: T) -> Self { self.stdin = Some(StdioContainer::new(cfg.into())); self } pub fn stdout>(mut self, cfg: T) -> Self { self.stdout = Some(StdioContainer::new(cfg.into())); self } pub fn stderr>(mut self, cfg: T) -> Self { self.stderr = Some(StdioContainer::new(cfg.into())); self } pub fn current_dir>(mut self, dir: P) -> Self { let dir = dir.as_ref().to_string_lossy().to_string(); self.cwd = Some(match self.cwd { Some(current) => current.join(dir), None => PathRef::new(dir), }); self } pub fn stdin_piped(self) -> Self { self.stdin(std::process::Stdio::piped()) } pub fn stdout_piped(self) -> Self { self.stdout(std::process::Stdio::piped()) } pub fn stderr_piped(self) -> Self { self.stderr(std::process::Stdio::piped()) } pub fn piped_output(self) -> Self { self.stdout_piped().stderr_piped() } pub fn stdin_text(mut self, text: impl AsRef) -> Self { self.stdin_text = Some(text.as_ref().to_string()); self.stdin_piped() } /// Splits the output into stdout and stderr rather than having them combined. pub fn split_output(mut self) -> Self { // Note: it was previously attempted to capture stdout & stderr separately // then forward the output to a combined pipe, but this was found to be // too racy compared to providing the same combined pipe to both. self.split_output = true; self } fn set_diagnostic_logger(mut self, logger: DiagnosticLogger) -> Self { self.diagnostic_logger = logger; self } pub fn with_pty(&self, mut action: impl FnMut(Pty)) { if !Pty::is_supported() { return; } let cwd = self.build_cwd(); let args = self.build_args(&cwd); let args = args.iter().map(|s| s.as_str()).collect::>(); let mut envs = self.build_envs(&cwd); if !envs.contains_key("NO_COLOR") { // set this by default for pty tests envs.insert("NO_COLOR".to_string(), "1".to_string()); } // note(dsherret): for some reason I need to inject the current // environment here for the pty tests or else I get dns errors if !self.env_clear { for (key, value) in std::env::vars() { envs.entry(key).or_insert(value); } } let command_path = self.build_command_path(); self.diagnostic_logger.writeln(format!( "command {} {}", command_path, args.join(" ") )); self .diagnostic_logger .writeln(format!("command cwd {}", cwd.display())); action(Pty::new(command_path.as_path(), &args, &cwd, Some(envs))) } pub fn output(&self) -> Result { assert!(self.stdin_text.is_none(), "use spawn instead"); self.build_command().output() } pub fn status(&self) -> Result { assert!(self.stdin_text.is_none(), "use spawn instead"); self.build_command().status() } pub fn spawn(&self) -> Result { let child = self.build_command().spawn()?; let mut child = DenoChild { _deno_dir: self.deno_dir.clone(), child, }; if let Some(input) = &self.stdin_text { let mut p_stdin = child.stdin.take().unwrap(); write!(p_stdin, "{input}").unwrap(); } Ok(child) } pub fn spawn_with_piped_output(&self) -> DenoChild { self.clone().piped_output().spawn().unwrap() } pub fn run(&self) -> TestCommandOutput { fn read_pipe_to_string( mut pipe: os_pipe::PipeReader, output_to_stderr: bool, ) -> String { if output_to_stderr { let mut buffer = vec![0; 512]; let mut final_data = Vec::new(); loop { let size = pipe.read(&mut buffer).unwrap(); if size == 0 { break; } final_data.extend(&buffer[..size]); std::io::stderr().write_all(&buffer[..size]).unwrap(); } String::from_utf8_lossy(&final_data).to_string() } else { let mut output = String::new(); pipe.read_to_string(&mut output).unwrap(); output } } fn sanitize_output(text: String, args: &[OsString]) -> String { let mut text = strip_ansi_codes(&text).to_string(); // deno test's output capturing flushes with a zero-width space in order to // synchronize the output pipes. Occasionally this zero width space // might end up in the output so strip it from the output comparison here. if args.first().and_then(|s| s.to_str()) == Some("test") { text = text.replace('\u{200B}', ""); } text } let mut command = self.build_command(); let args = command .get_args() .map(ToOwned::to_owned) .collect::>(); let (combined_reader, std_out_err_handle) = if self.split_output { let (stdout_reader, stdout_writer) = pipe().unwrap(); let (stderr_reader, stderr_writer) = pipe().unwrap(); command.stdout(stdout_writer); command.stderr(stderr_writer); let show_output = self.show_output; ( None, Some(( std::thread::spawn(move || { read_pipe_to_string(stdout_reader, show_output) }), std::thread::spawn(move || { read_pipe_to_string(stderr_reader, show_output) }), )), ) } else { let (combined_reader, combined_writer) = pipe().unwrap(); command.stdout(combined_writer.try_clone().unwrap()); command.stderr(combined_writer); (Some(combined_reader), None) }; let mut process = command.spawn().expect("Failed spawning command"); if let Some(input) = &self.stdin_text { let mut p_stdin = process.stdin.take().unwrap(); write!(p_stdin, "{input}").unwrap(); } // This parent process is still holding its copies of the write ends, // and we have to close them before we read, otherwise the read end // will never report EOF. The Command object owns the writers now, // and dropping it closes them. drop(command); let combined = combined_reader.map(|pipe| { sanitize_output(read_pipe_to_string(pipe, self.show_output), &args) }); let status = process.wait().unwrap(); let std_out_err = std_out_err_handle.map(|(stdout, stderr)| { ( sanitize_output(stdout.join().unwrap(), &args), sanitize_output(stderr.join().unwrap(), &args), ) }); let exit_code = status.code(); #[cfg(unix)] let signal = { use std::os::unix::process::ExitStatusExt; status.signal() }; #[cfg(not(unix))] let signal = None; TestCommandOutput { exit_code, signal, combined, std_out_err, asserted_exit_code: RefCell::new(false), asserted_stdout: RefCell::new(false), asserted_stderr: RefCell::new(false), asserted_combined: RefCell::new(false), diagnostic_logger: self.diagnostic_logger.clone(), _deno_dir: self.deno_dir.clone(), } } fn build_command(&self) -> Command { let command_path = self.build_command_path(); let cwd = self.build_cwd(); let args = self.build_args(&cwd); self.diagnostic_logger.writeln(format!( "command {} {}", command_path, args.join(" ") )); let mut command = Command::new(command_path); self .diagnostic_logger .writeln(format!("command cwd {}", cwd.display())); command.current_dir(&cwd); if let Some(stdin) = &self.stdin { command.stdin(stdin.take()); } if let Some(stdout) = &self.stdout { command.stdout(stdout.take()); } if let Some(stderr) = &self.stderr { command.stderr(stderr.take()); } command.args(args.iter()); if self.env_clear { command.env_clear(); } let envs = self.build_envs(&cwd); command.envs(envs); command.stdin(Stdio::piped()); command } fn build_command_path(&self) -> PathRef { let command_name = if cfg!(windows) && self.command_name == "npm" { "npm.cmd" } else { &self.command_name }; if command_name == "deno" { deno_exe_path() } else if command_name.starts_with("./") && self.cwd.is_some() { self.cwd.as_ref().unwrap().join(command_name) } else { PathRef::new(PathBuf::from(command_name)) } } fn build_args(&self, cwd: &Path) -> Vec { if self.args_vec.is_empty() { std::borrow::Cow::Owned( self .args_text .split_whitespace() .map(|s| s.to_string()) .collect::>(), ) } else { assert!( self.args_text.is_empty(), "Do not provide args when providing args_vec." ); std::borrow::Cow::Borrowed(&self.args_vec) } .iter() .map(|arg| self.replace_vars(arg, cwd)) .collect::>() } fn build_cwd(&self) -> PathBuf { self .cwd .as_ref() .map(PathBuf::from) .unwrap_or_else(|| std::env::current_dir().unwrap()) } fn build_envs(&self, cwd: &Path) -> HashMap { let mut envs = self.envs.clone(); if !envs.contains_key("DENO_DIR") { envs.insert("DENO_DIR".to_string(), self.deno_dir.path().to_string()); } if !envs.contains_key("NPM_CONFIG_REGISTRY") { envs.insert("NPM_CONFIG_REGISTRY".to_string(), npm_registry_unset_url()); } if !envs.contains_key("DENO_NO_UPDATE_CHECK") { envs.insert("DENO_NO_UPDATE_CHECK".to_string(), "1".to_string()); } if !envs.contains_key("JSR_URL") { envs.insert("JSR_URL".to_string(), jsr_registry_unset_url()); } if !envs.contains_key("NODEJS_ORG_MIRROR") { envs.insert( "NODEJS_ORG_MIRROR".to_string(), nodejs_org_mirror_unset_url(), ); } for key in &self.envs_remove { envs.remove(key); } // update any test variables in the env value for value in envs.values_mut() { *value = self.replace_vars(value, cwd); } envs } fn replace_vars(&self, text: &str, cwd: &Path) -> String { // todo(dsherret): use monch to extract out the vars text .replace("$DENO_DIR", &self.deno_dir.path().to_string_lossy()) .replace("$TESTDATA", &testdata_path().to_string_lossy()) .replace("$TESTS", &tests_path().to_string_lossy()) .replace("$PWD", &cwd.to_string_lossy()) } } pub struct DenoChild { // keep alive for the duration of the use of this struct _deno_dir: TempDir, child: Child, } impl Deref for DenoChild { type Target = Child; fn deref(&self) -> &Child { &self.child } } impl DerefMut for DenoChild { fn deref_mut(&mut self) -> &mut Child { &mut self.child } } impl DenoChild { pub fn wait_with_output( self, ) -> Result { self.child.wait_with_output() } } pub struct TestCommandOutput { combined: Option, std_out_err: Option<(String, String)>, exit_code: Option, signal: Option, asserted_stdout: RefCell, asserted_stderr: RefCell, asserted_combined: RefCell, asserted_exit_code: RefCell, diagnostic_logger: DiagnosticLogger, // keep alive for the duration of the output reference _deno_dir: TempDir, } impl Drop for TestCommandOutput { // assert the output and exit code was asserted fn drop(&mut self) { fn panic_unasserted_output(output: &TestCommandOutput, text: &str) { output .diagnostic_logger .writeln(format!("OUTPUT\n{}\nOUTPUT", text)); panic!(concat!( "The non-empty text of the command was not asserted. ", "Call `output.skip_output_check()` to skip if necessary.", )); } if std::thread::panicking() { return; } // either the combined output needs to be asserted or both stdout and stderr if let Some(combined) = &self.combined { if !*self.asserted_combined.borrow() && !combined.is_empty() { panic_unasserted_output(self, combined); } } if let Some((stdout, stderr)) = &self.std_out_err { if !*self.asserted_stdout.borrow() && !stdout.is_empty() { panic_unasserted_output(self, stdout); } if !*self.asserted_stderr.borrow() && !stderr.is_empty() { panic_unasserted_output(self, stderr); } } // now ensure the exit code was asserted if !*self.asserted_exit_code.borrow() && self.exit_code != Some(0) { self.print_output(); panic!( "The non-zero exit code of the command was not asserted: {:?}", self.exit_code, ) } } } impl TestCommandOutput { pub fn skip_output_check(&self) -> &Self { *self.asserted_combined.borrow_mut() = true; self.skip_stdout_check(); self.skip_stderr_check(); self } pub fn skip_stdout_check(&self) -> &Self { *self.asserted_stdout.borrow_mut() = true; self } pub fn skip_stderr_check(&self) -> &Self { *self.asserted_stderr.borrow_mut() = true; self } pub fn skip_exit_code_check(&self) -> &Self { *self.asserted_exit_code.borrow_mut() = true; self } pub fn exit_code(&self) -> Option { self.skip_exit_code_check(); self.exit_code } pub fn signal(&self) -> Option { self.signal } pub fn combined_output(&self) -> &str { self.skip_output_check(); self .combined .as_deref() .expect("not available since .split_output() was called") } pub fn stdout(&self) -> &str { *self.asserted_stdout.borrow_mut() = true; self .std_out_err .as_ref() .map(|(stdout, _)| stdout.as_str()) .expect("call .split_output() on the builder") } pub fn stderr(&self) -> &str { *self.asserted_stderr.borrow_mut() = true; self .std_out_err .as_ref() .map(|(_, stderr)| stderr.as_str()) .expect("call .split_output() on the builder") } #[track_caller] pub fn assert_exit_code(&self, expected_exit_code: i32) -> &Self { let actual_exit_code = self.exit_code(); if let Some(exit_code) = &actual_exit_code { if *exit_code != expected_exit_code { self.print_output(); panic!( "bad exit code, expected: {:?}, actual: {:?}", expected_exit_code, exit_code, ); } } else { self.print_output(); if let Some(signal) = self.signal() { panic!( "process terminated by signal, expected exit code: {:?}, actual signal: {:?}", actual_exit_code, signal, ); } else { panic!( "process terminated without status code on non unix platform, expected exit code: {:?}", actual_exit_code, ); } } self } pub fn print_output(&self) { if let Some(combined) = &self.combined { self .diagnostic_logger .writeln(format!("OUTPUT\n{combined}\nOUTPUT")); } else if let Some((stdout, stderr)) = &self.std_out_err { self .diagnostic_logger .writeln(format!("STDOUT OUTPUT\n{stdout}\nSTDOUT OUTPUT")); self .diagnostic_logger .writeln(format!("STDERR OUTPUT\n{stderr}\nSTDERR OUTPUT")); } } #[track_caller] pub fn assert_matches_text(&self, expected_text: impl AsRef) -> &Self { self.inner_assert_matches_text(self.combined_output(), expected_text) } #[track_caller] pub fn assert_matches_file(&self, file_path: impl AsRef) -> &Self { self.inner_assert_matches_file(self.combined_output(), file_path) } #[track_caller] pub fn assert_stdout_matches_text( &self, expected_text: impl AsRef, ) -> &Self { self.inner_assert_matches_text(self.stdout(), expected_text) } #[track_caller] pub fn assert_stdout_matches_file( &self, file_path: impl AsRef, ) -> &Self { self.inner_assert_matches_file(self.stdout(), file_path) } #[track_caller] pub fn assert_stderr_matches_text( &self, expected_text: impl AsRef, ) -> &Self { self.inner_assert_matches_text(self.stderr(), expected_text) } #[track_caller] pub fn assert_stderr_matches_file( &self, file_path: impl AsRef, ) -> &Self { self.inner_assert_matches_file(self.stderr(), file_path) } #[track_caller] fn inner_assert_matches_text( &self, actual: &str, expected: impl AsRef, ) -> &Self { match &self.diagnostic_logger.0 { Some(logger) => assert_wildcard_match_with_logger( actual, expected.as_ref(), &mut *logger.borrow_mut(), ), None => assert_wildcard_match(actual, expected.as_ref()), }; self } #[track_caller] fn inner_assert_matches_file( &self, actual: &str, file_path: impl AsRef, ) -> &Self { let output_path = testdata_path().join(file_path); self .diagnostic_logger .writeln(format!("output path {}", output_path)); let expected_text = output_path.read_to_string(); self.inner_assert_matches_text(actual, expected_text) } }