diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-02-27 16:52:49 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-27 16:52:49 -0400 |
commit | 7c090b1b14e6b5000dbbed434525387c414ca62c (patch) | |
tree | 1645fbd8a8b1aee55f79cfe74d2bb7ecadcf5215 /test_util/src | |
parent | 6bbb4c3af60d568a34e1472a0721ddd8a3dab469 (diff) |
chore: test builders for integration tests (#17965)
Start of adding test builders to simplify integration tests.
I only updated a few test files. We can complete upgrading over time.
Diffstat (limited to 'test_util/src')
-rw-r--r-- | test_util/src/assertions.rs | 60 | ||||
-rw-r--r-- | test_util/src/builders.rs | 369 | ||||
-rw-r--r-- | test_util/src/lib.rs | 152 |
3 files changed, 463 insertions, 118 deletions
diff --git a/test_util/src/assertions.rs b/test_util/src/assertions.rs index a004530b6..994926566 100644 --- a/test_util/src/assertions.rs +++ b/test_util/src/assertions.rs @@ -39,3 +39,63 @@ macro_rules! assert_not_contains { } } } + +#[macro_export] +macro_rules! assert_output_text { + ($output:expr, $expected:expr) => { + let expected_text = $expected; + let actual = $output.text(); + + if !expected_text.contains("[WILDCARD]") { + assert_eq!(actual, expected_text) + } else if !test_util::wildcard_match(&expected_text, actual) { + println!("OUTPUT START\n{}\nOUTPUT END", actual); + println!("EXPECTED START\n{expected_text}\nEXPECTED END"); + panic!("pattern match failed"); + } + }; +} + +#[macro_export] +macro_rules! assert_output_file { + ($output:expr, $file_path:expr) => { + let output = &$output; + let output_path = output.testdata_dir().join($file_path); + println!("output path {}", output_path.display()); + let expected_text = + std::fs::read_to_string(&output_path).unwrap_or_else(|err| { + panic!("failed loading {}\n\n{err:#}", output_path.display()) + }); + test_util::assert_output_text!(output, expected_text); + }; +} + +#[macro_export] +macro_rules! assert_exit_code { + ($output:expr, $exit_code:expr) => { + let output = &$output; + let actual = output.text(); + let expected_exit_code = $exit_code; + let actual_exit_code = output.exit_code(); + + if let Some(exit_code) = &actual_exit_code { + if *exit_code != expected_exit_code { + println!("OUTPUT\n{actual}\nOUTPUT"); + panic!( + "bad exit code, expected: {:?}, actual: {:?}", + expected_exit_code, exit_code + ); + } + } else { + println!("OUTPUT\n{actual}\nOUTPUT"); + if let Some(signal) = output.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); + } + } + }; +} diff --git a/test_util/src/builders.rs b/test_util/src/builders.rs new file mode 100644 index 000000000..bbd045a10 --- /dev/null +++ b/test_util/src/builders.rs @@ -0,0 +1,369 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::io::Read; +use std::io::Write; +use std::path::PathBuf; +use std::process::Command; +use std::process::Stdio; +use std::rc::Rc; + +use os_pipe::pipe; + +use crate::copy_dir_recursive; +use crate::deno_exe_path; +use crate::http_server; +use crate::new_deno_dir; +use crate::strip_ansi_codes; +use crate::testdata_path; +use crate::HttpServerGuard; +use crate::TempDir; + +#[derive(Default)] +pub struct TestContextBuilder { + use_http_server: bool, + use_temp_cwd: 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<String>, + cwd: Option<String>, + envs: HashMap<String, String>, +} + +impl TestContextBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn use_http_server(&mut self) -> &mut Self { + self.use_http_server = true; + self + } + + pub fn use_temp_cwd(&mut self) -> &mut Self { + self.use_temp_cwd = 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<str>) { + self.copy_temp_dir = Some(dir.as_ref().to_string()); + } + + pub fn set_cwd(&mut self, cwd: impl AsRef<str>) -> &mut Self { + self.cwd = Some(cwd.as_ref().to_string()); + self + } + + pub fn env( + &mut self, + key: impl AsRef<str>, + value: impl AsRef<str>, + ) -> &mut Self { + self + .envs + .insert(key.as_ref().to_string(), value.as_ref().to_string()); + self + } + + pub fn build(&self) -> TestContext { + let deno_dir = new_deno_dir(); // keep this alive for the test + let testdata_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 = deno_dir.path().join(temp_copy_dir); + std::fs::create_dir_all(&temp_copy_dir).unwrap(); + copy_dir_recursive(&test_data_path, &temp_copy_dir).unwrap(); + deno_dir.path().to_owned() + } else { + testdata_path() + }; + + let deno_exe = deno_exe_path(); + println!("deno_exe path {}", deno_exe.display()); + + let http_server_guard = if self.use_http_server { + Some(Rc::new(http_server())) + } else { + None + }; + + TestContext { + cwd: self.cwd.clone(), + envs: self.envs.clone(), + use_temp_cwd: self.use_temp_cwd, + _http_server_guard: http_server_guard, + deno_dir, + testdata_dir, + } + } +} + +#[derive(Clone)] +pub struct TestContext { + envs: HashMap<String, String>, + use_temp_cwd: bool, + cwd: Option<String>, + _http_server_guard: Option<Rc<HttpServerGuard>>, + deno_dir: TempDir, + testdata_dir: PathBuf, +} + +impl Default for TestContext { + fn default() -> Self { + TestContextBuilder::default().build() + } +} + +impl TestContext { + pub fn with_http_server() -> Self { + TestContextBuilder::default().use_http_server().build() + } + + pub fn testdata_path(&self) -> &PathBuf { + &self.testdata_dir + } + + pub fn deno_dir(&self) -> &TempDir { + &self.deno_dir + } + + pub fn new_command(&self) -> TestCommandBuilder { + TestCommandBuilder { + command_name: Default::default(), + args: Default::default(), + args_vec: Default::default(), + stdin: Default::default(), + envs: Default::default(), + env_clear: Default::default(), + cwd: Default::default(), + context: self.clone(), + } + } +} + +pub struct TestCommandBuilder { + command_name: Option<String>, + args: String, + args_vec: Vec<String>, + stdin: Option<String>, + envs: HashMap<String, String>, + env_clear: bool, + cwd: Option<String>, + context: TestContext, +} + +impl TestCommandBuilder { + pub fn command_name(&mut self, name: impl AsRef<str>) -> &mut Self { + self.command_name = Some(name.as_ref().to_string()); + self + } + + pub fn args(&mut self, text: impl AsRef<str>) -> &mut Self { + self.args = text.as_ref().to_string(); + self + } + + pub fn args_vec(&mut self, args: Vec<String>) -> &mut Self { + self.args_vec = args; + self + } + + pub fn stdin(&mut self, text: impl AsRef<str>) -> &mut Self { + self.stdin = Some(text.as_ref().to_string()); + self + } + + pub fn env( + &mut self, + key: impl AsRef<str>, + value: impl AsRef<str>, + ) -> &mut Self { + self + .envs + .insert(key.as_ref().to_string(), value.as_ref().to_string()); + self + } + + pub fn env_clear(&mut self) -> &mut Self { + self.env_clear = true; + self + } + + pub fn cwd(&mut self, cwd: impl AsRef<str>) -> &mut Self { + self.cwd = Some(cwd.as_ref().to_string()); + self + } + + pub fn run(&self) -> TestCommandOutput { + let cwd = self.cwd.as_ref().or(self.context.cwd.as_ref()); + let cwd = if self.context.use_temp_cwd { + assert!(cwd.is_none()); + self.context.deno_dir.path().to_owned() + } else if let Some(cwd_) = cwd { + self.context.testdata_dir.join(cwd_) + } else { + self.context.testdata_dir.clone() + }; + let args = if self.args_vec.is_empty() { + std::borrow::Cow::Owned( + self + .args + .split_whitespace() + .map(|s| s.to_string()) + .collect::<Vec<_>>(), + ) + } else { + assert!( + self.args.is_empty(), + "Do not provide args when providing args_vec." + ); + std::borrow::Cow::Borrowed(&self.args_vec) + } + .iter() + .map(|arg| { + arg.replace("$TESTDATA", &self.context.testdata_dir.to_string_lossy()) + }) + .collect::<Vec<_>>(); + let (mut reader, writer) = pipe().unwrap(); + let command_name = self + .command_name + .as_ref() + .cloned() + .unwrap_or("deno".to_string()); + let mut command = if command_name == "deno" { + Command::new(deno_exe_path()) + } else { + Command::new(&command_name) + }; + command.env("DENO_DIR", self.context.deno_dir.path()); + + println!("command {} {}", command_name, args.join(" ")); + println!("command cwd {:?}", &cwd); + command.args(args.iter()); + if self.env_clear { + command.env_clear(); + } + command.envs({ + let mut envs = self.context.envs.clone(); + for (key, value) in &self.envs { + envs.insert(key.to_string(), value.to_string()); + } + envs + }); + command.current_dir(cwd); + command.stdin(Stdio::piped()); + let writer_clone = writer.try_clone().unwrap(); + command.stderr(writer_clone); + command.stdout(writer); + + let mut process = command.spawn().unwrap(); + + if let Some(input) = &self.stdin { + 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 mut actual = String::new(); + reader.read_to_string(&mut actual).unwrap(); + + let status = process.wait().expect("failed to finish process"); + let exit_code = status.code(); + #[cfg(unix)] + let signal = { + use std::os::unix::process::ExitStatusExt; + status.signal() + }; + #[cfg(not(unix))] + let signal = None; + + actual = strip_ansi_codes(&actual).to_string(); + + // deno test's output capturing flushes with a zero-width space in order to + // synchronize the output pipes. Occassionally this zero width space + // might end up in the output so strip it from the output comparison here. + if args.first().map(|s| s.as_str()) == Some("test") { + actual = actual.replace('\u{200B}', ""); + } + + TestCommandOutput { + exit_code, + signal, + text: actual, + testdata_dir: self.context.testdata_dir.clone(), + asserted_exit_code: RefCell::new(false), + asserted_text: RefCell::new(false), + _test_context: self.context.clone(), + } + } +} + +pub struct TestCommandOutput { + text: String, + exit_code: Option<i32>, + signal: Option<i32>, + testdata_dir: PathBuf, + asserted_text: RefCell<bool>, + asserted_exit_code: RefCell<bool>, + // keep alive for the duration of the output reference + _test_context: TestContext, +} + +impl Drop for TestCommandOutput { + fn drop(&mut self) { + if std::thread::panicking() { + return; + } + // force the caller to assert these + if !*self.asserted_exit_code.borrow() && self.exit_code != Some(0) { + panic!( + "The non-zero exit code of the command was not asserted: {:?}.", + self.exit_code + ) + } + if !*self.asserted_text.borrow() && !self.text.is_empty() { + println!("OUTPUT\n{}\nOUTPUT", self.text); + panic!(concat!( + "The non-empty text of the command was not asserted. ", + "Call `output.skip_output_check()` to skip if necessary.", + )); + } + } +} + +impl TestCommandOutput { + pub fn testdata_dir(&self) -> &PathBuf { + &self.testdata_dir + } + + pub fn skip_output_check(&self) { + *self.asserted_text.borrow_mut() = true; + } + + pub fn skip_exit_code_check(&self) { + *self.asserted_exit_code.borrow_mut() = true; + } + + pub fn exit_code(&self) -> Option<i32> { + self.skip_exit_code_check(); + self.exit_code + } + + pub fn signal(&self) -> Option<i32> { + self.signal + } + + pub fn text(&self) -> &str { + self.skip_output_check(); + &self.text + } +} diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 2e053f85f..2a22afb4b 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -15,7 +15,6 @@ use hyper::Response; use hyper::StatusCode; use lazy_static::lazy_static; use npm::CUSTOM_NPM_PACKAGE_CACHE; -use os_pipe::pipe; use pretty_assertions::assert_eq; use regex::Regex; use rustls::Certificate; @@ -54,11 +53,16 @@ use tokio_tungstenite::accept_async; use url::Url; pub mod assertions; +mod builders; pub mod lsp; mod npm; pub mod pty; mod temp_dir; +pub use builders::TestCommandBuilder; +pub use builders::TestCommandOutput; +pub use builders::TestContext; +pub use builders::TestContextBuilder; pub use temp_dir::TempDir; const PORT: u16 = 4545; @@ -1948,131 +1952,43 @@ pub struct CheckOutputIntegrationTest<'a> { } impl<'a> CheckOutputIntegrationTest<'a> { - pub fn run(&self) { - let deno_dir = new_deno_dir(); // keep this alive for the test - let args = if self.args_vec.is_empty() { - std::borrow::Cow::Owned(self.args.split_whitespace().collect::<Vec<_>>()) - } else { - assert!( - self.args.is_empty(), - "Do not provide args when providing args_vec." - ); - std::borrow::Cow::Borrowed(&self.args_vec) - }; - let testdata_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 = deno_dir.path().join(temp_copy_dir); - std::fs::create_dir_all(&temp_copy_dir).unwrap(); - copy_dir_recursive(&test_data_path, &temp_copy_dir).unwrap(); - deno_dir.path().to_owned() - } else { - testdata_path() - }; - let args = args - .iter() - .map(|arg| arg.replace("$TESTDATA", &testdata_dir.to_string_lossy())) - .collect::<Vec<_>>(); - let deno_exe = deno_exe_path(); - println!("deno_exe path {}", deno_exe.display()); - - let _http_server_guard = if self.http_server { - Some(http_server()) - } else { - None - }; - - let (mut reader, writer) = pipe().unwrap(); - let mut command = deno_cmd_with_deno_dir(&deno_dir); - let cwd = if self.temp_cwd { - deno_dir.path().to_owned() - } else if let Some(cwd_) = &self.cwd { - testdata_dir.join(cwd_) - } else { - testdata_dir.clone() - }; - println!("deno_exe args {}", args.join(" ")); - println!("deno_exe cwd {:?}", &cwd); - command.args(args.iter()); - if self.env_clear { - command.env_clear(); + pub fn output(&self) -> TestCommandOutput { + let mut context_builder = TestContextBuilder::default(); + if self.temp_cwd { + context_builder.use_temp_cwd(); } - command.envs(self.envs.clone()); - command.current_dir(cwd); - command.stdin(Stdio::piped()); - let writer_clone = writer.try_clone().unwrap(); - command.stderr(writer_clone); - command.stdout(writer); - - let mut process = command.spawn().expect("failed to execute process"); - - if let Some(input) = self.input { - let mut p_stdin = process.stdin.take().unwrap(); - write!(p_stdin, "{input}").unwrap(); + if let Some(dir) = &self.copy_temp_dir { + context_builder.use_copy_temp_dir(dir); + } + if self.http_server { + context_builder.use_http_server(); } - // Very important when using pipes: 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 mut actual = String::new(); - reader.read_to_string(&mut actual).unwrap(); + let context = context_builder.build(); - let status = process.wait().expect("failed to finish process"); + let mut command_builder = context.new_command(); - if let Some(exit_code) = status.code() { - if self.exit_code != exit_code { - println!("OUTPUT\n{actual}\nOUTPUT"); - panic!( - "bad exit code, expected: {:?}, actual: {:?}", - self.exit_code, exit_code - ); - } - } else { - #[cfg(unix)] - { - use std::os::unix::process::ExitStatusExt; - let signal = status.signal().unwrap(); - println!("OUTPUT\n{actual}\nOUTPUT"); - panic!( - "process terminated by signal, expected exit code: {:?}, actual signal: {:?}", - self.exit_code, signal, - ); - } - #[cfg(not(unix))] - { - println!("OUTPUT\n{actual}\nOUTPUT"); - panic!("process terminated without status code on non unix platform, expected exit code: {:?}", self.exit_code); - } + if !self.args.is_empty() { + command_builder.args(self.args); } - - actual = strip_ansi_codes(&actual).to_string(); - - // deno test's output capturing flushes with a zero-width space in order to - // synchronize the output pipes. Occassionally this zero width space - // might end up in the output so strip it from the output comparison here. - if args.first().map(|s| s.as_str()) == Some("test") { - actual = actual.replace('\u{200B}', ""); + if !self.args_vec.is_empty() { + command_builder + .args_vec(self.args_vec.iter().map(|a| a.to_string()).collect()); } - - let expected = if let Some(s) = self.output_str { - s.to_owned() - } else if self.output.is_empty() { - String::new() - } else { - let output_path = testdata_dir.join(self.output); - println!("output path {}", output_path.display()); - std::fs::read_to_string(output_path).expect("cannot read output") - }; - - if !expected.contains("[WILDCARD]") { - assert_eq!(actual, expected) - } else if !wildcard_match(&expected, &actual) { - println!("OUTPUT\n{actual}\nOUTPUT"); - println!("EXPECTED\n{expected}\nEXPECTED"); - panic!("pattern match failed"); + if let Some(input) = &self.input { + command_builder.stdin(input); } + for (key, value) in &self.envs { + command_builder.env(key, value); + } + if self.env_clear { + command_builder.env_clear(); + } + if let Some(cwd) = &self.cwd { + command_builder.cwd(cwd); + } + + command_builder.run() } } |