summaryrefslogtreecommitdiff
path: root/test_util/src/builders.rs
diff options
context:
space:
mode:
Diffstat (limited to 'test_util/src/builders.rs')
-rw-r--r--test_util/src/builders.rs369
1 files changed, 369 insertions, 0 deletions
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
+ }
+}