1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-26 00:59:24 -05:00

fix(test): actually capture stdout and stderr in workers (#14435)

This commit is contained in:
David Sherret 2022-05-01 14:44:55 -04:00 committed by GitHub
parent 671f56f8ff
commit de33017a8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 94 deletions

View file

@ -16,6 +16,7 @@ use crate::lsp::logging::lsp_log;
use crate::ops; use crate::ops;
use crate::proc_state; use crate::proc_state;
use crate::tools::test; use crate::tools::test;
use crate::tools::test::TestEventSender;
use deno_core::anyhow::anyhow; use deno_core::anyhow::anyhow;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -180,21 +181,20 @@ async fn test_specifier(
permissions: Permissions, permissions: Permissions,
specifier: ModuleSpecifier, specifier: ModuleSpecifier,
mode: test::TestMode, mode: test::TestMode,
channel: mpsc::UnboundedSender<test::TestEvent>, sender: TestEventSender,
token: CancellationToken, token: CancellationToken,
options: Option<Value>, options: Option<Value>,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
if !token.is_cancelled() { if !token.is_cancelled() {
let (stdout, stderr) = test::create_stdout_stderr_pipes(channel.clone());
let mut worker = create_main_worker( let mut worker = create_main_worker(
&ps, &ps,
specifier.clone(), specifier.clone(),
permissions, permissions,
vec![ops::testing::init(channel.clone())], vec![ops::testing::init(sender.clone())],
Stdio { Stdio {
stdin: StdioPipe::Inherit, stdin: StdioPipe::Inherit,
stdout: StdioPipe::File(stdout), stdout: StdioPipe::File(sender.stdout()),
stderr: StdioPipe::File(stderr), stderr: StdioPipe::File(sender.stderr()),
}, },
); );
@ -318,6 +318,7 @@ impl TestRun {
.await?; .await?;
let (sender, mut receiver) = mpsc::unbounded_channel::<test::TestEvent>(); let (sender, mut receiver) = mpsc::unbounded_channel::<test::TestEvent>();
let sender = TestEventSender::new(sender);
let (concurrent_jobs, fail_fast) = let (concurrent_jobs, fail_fast) =
if let flags::DenoSubcommand::Test(test_flags) = &ps.flags.subcommand { if let flags::DenoSubcommand::Test(test_flags) = &ps.flags.subcommand {
@ -754,19 +755,14 @@ impl test::TestReporter for LspTestReporter {
self.progress(lsp_custom::TestRunProgressMessage::Started { test }); self.progress(lsp_custom::TestRunProgressMessage::Started { test });
} }
fn report_output(&mut self, output: &test::TestOutput) { fn report_output(&mut self, output: &[u8]) {
let test = self.current_origin.as_ref().and_then(|origin| { let test = self.current_origin.as_ref().and_then(|origin| {
self self
.stack .stack
.get(origin) .get(origin)
.and_then(|v| v.last().map(|td| td.into())) .and_then(|v| v.last().map(|td| td.into()))
}); });
let value = match output { let value = String::from_utf8_lossy(output).replace('\n', "\r\n");
test::TestOutput::String(value) => value.replace('\n', "\r\n"),
test::TestOutput::Bytes(bytes) => {
String::from_utf8_lossy(bytes).replace('\n', "\r\n")
}
};
self.progress(lsp_custom::TestRunProgressMessage::Output { self.progress(lsp_custom::TestRunProgressMessage::Output {
value, value,

View file

@ -1,7 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use crate::tools::test::TestEvent; use crate::tools::test::TestEvent;
use crate::tools::test::TestOutput; use crate::tools::test::TestEventSender;
use deno_core::error::generic_error; use deno_core::error::generic_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -12,10 +12,9 @@ use deno_core::OpState;
use deno_runtime::permissions::create_child_permissions; use deno_runtime::permissions::create_child_permissions;
use deno_runtime::permissions::ChildPermissionsArg; use deno_runtime::permissions::ChildPermissionsArg;
use deno_runtime::permissions::Permissions; use deno_runtime::permissions::Permissions;
use tokio::sync::mpsc::UnboundedSender;
use uuid::Uuid; use uuid::Uuid;
pub fn init(sender: UnboundedSender<TestEvent>) -> Extension { pub fn init(sender: TestEventSender) -> Extension {
Extension::builder() Extension::builder()
.ops(vec![ .ops(vec![
op_pledge_test_permissions::decl(), op_pledge_test_permissions::decl(),
@ -23,10 +22,6 @@ pub fn init(sender: UnboundedSender<TestEvent>) -> Extension {
op_get_test_origin::decl(), op_get_test_origin::decl(),
op_dispatch_test_event::decl(), op_dispatch_test_event::decl(),
]) ])
.middleware(|op| match op.name {
"op_print" => op_print::decl(),
_ => op,
})
.state(move |state| { .state(move |state| {
state.put(sender.clone()); state.put(sender.clone());
Ok(()) Ok(())
@ -86,19 +81,7 @@ fn op_dispatch_test_event(
state: &mut OpState, state: &mut OpState,
event: TestEvent, event: TestEvent,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let sender = state.borrow::<UnboundedSender<TestEvent>>().clone(); let mut sender = state.borrow::<TestEventSender>().clone();
sender.send(event).ok(); sender.send(event).ok();
Ok(()) Ok(())
} }
#[op]
pub fn op_print(
state: &mut OpState,
msg: String,
_is_err: bool,
) -> Result<(), AnyError> {
let sender = state.borrow::<UnboundedSender<TestEvent>>().clone();
let msg = TestOutput::String(msg);
sender.send(TestEvent::Output(msg)).ok();
Ok(())
}

View file

@ -302,11 +302,37 @@ itest!(no_prompt_with_denied_perms {
output: "test/no_prompt_with_denied_perms.out", output: "test/no_prompt_with_denied_perms.out",
}); });
itest!(captured_output { #[test]
args: "test --allow-run --allow-read --unstable test/captured_output.ts", fn captured_output() {
exit_code: 0, let output = util::deno_cmd()
output: "test/captured_output.out", .current_dir(util::testdata_path())
}); .arg("test")
.arg("--allow-run")
.arg("--allow-read")
.arg("--unstable")
.arg("test/captured_output.ts")
.env("NO_COLOR", "1")
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
let output_start = "------- output -------";
let output_end = "----- output end -----";
assert!(output.status.success());
let output_text = String::from_utf8(output.stdout).unwrap();
let start = output_text.find(output_start).unwrap() + output_start.len();
let end = output_text.find(output_end).unwrap();
let output_text = output_text[start..end].trim();
let mut lines = output_text.lines().collect::<Vec<_>>();
// the output is racy on either stdout or stderr being flushed
// from the runtime into the rust code, so sort it... the main
// thing here to ensure is that we're capturing the output in
// this block on stdout
lines.sort_unstable();
assert_eq!(lines.join(" "), "0 1 2 3 4 5 6 7 8 9");
}
#[test] #[test]
fn recursive_permissions_pledge() { fn recursive_permissions_pledge() {

View file

@ -1,19 +0,0 @@
[WILDCARD]
running 1 test from [WILDCARD]/captured_output.ts
output ...
------- output -------
1
2
3
4
5
6
7
8
9
10
----- output end -----
ok ([WILDCARD]s)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ([WILDCARD]s)
[WILDCARD]

View file

@ -1,21 +1,21 @@
Deno.test("output", async () => { Deno.test("output", async () => {
const p = Deno.run({ const p = Deno.run({
cmd: [Deno.execPath(), "eval", "console.log(1); console.error(2);"], cmd: [Deno.execPath(), "eval", "console.log(0); console.error(1);"],
}); });
await p.status(); await p.status();
await p.close(); await p.close();
Deno.spawnSync(Deno.execPath(), { Deno.spawnSync(Deno.execPath(), {
args: ["eval", "console.log(3); console.error(4);"], args: ["eval", "console.log(2); console.error(3);"],
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",
}); });
await Deno.spawn(Deno.execPath(), { await Deno.spawn(Deno.execPath(), {
args: ["eval", "console.log(5); console.error(6);"], args: ["eval", "console.log(4); console.error(5);"],
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",
}); });
const c = await Deno.spawnChild(Deno.execPath(), { const c = await Deno.spawnChild(Deno.execPath(), {
args: ["eval", "console.log(7); console.error(8);"], args: ["eval", "console.log(6); console.error(7);"],
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",
}); });

View file

@ -1,6 +1,6 @@
self.onmessage = () => { self.onmessage = () => {
console.log(9); console.log(8);
console.error(10); console.error(9);
self.postMessage({}); self.postMessage({});
self.close(); self.close();
}; };

View file

@ -138,7 +138,7 @@ pub struct TestPlan {
pub enum TestEvent { pub enum TestEvent {
Plan(TestPlan), Plan(TestPlan),
Wait(TestDescription), Wait(TestDescription),
Output(TestOutput), Output(Vec<u8>),
Result(TestDescription, TestResult, u64), Result(TestDescription, TestResult, u64),
StepWait(TestStepDescription), StepWait(TestStepDescription),
StepResult(TestStepDescription, TestStepResult, u64), StepResult(TestStepDescription, TestStepResult, u64),
@ -198,7 +198,7 @@ impl TestSummary {
pub trait TestReporter { pub trait TestReporter {
fn report_plan(&mut self, plan: &TestPlan); fn report_plan(&mut self, plan: &TestPlan);
fn report_wait(&mut self, description: &TestDescription); fn report_wait(&mut self, description: &TestDescription);
fn report_output(&mut self, output: &TestOutput); fn report_output(&mut self, output: &[u8]);
fn report_result( fn report_result(
&mut self, &mut self,
description: &TestDescription, description: &TestDescription,
@ -340,7 +340,7 @@ impl TestReporter for PrettyTestReporter {
self.in_test_count += 1; self.in_test_count += 1;
} }
fn report_output(&mut self, output: &TestOutput) { fn report_output(&mut self, output: &[u8]) {
if !self.echo_output { if !self.echo_output {
return; return;
} }
@ -350,16 +350,10 @@ impl TestReporter for PrettyTestReporter {
println!(); println!();
println!("{}", colors::gray("------- output -------")); println!("{}", colors::gray("------- output -------"));
} }
match output {
TestOutput::String(line) => {
// output everything to stdout in order to prevent // output everything to stdout in order to prevent
// stdout and stderr racing // stdout and stderr racing
print!("{}", line) std::io::stdout().write_all(output).unwrap();
}
TestOutput::Bytes(bytes) => {
std::io::stdout().write_all(bytes).unwrap();
}
}
} }
fn report_result( fn report_result(
@ -587,19 +581,18 @@ async fn test_specifier(
permissions: Permissions, permissions: Permissions,
specifier: ModuleSpecifier, specifier: ModuleSpecifier,
mode: TestMode, mode: TestMode,
channel: UnboundedSender<TestEvent>, sender: TestEventSender,
options: TestSpecifierOptions, options: TestSpecifierOptions,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let (stdout, stderr) = create_stdout_stderr_pipes(channel.clone());
let mut worker = create_main_worker( let mut worker = create_main_worker(
&ps, &ps,
specifier.clone(), specifier.clone(),
permissions, permissions,
vec![ops::testing::init(channel.clone())], vec![ops::testing::init(sender.clone())],
Stdio { Stdio {
stdin: StdioPipe::Inherit, stdin: StdioPipe::Inherit,
stdout: StdioPipe::File(stdout), stdout: StdioPipe::File(sender.stdout()),
stderr: StdioPipe::File(stderr), stderr: StdioPipe::File(sender.stderr()),
}, },
); );
@ -951,6 +944,7 @@ async fn test_specifiers(
}; };
let (sender, mut receiver) = unbounded_channel::<TestEvent>(); let (sender, mut receiver) = unbounded_channel::<TestEvent>();
let sender = TestEventSender::new(sender);
let concurrent_jobs = options.concurrent_jobs; let concurrent_jobs = options.concurrent_jobs;
let fail_fast = options.fail_fast; let fail_fast = options.fail_fast;
@ -1455,20 +1449,61 @@ pub async fn run_tests_with_watch(
Ok(()) Ok(())
} }
/// Creates the stdout and stderr pipes and returns the writers for stdout and stderr. pub struct TestEventSender {
pub fn create_stdout_stderr_pipes(
sender: UnboundedSender<TestEvent>, sender: UnboundedSender<TestEvent>,
) -> (std::fs::File, std::fs::File) { stdout_writer: os_pipe::PipeWriter,
stderr_writer: os_pipe::PipeWriter,
}
impl Clone for TestEventSender {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
stdout_writer: self.stdout_writer.try_clone().unwrap(),
stderr_writer: self.stderr_writer.try_clone().unwrap(),
}
}
}
impl TestEventSender {
pub fn new(sender: UnboundedSender<TestEvent>) -> Self {
let (stdout_reader, stdout_writer) = os_pipe::pipe().unwrap(); let (stdout_reader, stdout_writer) = os_pipe::pipe().unwrap();
let (stderr_reader, stderr_writer) = os_pipe::pipe().unwrap(); let (stderr_reader, stderr_writer) = os_pipe::pipe().unwrap();
start_output_redirect_thread(stdout_reader, sender.clone()); start_output_redirect_thread(stdout_reader, sender.clone());
start_output_redirect_thread(stderr_reader, sender); start_output_redirect_thread(stderr_reader, sender.clone());
( Self {
pipe_writer_to_file(stdout_writer), sender,
pipe_writer_to_file(stderr_writer), stdout_writer,
) stderr_writer,
}
}
pub fn stdout(&self) -> std::fs::File {
pipe_writer_to_file(self.stdout_writer.try_clone().unwrap())
}
pub fn stderr(&self) -> std::fs::File {
pipe_writer_to_file(self.stderr_writer.try_clone().unwrap())
}
pub fn send(&mut self, message: TestEvent) -> Result<(), AnyError> {
// for any event that finishes collecting output, we need to
// ensure that the collected stdout and stderr pipes are flushed
if matches!(
message,
TestEvent::Result(_, _, _)
| TestEvent::StepWait(_)
| TestEvent::StepResult(_, _, _)
) {
self.stdout_writer.flush().unwrap();
self.stderr_writer.flush().unwrap();
}
self.sender.send(message)?;
Ok(())
}
} }
#[cfg(windows)] #[cfg(windows)]
@ -1497,10 +1532,9 @@ fn start_output_redirect_thread(
Ok(0) | Err(_) => break, Ok(0) | Err(_) => break,
Ok(size) => size, Ok(size) => size,
}; };
if sender if sender
.send(TestEvent::Output(TestOutput::Bytes( .send(TestEvent::Output(buffer[0..size].to_vec()))
buffer[0..size].to_vec(),
)))
.is_err() .is_err()
{ {
break; break;

View file

@ -111,6 +111,10 @@ pub fn init_stdio(stdio: Stdio) -> Extension {
let stdio = Rc::new(RefCell::new(Some(stdio))); let stdio = Rc::new(RefCell::new(Some(stdio)));
Extension::builder() Extension::builder()
.middleware(|op| match op.name {
"op_print" => op_print::decl(),
_ => op,
})
.state(move |state| { .state(move |state| {
let stdio = stdio let stdio = stdio
.borrow_mut() .borrow_mut()
@ -419,6 +423,24 @@ impl Resource for StdFileResource {
} }
} }
// override op_print to use the stdout and stderr in the resource table
#[op]
pub fn op_print(
state: &mut OpState,
msg: String,
is_err: bool,
) -> Result<(), AnyError> {
let rid = if is_err { 2 } else { 1 };
StdFileResource::with(state, rid, move |r| match r {
Ok(std_file) => {
std_file.write_all(msg.as_bytes())?;
std_file.flush().unwrap();
Ok(())
}
Err(_) => Err(not_supported()),
})
}
#[op] #[op]
fn op_read_sync( fn op_read_sync(
state: &mut OpState, state: &mut OpState,