mirror of
https://github.com/denoland/deno.git
synced 2024-11-30 16:40:57 -05:00
chore(tests): ability to capture stdout and stderr separately (#18035)
This is to allow making assertions on stdout and stderr separately.
This commit is contained in:
parent
f065380414
commit
bd6fd4ea5f
3 changed files with 184 additions and 50 deletions
|
@ -196,7 +196,7 @@ fn recursive_permissions_pledge() {
|
||||||
.run();
|
.run();
|
||||||
output.assert_exit_code(1);
|
output.assert_exit_code(1);
|
||||||
assert_contains!(
|
assert_contains!(
|
||||||
output.text(),
|
output.combined_output(),
|
||||||
"pledge test permissions called before restoring previous pledge"
|
"pledge test permissions called before restoring previous pledge"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
use super::check_unstable;
|
use super::check_unstable;
|
||||||
use super::signal;
|
|
||||||
use crate::permissions::PermissionsContainer;
|
use crate::permissions::PermissionsContainer;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::op;
|
use deno_core::op;
|
||||||
|
@ -565,7 +564,7 @@ mod deprecated {
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
|
pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
|
||||||
let signo = super::signal::signal_str_to_int(signal)?;
|
let signo = super::super::signal::signal_str_to_int(signal)?;
|
||||||
use nix::sys::signal::kill as unix_kill;
|
use nix::sys::signal::kill as unix_kill;
|
||||||
use nix::sys::signal::Signal;
|
use nix::sys::signal::Signal;
|
||||||
use nix::unistd::Pid;
|
use nix::unistd::Pid;
|
||||||
|
@ -593,8 +592,8 @@ mod deprecated {
|
||||||
} else if pid <= 0 {
|
} else if pid <= 0 {
|
||||||
Err(type_error("Invalid pid"))
|
Err(type_error("Invalid pid"))
|
||||||
} else {
|
} else {
|
||||||
// SAFETY: winapi call
|
|
||||||
let handle =
|
let handle =
|
||||||
|
// SAFETY: winapi call
|
||||||
unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) };
|
unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) };
|
||||||
|
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
|
|
|
@ -168,6 +168,7 @@ impl TestContext {
|
||||||
envs: Default::default(),
|
envs: Default::default(),
|
||||||
env_clear: Default::default(),
|
env_clear: Default::default(),
|
||||||
cwd: Default::default(),
|
cwd: Default::default(),
|
||||||
|
split_output: false,
|
||||||
context: self.clone(),
|
context: self.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,6 +182,7 @@ pub struct TestCommandBuilder {
|
||||||
envs: HashMap<String, String>,
|
envs: HashMap<String, String>,
|
||||||
env_clear: bool,
|
env_clear: bool,
|
||||||
cwd: Option<String>,
|
cwd: Option<String>,
|
||||||
|
split_output: bool,
|
||||||
context: TestContext,
|
context: TestContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,6 +207,15 @@ impl TestCommandBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Splits the output into stdout and stderr rather than having them combined.
|
||||||
|
pub fn split_output(&mut self) -> &mut 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 env(
|
pub fn env(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: impl AsRef<str>,
|
key: impl AsRef<str>,
|
||||||
|
@ -227,6 +238,23 @@ impl TestCommandBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self) -> TestCommandOutput {
|
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(text: String, args: &[String]) -> 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. 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") {
|
||||||
|
text = text.replace('\u{200B}', "");
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
let cwd = self.cwd.as_ref().or(self.context.cwd.as_ref());
|
let cwd = self.cwd.as_ref().or(self.context.cwd.as_ref());
|
||||||
let cwd = if self.context.use_temp_cwd {
|
let cwd = if self.context.use_temp_cwd {
|
||||||
assert!(cwd.is_none());
|
assert!(cwd.is_none());
|
||||||
|
@ -256,7 +284,6 @@ impl TestCommandBuilder {
|
||||||
arg.replace("$TESTDATA", &self.context.testdata_dir.to_string_lossy())
|
arg.replace("$TESTDATA", &self.context.testdata_dir.to_string_lossy())
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let (mut reader, writer) = pipe().unwrap();
|
|
||||||
let command_name = self
|
let command_name = self
|
||||||
.command_name
|
.command_name
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -284,9 +311,25 @@ impl TestCommandBuilder {
|
||||||
});
|
});
|
||||||
command.current_dir(cwd);
|
command.current_dir(cwd);
|
||||||
command.stdin(Stdio::piped());
|
command.stdin(Stdio::piped());
|
||||||
let writer_clone = writer.try_clone().unwrap();
|
|
||||||
command.stderr(writer_clone);
|
let (combined_reader, std_out_err_handle) = if self.split_output {
|
||||||
command.stdout(writer);
|
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().unwrap();
|
let mut process = command.spawn().unwrap();
|
||||||
|
|
||||||
|
@ -301,10 +344,16 @@ impl TestCommandBuilder {
|
||||||
// and dropping it closes them.
|
// and dropping it closes them.
|
||||||
drop(command);
|
drop(command);
|
||||||
|
|
||||||
let mut actual = String::new();
|
let combined = combined_reader
|
||||||
reader.read_to_string(&mut actual).unwrap();
|
.map(|pipe| sanitize_output(read_pipe_to_string(pipe), &args));
|
||||||
|
|
||||||
let status = process.wait().expect("failed to finish process");
|
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();
|
let exit_code = status.code();
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let signal = {
|
let signal = {
|
||||||
|
@ -314,33 +363,30 @@ impl TestCommandBuilder {
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
let signal = None;
|
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 {
|
TestCommandOutput {
|
||||||
exit_code,
|
exit_code,
|
||||||
signal,
|
signal,
|
||||||
text: actual,
|
combined,
|
||||||
|
std_out_err,
|
||||||
testdata_dir: self.context.testdata_dir.clone(),
|
testdata_dir: self.context.testdata_dir.clone(),
|
||||||
asserted_exit_code: RefCell::new(false),
|
asserted_exit_code: RefCell::new(false),
|
||||||
asserted_text: RefCell::new(false),
|
asserted_stdout: RefCell::new(false),
|
||||||
|
asserted_stderr: RefCell::new(false),
|
||||||
|
asserted_combined: RefCell::new(false),
|
||||||
_test_context: self.context.clone(),
|
_test_context: self.context.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestCommandOutput {
|
pub struct TestCommandOutput {
|
||||||
text: String,
|
combined: Option<String>,
|
||||||
|
std_out_err: Option<(String, String)>,
|
||||||
exit_code: Option<i32>,
|
exit_code: Option<i32>,
|
||||||
signal: Option<i32>,
|
signal: Option<i32>,
|
||||||
testdata_dir: PathBuf,
|
testdata_dir: PathBuf,
|
||||||
asserted_text: RefCell<bool>,
|
asserted_stdout: RefCell<bool>,
|
||||||
|
asserted_stderr: RefCell<bool>,
|
||||||
|
asserted_combined: RefCell<bool>,
|
||||||
asserted_exit_code: RefCell<bool>,
|
asserted_exit_code: RefCell<bool>,
|
||||||
// keep alive for the duration of the output reference
|
// keep alive for the duration of the output reference
|
||||||
_test_context: TestContext,
|
_test_context: TestContext,
|
||||||
|
@ -348,6 +394,17 @@ pub struct TestCommandOutput {
|
||||||
|
|
||||||
impl Drop for TestCommandOutput {
|
impl Drop for TestCommandOutput {
|
||||||
fn drop(&mut self) {
|
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 at {}. ",
|
||||||
|
"Call `output.skip_output_check()` to skip if necessary.",
|
||||||
|
),
|
||||||
|
failed_position()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if std::thread::panicking() {
|
if std::thread::panicking() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -359,15 +416,20 @@ impl Drop for TestCommandOutput {
|
||||||
failed_position(),
|
failed_position(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if !*self.asserted_text.borrow() && !self.text.is_empty() {
|
|
||||||
println!("OUTPUT\n{}\nOUTPUT", self.text);
|
// either the combined output needs to be asserted or both stdout and stderr
|
||||||
panic!(
|
if let Some(combined) = &self.combined {
|
||||||
concat!(
|
if !*self.asserted_combined.borrow() && !combined.is_empty() {
|
||||||
"The non-empty text of the command was not asserted. ",
|
panic_unasserted_output(combined);
|
||||||
"Call `output.skip_output_check()` to skip if necessary at {}.",
|
}
|
||||||
),
|
}
|
||||||
failed_position()
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,7 +440,9 @@ impl TestCommandOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skip_output_check(&self) {
|
pub fn skip_output_check(&self) {
|
||||||
*self.asserted_text.borrow_mut() = true;
|
*self.asserted_combined.borrow_mut() = true;
|
||||||
|
*self.asserted_stdout.borrow_mut() = true;
|
||||||
|
*self.asserted_stderr.borrow_mut() = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skip_exit_code_check(&self) {
|
pub fn skip_exit_code_check(&self) {
|
||||||
|
@ -394,9 +458,30 @@ impl TestCommandOutput {
|
||||||
self.signal
|
self.signal
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(&self) -> &str {
|
pub fn combined_output(&self) -> &str {
|
||||||
self.skip_output_check();
|
self.skip_output_check();
|
||||||
&self.text
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_exit_code(&self, expected_exit_code: i32) -> &Self {
|
pub fn assert_exit_code(&self, expected_exit_code: i32) -> &Self {
|
||||||
|
@ -404,7 +489,7 @@ impl TestCommandOutput {
|
||||||
|
|
||||||
if let Some(exit_code) = &actual_exit_code {
|
if let Some(exit_code) = &actual_exit_code {
|
||||||
if *exit_code != expected_exit_code {
|
if *exit_code != expected_exit_code {
|
||||||
println!("OUTPUT\n{}\nOUTPUT", self.text());
|
self.print_output();
|
||||||
panic!(
|
panic!(
|
||||||
"bad exit code, expected: {:?}, actual: {:?} at {}",
|
"bad exit code, expected: {:?}, actual: {:?} at {}",
|
||||||
expected_exit_code,
|
expected_exit_code,
|
||||||
|
@ -413,7 +498,7 @@ impl TestCommandOutput {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("OUTPUT\n{}\nOUTPUT", self.text());
|
self.print_output();
|
||||||
if let Some(signal) = self.signal() {
|
if let Some(signal) = self.signal() {
|
||||||
panic!(
|
panic!(
|
||||||
"process terminated by signal, expected exit code: {:?}, actual signal: {:?} at {}",
|
"process terminated by signal, expected exit code: {:?}, actual signal: {:?} at {}",
|
||||||
|
@ -433,29 +518,79 @@ impl TestCommandOutput {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_matches_text(&self, expected_text: impl AsRef<str>) -> &Self {
|
pub fn print_output(&self) {
|
||||||
let expected_text = expected_text.as_ref();
|
if let Some(combined) = &self.combined {
|
||||||
let actual = self.text();
|
println!("OUTPUT\n{combined}\nOUTPUT");
|
||||||
|
} else if let Some((stdout, stderr)) = &self.std_out_err {
|
||||||
if !expected_text.contains("[WILDCARD]") {
|
println!("STDOUT OUTPUT\n{stdout}\nSTDOUT OUTPUT");
|
||||||
assert_eq!(actual, expected_text, "at {}", failed_position());
|
println!("STDERR OUTPUT\n{stderr}\nSTDERR OUTPUT");
|
||||||
} else if !wildcard_match(expected_text, actual) {
|
|
||||||
println!("OUTPUT START\n{actual}\nOUTPUT END");
|
|
||||||
println!("EXPECTED START\n{expected_text}\nEXPECTED END");
|
|
||||||
panic!("pattern match failed at {}", failed_position());
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self
|
pub fn assert_matches_text(&self, expected_text: impl AsRef<str>) -> &Self {
|
||||||
|
self.inner_assert_matches_text(self.combined_output(), expected_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_matches_file(&self, file_path: impl AsRef<Path>) -> &Self {
|
pub fn assert_matches_file(&self, file_path: impl AsRef<Path>) -> &Self {
|
||||||
|
self.inner_assert_matches_file(self.combined_output(), file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_stdout_matches_text(
|
||||||
|
&self,
|
||||||
|
expected_text: impl AsRef<str>,
|
||||||
|
) -> &Self {
|
||||||
|
self.inner_assert_matches_text(self.stdout(), expected_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_stdout_matches_file(
|
||||||
|
&self,
|
||||||
|
file_path: impl AsRef<Path>,
|
||||||
|
) -> &Self {
|
||||||
|
self.inner_assert_matches_file(self.stdout(), file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_stderr_matches_text(
|
||||||
|
&self,
|
||||||
|
expected_text: impl AsRef<str>,
|
||||||
|
) -> &Self {
|
||||||
|
self.inner_assert_matches_text(self.stderr(), expected_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_stderrr_matches_file(
|
||||||
|
&self,
|
||||||
|
file_path: impl AsRef<Path>,
|
||||||
|
) -> &Self {
|
||||||
|
self.inner_assert_matches_file(self.stderr(), file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_assert_matches_text(
|
||||||
|
&self,
|
||||||
|
actual: &str,
|
||||||
|
expected: impl AsRef<str>,
|
||||||
|
) -> &Self {
|
||||||
|
let expected = expected.as_ref();
|
||||||
|
if !expected.contains("[WILDCARD]") {
|
||||||
|
assert_eq!(actual, expected, "at {}", failed_position());
|
||||||
|
} else if !wildcard_match(expected, actual) {
|
||||||
|
println!("OUTPUT START\n{actual}\nOUTPUT END");
|
||||||
|
println!("EXPECTED START\n{expected}\nEXPECTED END");
|
||||||
|
panic!("pattern match failed at {}", failed_position());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_assert_matches_file(
|
||||||
|
&self,
|
||||||
|
actual: &str,
|
||||||
|
file_path: impl AsRef<Path>,
|
||||||
|
) -> &Self {
|
||||||
let output_path = self.testdata_dir().join(file_path);
|
let output_path = self.testdata_dir().join(file_path);
|
||||||
println!("output path {}", output_path.display());
|
println!("output path {}", output_path.display());
|
||||||
let expected_text =
|
let expected_text =
|
||||||
std::fs::read_to_string(&output_path).unwrap_or_else(|err| {
|
std::fs::read_to_string(&output_path).unwrap_or_else(|err| {
|
||||||
panic!("failed loading {}\n\n{err:#}", output_path.display())
|
panic!("failed loading {}\n\n{err:#}", output_path.display())
|
||||||
});
|
});
|
||||||
self.assert_matches_text(expected_text)
|
self.inner_assert_matches_text(actual, expected_text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue