summaryrefslogtreecommitdiff
path: root/tests/util/server/src/builders.rs
diff options
context:
space:
mode:
authorAsher Gomez <ashersaupingomez@gmail.com>2024-02-20 00:34:24 +1100
committerGitHub <noreply@github.com>2024-02-19 06:34:24 -0700
commit2b279ad630651e973d5a31586f58809f005bc925 (patch)
tree3e3cbeb4126643c75381dd5422e8603a7488bb8a /tests/util/server/src/builders.rs
parenteb542bc185c6c4ce1847417a2dfdf04862cd86db (diff)
chore: move `test_util` to `tests/util/server` (#22444)
As discussed with @mmastrac. --------- Signed-off-by: Asher Gomez <ashersaupingomez@gmail.com> Co-authored-by: Matt Mastracci <matthew@mastracci.com>
Diffstat (limited to 'tests/util/server/src/builders.rs')
-rw-r--r--tests/util/server/src/builders.rs997
1 files changed, 997 insertions, 0 deletions
diff --git a/tests/util/server/src/builders.rs b/tests/util/server/src/builders.rs
new file mode 100644
index 000000000..d8c209dd7
--- /dev/null
+++ b/tests/util/server/src/builders.rs
@@ -0,0 +1,997 @@
+// 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::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::npm_registry_unset_url;
+use crate::pty::Pty;
+use crate::strip_ansi_codes;
+use crate::testdata_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<Option<String>> =
+ 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)]
+pub struct TestContextBuilder {
+ use_http_server: bool,
+ use_temp_cwd: bool,
+ use_symlinked_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<String>,
+ temp_dir_path: Option<PathBuf>,
+ cwd: Option<String>,
+ envs: HashMap<String, String>,
+}
+
+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 temp_dir_path(mut self, path: impl AsRef<Path>) -> 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
+ }
+
+ /// 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 {
+ self.copy_temp_dir = Some(dir.as_ref().to_string());
+ self
+ }
+
+ pub fn cwd(mut self, cwd: impl AsRef<str>) -> Self {
+ self.cwd = Some(cwd.as_ref().to_string());
+ self
+ }
+
+ pub fn envs<I, K, V>(self, vars: I) -> Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: AsRef<str>,
+ V: AsRef<str>,
+ {
+ let mut this = self;
+ for (key, value) in vars {
+ this = this.env(key, value);
+ }
+ this
+ }
+
+ pub fn env(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> 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_future_env_vars(mut self) -> Self {
+ self = self.env("DENO_FUTURE", "1");
+ 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 = self
+ .temp_dir_path
+ .clone()
+ .unwrap_or_else(std::env::temp_dir);
+ let deno_dir = TempDir::new_in(&temp_dir_path);
+ let temp_dir = TempDir::new_in(&temp_dir_path);
+ let temp_dir = if self.use_symlinked_temp_dir {
+ 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();
+ println!("deno_exe path {}", deno_exe);
+
+ 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(),
+ _http_server_guard: http_server_guard,
+ deno_dir,
+ temp_dir,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct TestContext {
+ deno_exe: PathRef,
+ envs: HashMap<String, String>,
+ cwd: PathRef,
+ _http_server_guard: Option<Rc<HttpServerGuard>>,
+ 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())
+ .envs(self.envs.clone())
+ .current_dir(&self.cwd)
+ }
+
+ pub fn new_lsp_command(&self) -> LspClientBuilder {
+ LspClientBuilder::new_with_dir(self.deno_dir.clone())
+ .deno_exe(&self.deno_exe)
+ .set_root_dir(self.temp_dir.path().clone())
+ }
+
+ pub fn run_npm(&self, args: impl AsRef<str>) {
+ 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<Option<Stdio>>),
+}
+
+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,
+ stdin: Option<StdioContainer>,
+ stdout: Option<StdioContainer>,
+ stderr: Option<StdioContainer>,
+ stdin_text: Option<String>,
+ command_name: String,
+ cwd: Option<PathRef>,
+ envs: HashMap<String, String>,
+ envs_remove: HashSet<String>,
+ env_clear: bool,
+ args_text: String,
+ args_vec: Vec<String>,
+ split_output: bool,
+ skip_strip_ansi: bool,
+}
+
+impl TestCommandBuilder {
+ pub fn new(deno_dir: TempDir) -> Self {
+ Self {
+ deno_dir,
+ stdin: None,
+ stdout: None,
+ stderr: None,
+ stdin_text: None,
+ split_output: false,
+ skip_strip_ansi: 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(),
+ }
+ }
+
+ pub fn name(mut self, name: impl AsRef<OsStr>) -> Self {
+ self.command_name = name.as_ref().to_string_lossy().to_string();
+ self
+ }
+
+ pub fn args(mut self, args: impl AsRef<str>) -> Self {
+ self.args_text = args.as_ref().to_string();
+ self
+ }
+
+ pub fn args_vec<I, S>(mut self, args: I) -> Self
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<std::ffi::OsStr>,
+ {
+ self.args_vec.extend(
+ args
+ .into_iter()
+ .map(|s| s.as_ref().to_string_lossy().to_string()),
+ );
+ self
+ }
+
+ pub fn arg<S>(mut self, arg: S) -> Self
+ where
+ S: AsRef<std::ffi::OsStr>,
+ {
+ 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<I, K, V>(self, vars: I) -> Self
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: AsRef<std::ffi::OsStr>,
+ V: AsRef<std::ffi::OsStr>,
+ {
+ let mut this = self;
+ for (key, value) in vars {
+ this = this.env(key, value);
+ }
+ this
+ }
+
+ pub fn env<K, V>(mut self, key: K, val: V) -> Self
+ where
+ K: AsRef<std::ffi::OsStr>,
+ V: AsRef<std::ffi::OsStr>,
+ {
+ self.envs.insert(
+ key.as_ref().to_string_lossy().to_string(),
+ val.as_ref().to_string_lossy().to_string(),
+ );
+ self
+ }
+
+ pub fn env_remove<K>(mut self, key: K) -> Self
+ where
+ K: AsRef<std::ffi::OsStr>,
+ {
+ self
+ .envs_remove
+ .insert(key.as_ref().to_string_lossy().to_string());
+ self
+ }
+
+ pub fn skip_strip_ansi(mut self) -> Self {
+ self.skip_strip_ansi = true;
+ self
+ }
+
+ pub fn stdin<T: Into<Stdio>>(mut self, cfg: T) -> Self {
+ self.stdin = Some(StdioContainer::new(cfg.into()));
+ self
+ }
+
+ pub fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
+ self.stdout = Some(StdioContainer::new(cfg.into()));
+ self
+ }
+
+ pub fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
+ self.stderr = Some(StdioContainer::new(cfg.into()));
+ self
+ }
+
+ pub fn current_dir<P: AsRef<OsStr>>(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<str>) -> 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
+ }
+
+ pub fn with_pty(&self, mut action: impl FnMut(Pty)) {
+ if !Pty::is_supported() {
+ return;
+ }
+
+ let args = self.build_args();
+ let args = args.iter().map(|s| s.as_str()).collect::<Vec<_>>();
+ let mut envs = self.build_envs();
+ 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 cwd = self
+ .cwd
+ .as_ref()
+ .map(PathBuf::from)
+ .unwrap_or_else(|| std::env::current_dir().unwrap());
+ let command_path = self.build_command_path();
+
+ println!("command {} {}", command_path, args.join(" "));
+ println!("command cwd {}", cwd.display());
+ action(Pty::new(command_path.as_path(), &args, &cwd, Some(envs)))
+ }
+
+ pub fn output(&self) -> Result<std::process::Output, std::io::Error> {
+ assert!(self.stdin_text.is_none(), "use spawn instead");
+ self.build_command().output()
+ }
+
+ pub fn status(&self) -> Result<std::process::ExitStatus, std::io::Error> {
+ assert!(self.stdin_text.is_none(), "use spawn instead");
+ self.build_command().status()
+ }
+
+ pub fn spawn(&self) -> Result<DenoChild, std::io::Error> {
+ 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) -> String {
+ let mut output = String::new();
+ pipe.read_to_string(&mut output).unwrap();
+ output
+ }
+
+ fn sanitize_output(
+ mut text: String,
+ args: &[OsString],
+ skip_strip_ansi: bool,
+ ) -> String {
+ if !skip_strip_ansi {
+ 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::<Vec<_>>();
+ 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);
+ (
+ None,
+ Some((
+ std::thread::spawn(move || read_pipe_to_string(stdout_reader)),
+ std::thread::spawn(move || read_pipe_to_string(stderr_reader)),
+ )),
+ )
+ } 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), &args, self.skip_strip_ansi)
+ });
+
+ let status = process.wait().unwrap();
+ let std_out_err = std_out_err_handle.map(|(stdout, stderr)| {
+ (
+ sanitize_output(stdout.join().unwrap(), &args, self.skip_strip_ansi),
+ sanitize_output(stderr.join().unwrap(), &args, self.skip_strip_ansi),
+ )
+ });
+ 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),
+ _deno_dir: self.deno_dir.clone(),
+ }
+ }
+
+ fn build_command(&self) -> Command {
+ let command_path = self.build_command_path();
+ let args = self.build_args();
+ println!("command {} {}", command_path, args.join(" "));
+ let mut command = Command::new(command_path);
+ if let Some(cwd) = &self.cwd {
+ println!("command cwd {}", cwd);
+ 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();
+ 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 {
+ PathRef::new(PathBuf::from(command_name))
+ }
+ }
+
+ fn build_args(&self) -> Vec<String> {
+ if self.args_vec.is_empty() {
+ std::borrow::Cow::Owned(
+ self
+ .args_text
+ .split_whitespace()
+ .map(|s| s.to_string())
+ .collect::<Vec<_>>(),
+ )
+ } 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| arg.replace("$TESTDATA", &testdata_path().to_string_lossy()))
+ .collect::<Vec<_>>()
+ }
+
+ fn build_envs(&self) -> HashMap<String, String> {
+ 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());
+ }
+ for key in &self.envs_remove {
+ envs.remove(key);
+ }
+ envs
+ }
+}
+
+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<std::process::Output, std::io::Error> {
+ self.child.wait_with_output()
+ }
+}
+
+pub struct TestCommandOutput {
+ combined: Option<String>,
+ std_out_err: Option<(String, String)>,
+ exit_code: Option<i32>,
+ signal: Option<i32>,
+ asserted_stdout: RefCell<bool>,
+ asserted_stderr: RefCell<bool>,
+ asserted_combined: RefCell<bool>,
+ asserted_exit_code: RefCell<bool>,
+ // 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(text: &str) {
+ println!("OUTPUT\n{text}\nOUTPUT");
+ 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(combined);
+ }
+ }
+ if let Some((stdout, stderr)) = &self.std_out_err {
+ if !*self.asserted_stdout.borrow() && !stdout.is_empty() {
+ panic_unasserted_output(stdout);
+ }
+ if !*self.asserted_stderr.borrow() && !stderr.is_empty() {
+ panic_unasserted_output(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<i32> {
+ self.skip_exit_code_check();
+ self.exit_code
+ }
+
+ pub fn signal(&self) -> Option<i32> {
+ 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 {
+ println!("OUTPUT\n{combined}\nOUTPUT");
+ } else if let Some((stdout, stderr)) = &self.std_out_err {
+ println!("STDOUT OUTPUT\n{stdout}\nSTDOUT OUTPUT");
+ println!("STDERR OUTPUT\n{stderr}\nSTDERR OUTPUT");
+ }
+ }
+
+ #[track_caller]
+ pub fn assert_matches_text(&self, expected_text: impl AsRef<str>) -> &Self {
+ self.inner_assert_matches_text(self.combined_output(), expected_text)
+ }
+
+ #[track_caller]
+ pub fn assert_matches_file(&self, file_path: impl AsRef<Path>) -> &Self {
+ self.inner_assert_matches_file(self.combined_output(), file_path)
+ }
+
+ #[track_caller]
+ pub fn assert_stdout_matches_text(
+ &self,
+ expected_text: impl AsRef<str>,
+ ) -> &Self {
+ self.inner_assert_matches_text(self.stdout(), expected_text)
+ }
+
+ #[track_caller]
+ pub fn assert_stdout_matches_file(
+ &self,
+ file_path: impl AsRef<Path>,
+ ) -> &Self {
+ self.inner_assert_matches_file(self.stdout(), file_path)
+ }
+
+ #[track_caller]
+ pub fn assert_stderr_matches_text(
+ &self,
+ expected_text: impl AsRef<str>,
+ ) -> &Self {
+ self.inner_assert_matches_text(self.stderr(), expected_text)
+ }
+
+ #[track_caller]
+ pub fn assert_stderr_matches_file(
+ &self,
+ file_path: impl AsRef<Path>,
+ ) -> &Self {
+ self.inner_assert_matches_file(self.stderr(), file_path)
+ }
+
+ #[track_caller]
+ fn inner_assert_matches_text(
+ &self,
+ actual: &str,
+ expected: impl AsRef<str>,
+ ) -> &Self {
+ assert_wildcard_match(actual, expected.as_ref());
+ self
+ }
+
+ #[track_caller]
+ fn inner_assert_matches_file(
+ &self,
+ actual: &str,
+ file_path: impl AsRef<Path>,
+ ) -> &Self {
+ let output_path = testdata_path().join(file_path);
+ println!("output path {}", output_path);
+ let expected_text = output_path.read_to_string();
+ self.inner_assert_matches_text(actual, expected_text)
+ }
+}