0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

fix(cli): ensure that pre- and post-test output is flushed at the appropriate times (#22611)

Some `deno_std` tests were failing to print output that was resolved
after the last test finished. In addition, output printed before tests
began would sometimes appear above the "running X tests ..." line, and
sometimes below it depending on timing.

We now guarantee that all output is flushed before and after tests run,
making the output consistent.

Pre-test and post-test output are captured in `------ pre-test output
------` and `------ post-test output ------` blocks to differentiate
them from the regular output blocks.

Here's an example of a test (that is much noisier than normal, but an
example of what the output will look like):

```
Check ./load_unload.ts
------- pre-test output -------
load
----- output end -----
running 1 test from ./load_unload.ts
test ...
------- output -------
test
----- output end -----
test ... ok ([WILDCARD])
------- post-test output -------
unload
----- output end -----
```
This commit is contained in:
Matt Mastracci 2024-02-28 15:12:21 -07:00 committed by GitHub
parent c9b2139b1e
commit b6e44f91ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 213 additions and 31 deletions

View file

@ -397,6 +397,9 @@ impl TestRun {
); );
} }
} }
test::TestEvent::Completed => {
reporter.report_completed();
}
test::TestEvent::ForceEndReport => {} test::TestEvent::ForceEndReport => {}
test::TestEvent::Sigint => {} test::TestEvent::Sigint => {}
} }
@ -742,6 +745,10 @@ impl LspTestReporter {
} }
} }
fn report_completed(&mut self) {
// there is nothing to do on report_completed
}
fn report_summary( fn report_summary(
&mut self, &mut self,
_summary: &test::TestSummary, _summary: &test::TestSummary,

View file

@ -384,6 +384,8 @@ impl TestEventSender {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tools::test::TestResult;
use super::*; use super::*;
use deno_core::unsync::spawn; use deno_core::unsync::spawn;
use deno_core::unsync::spawn_blocking; use deno_core::unsync::spawn_blocking;
@ -466,6 +468,75 @@ mod tests {
assert_eq!(messages.len(), 100000); assert_eq!(messages.len(), 100000);
} }
/// Test that large numbers of interleaved steps are routed properly.
#[tokio::test]
async fn test_interleave() {
test_util::timeout!(60);
const MESSAGE_COUNT: usize = 10_000;
let (mut worker, mut receiver) = create_single_test_event_channel();
let recv_handle = spawn(async move {
let mut i = 0;
while let Some((_, message)) = receiver.recv().await {
if i % 2 == 0 {
let expected_text = format!("{:08x}", i / 2).into_bytes();
let TestEvent::Output(TestStdioStream::Stderr, text) = message else {
panic!("Incorrect message: {message:?}");
};
assert_eq!(text, expected_text);
} else {
let TestEvent::Result(index, TestResult::Ok, 0) = message else {
panic!("Incorrect message: {message:?}");
};
assert_eq!(index, i / 2);
}
i += 1;
}
eprintln!("Receiver closed");
i
});
let send_handle: deno_core::unsync::JoinHandle<()> =
spawn_blocking(move || {
for i in 0..MESSAGE_COUNT {
worker
.stderr
.write_all(format!("{i:08x}").as_str().as_bytes())
.unwrap();
worker
.sender
.send(TestEvent::Result(i, TestResult::Ok, 0))
.unwrap();
}
eprintln!("Sent all messages");
});
send_handle.await.unwrap();
let messages = recv_handle.await.unwrap();
assert_eq!(messages, MESSAGE_COUNT * 2);
}
#[tokio::test]
async fn test_sender_shutdown_before_receive() {
test_util::timeout!(60);
for _ in 0..10 {
let (mut worker, mut receiver) = create_single_test_event_channel();
worker.stderr.write_all(b"hello").unwrap();
worker
.sender
.send(TestEvent::Result(0, TestResult::Ok, 0))
.unwrap();
drop(worker);
let (_, message) = receiver.recv().await.unwrap();
let TestEvent::Output(TestStdioStream::Stderr, text) = message else {
panic!("Incorrect message: {message:?}");
};
assert_eq!(text.as_slice(), b"hello");
let (_, message) = receiver.recv().await.unwrap();
let TestEvent::Result(..) = message else {
panic!("Incorrect message: {message:?}");
};
assert!(receiver.recv().await.is_none());
}
}
/// Ensure nothing panics if we're racing the runtime shutdown. /// Ensure nothing panics if we're racing the runtime shutdown.
#[test] #[test]
fn test_runtime_shutdown() { fn test_runtime_shutdown() {

View file

@ -391,8 +391,14 @@ pub enum TestEvent {
StepRegister(TestStepDescription), StepRegister(TestStepDescription),
StepWait(usize), StepWait(usize),
StepResult(usize, TestStepResult, u64), StepResult(usize, TestStepResult, u64),
ForceEndReport, /// Indicates that this worker has completed.
Completed,
/// Indicates that the user has cancelled the test run with Ctrl+C and
/// the run should be aborted.
Sigint, Sigint,
/// Used by the REPL to force a report to end without closing the worker
/// or receiver.
ForceEndReport,
} }
impl TestEvent { impl TestEvent {
@ -401,11 +407,13 @@ impl TestEvent {
pub fn requires_stdio_sync(&self) -> bool { pub fn requires_stdio_sync(&self) -> bool {
matches!( matches!(
self, self,
TestEvent::Result(..) TestEvent::Plan(..)
| TestEvent::Result(..)
| TestEvent::StepWait(..) | TestEvent::StepWait(..)
| TestEvent::StepResult(..) | TestEvent::StepResult(..)
| TestEvent::UncaughtError(..) | TestEvent::UncaughtError(..)
| TestEvent::ForceEndReport | TestEvent::ForceEndReport
| TestEvent::Completed
) )
} }
} }
@ -641,24 +649,64 @@ pub async fn run_tests_for_worker(
}; };
let tests: Arc<TestDescriptions> = tests.into(); let tests: Arc<TestDescriptions> = tests.into();
sender.send(TestEvent::Register(tests.clone()))?; sender.send(TestEvent::Register(tests.clone()))?;
let res = run_tests_for_worker_inner(
worker,
specifier,
tests,
test_functions,
&mut sender,
options,
fail_fast_tracker,
)
.await;
_ = sender.send(TestEvent::Completed);
res
}
async fn run_tests_for_worker_inner(
worker: &mut MainWorker,
specifier: &ModuleSpecifier,
tests: Arc<TestDescriptions>,
test_functions: Vec<v8::Global<v8::Function>>,
sender: &mut TestEventSender,
options: &TestSpecifierOptions,
fail_fast_tracker: &FailFastTracker,
) -> Result<(), AnyError> {
let unfiltered = tests.len(); let unfiltered = tests.len();
let tests = tests
.into_iter() // Build the test plan in a single pass
.filter(|(_, d)| options.filter.includes(&d.name)) let mut tests_to_run = Vec::with_capacity(tests.len());
.collect::<Vec<_>>(); let mut used_only = false;
let (only, no_only): (Vec<_>, Vec<_>) = for ((_, d), f) in tests.tests.iter().zip(test_functions) {
tests.into_iter().partition(|(_, d)| d.only); if !options.filter.includes(&d.name) {
let used_only = !only.is_empty(); continue;
let mut tests = if used_only { only } else { no_only }; }
if let Some(seed) = options.shuffle {
tests.shuffle(&mut SmallRng::seed_from_u64(seed)); // If we've seen an "only: true" test, the remaining tests must be "only: true" to be added
if used_only && !d.only {
continue;
}
// If this is the first "only: true" test we've seen, clear the other tests since they were
// only: false.
if d.only && !used_only {
used_only = true;
tests_to_run.clear();
}
tests_to_run.push((d, f));
} }
if let Some(seed) = options.shuffle {
tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed));
}
sender.send(TestEvent::Plan(TestPlan { sender.send(TestEvent::Plan(TestPlan {
origin: specifier.to_string(), origin: specifier.to_string(),
total: tests.len(), total: tests_to_run.len(),
filtered_out: unfiltered - tests.len(), filtered_out: unfiltered - tests_to_run.len(),
used_only, used_only,
}))?; }))?;
let mut had_uncaught_error = false; let mut had_uncaught_error = false;
let stats = worker.js_runtime.runtime_activity_stats_factory(); let stats = worker.js_runtime.runtime_activity_stats_factory();
let ops = worker.js_runtime.op_names(); let ops = worker.js_runtime.op_names();
@ -683,7 +731,7 @@ pub async fn run_tests_for_worker(
filter = filter.omit_op(op_id_host_recv_ctrl as _); filter = filter.omit_op(op_id_host_recv_ctrl as _);
filter = filter.omit_op(op_id_host_recv_message as _); filter = filter.omit_op(op_id_host_recv_message as _);
for ((_, desc), function) in tests.into_iter().zip(test_functions) { for (desc, function) in tests_to_run.into_iter() {
if fail_fast_tracker.should_stop() { if fail_fast_tracker.should_stop() {
break; break;
} }
@ -1254,6 +1302,9 @@ pub async fn report_tests(
TestEvent::ForceEndReport => { TestEvent::ForceEndReport => {
break; break;
} }
TestEvent::Completed => {
reporter.report_completed();
}
TestEvent::Sigint => { TestEvent::Sigint => {
let elapsed = start_time let elapsed = start_time
.map(|t| Instant::now().duration_since(t)) .map(|t| Instant::now().duration_since(t))

View file

@ -101,6 +101,12 @@ impl TestReporter for CompoundTestReporter {
} }
} }
fn report_completed(&mut self) {
for reporter in &mut self.test_reporters {
reporter.report_completed();
}
}
fn flush_report( fn flush_report(
&mut self, &mut self,
elapsed: &Duration, elapsed: &Duration,

View file

@ -206,6 +206,8 @@ impl TestReporter for DotTestReporter {
); );
} }
fn report_completed(&mut self) {}
fn flush_report( fn flush_report(
&mut self, &mut self,
_elapsed: &Duration, _elapsed: &Duration,

View file

@ -158,6 +158,11 @@ impl TestReporter for JunitTestReporter {
} }
} }
fn report_completed(&mut self) {
// TODO(mmastrac): This reporter does not handle stdout/stderr yet, and when we do, we may need to redirect
// pre-and-post-test output somewhere.
}
fn flush_report( fn flush_report(
&mut self, &mut self,
elapsed: &Duration, elapsed: &Duration,

View file

@ -49,6 +49,7 @@ pub trait TestReporter {
tests: &IndexMap<usize, TestDescription>, tests: &IndexMap<usize, TestDescription>,
test_steps: &IndexMap<usize, TestStepDescription>, test_steps: &IndexMap<usize, TestStepDescription>,
); );
fn report_completed(&mut self);
fn flush_report( fn flush_report(
&mut self, &mut self,
elapsed: &Duration, elapsed: &Duration,

View file

@ -8,12 +8,14 @@ pub struct PrettyTestReporter {
parallel: bool, parallel: bool,
echo_output: bool, echo_output: bool,
in_new_line: bool, in_new_line: bool,
phase: &'static str,
filter: bool, filter: bool,
repl: bool, repl: bool,
scope_test_id: Option<usize>, scope_test_id: Option<usize>,
cwd: Url, cwd: Url,
did_have_user_output: bool, did_have_user_output: bool,
started_tests: bool, started_tests: bool,
ended_tests: bool,
child_results_buffer: child_results_buffer:
HashMap<usize, IndexMap<usize, (TestStepDescription, TestStepResult, u64)>>, HashMap<usize, IndexMap<usize, (TestStepDescription, TestStepResult, u64)>>,
summary: TestSummary, summary: TestSummary,
@ -31,12 +33,14 @@ impl PrettyTestReporter {
parallel, parallel,
echo_output, echo_output,
in_new_line: true, in_new_line: true,
phase: "",
filter, filter,
repl, repl,
scope_test_id: None, scope_test_id: None,
cwd: Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(), cwd: Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(),
did_have_user_output: false, did_have_user_output: false,
started_tests: false, started_tests: false,
ended_tests: false,
child_results_buffer: Default::default(), child_results_buffer: Default::default(),
summary: TestSummary::new(), summary: TestSummary::new(),
writer: Box::new(std::io::stdout()), writer: Box::new(std::io::stdout()),
@ -149,7 +153,7 @@ impl PrettyTestReporter {
writeln!( writeln!(
&mut self.writer, &mut self.writer,
"{}", "{}",
colors::gray("----- output end -----") colors::gray(format!("----- {}output end -----", self.phase))
) )
.unwrap(); .unwrap();
self.in_new_line = true; self.in_new_line = true;
@ -161,6 +165,7 @@ impl PrettyTestReporter {
impl TestReporter for PrettyTestReporter { impl TestReporter for PrettyTestReporter {
fn report_register(&mut self, _description: &TestDescription) {} fn report_register(&mut self, _description: &TestDescription) {}
fn report_plan(&mut self, plan: &TestPlan) { fn report_plan(&mut self, plan: &TestPlan) {
self.write_output_end();
self.summary.total += plan.total; self.summary.total += plan.total;
self.summary.filtered_out += plan.filtered_out; self.summary.filtered_out += plan.filtered_out;
if self.repl { if self.repl {
@ -196,15 +201,22 @@ impl TestReporter for PrettyTestReporter {
return; return;
} }
if !self.did_have_user_output && self.started_tests { if !self.did_have_user_output {
self.did_have_user_output = true; self.did_have_user_output = true;
if !self.in_new_line { if !self.in_new_line {
writeln!(&mut self.writer).unwrap(); writeln!(&mut self.writer).unwrap();
} }
self.phase = if !self.started_tests {
"pre-test "
} else if self.ended_tests {
"post-test "
} else {
""
};
writeln!( writeln!(
&mut self.writer, &mut self.writer,
"{}", "{}",
colors::gray("------- output -------") colors::gray(format!("------- {}output -------", self.phase))
) )
.unwrap(); .unwrap();
self.in_new_line = true; self.in_new_line = true;
@ -369,6 +381,7 @@ impl TestReporter for PrettyTestReporter {
_tests: &IndexMap<usize, TestDescription>, _tests: &IndexMap<usize, TestDescription>,
_test_steps: &IndexMap<usize, TestStepDescription>, _test_steps: &IndexMap<usize, TestStepDescription>,
) { ) {
self.write_output_end();
common::report_summary(&mut self.writer, &self.cwd, &self.summary, elapsed); common::report_summary(&mut self.writer, &self.cwd, &self.summary, elapsed);
if !self.repl { if !self.repl {
writeln!(&mut self.writer).unwrap(); writeln!(&mut self.writer).unwrap();
@ -392,6 +405,11 @@ impl TestReporter for PrettyTestReporter {
self.in_new_line = true; self.in_new_line = true;
} }
fn report_completed(&mut self) {
self.write_output_end();
self.ended_tests = true;
}
fn flush_report( fn flush_report(
&mut self, &mut self,
_elapsed: &Duration, _elapsed: &Duration,

View file

@ -236,6 +236,8 @@ impl TestReporter for TapTestReporter {
); );
} }
fn report_completed(&mut self) {}
fn flush_report( fn flush_report(
&mut self, &mut self,
_elapsed: &Duration, _elapsed: &Duration,

View file

@ -4560,15 +4560,14 @@ async fn websocket_server_multi_field_connection_header() {
assert!(child.wait().unwrap().success()); assert!(child.wait().unwrap().success());
} }
// TODO(bartlomieju): this should use `deno run`, not `deno test`; but the
// test hangs then. https://github.com/denoland/deno/issues/14283
#[tokio::test] #[tokio::test]
async fn websocket_server_idletimeout() { async fn websocket_server_idletimeout() {
test_util::timeout!(60);
let script = let script =
util::testdata_path().join("run/websocket_server_idletimeout.ts"); util::testdata_path().join("run/websocket_server_idletimeout.ts");
let root_ca = util::testdata_path().join("tls/RootCA.pem"); let root_ca = util::testdata_path().join("tls/RootCA.pem");
let mut child = util::deno_cmd() let mut child = util::deno_cmd()
.arg("test") .arg("run")
.arg("--unstable") .arg("--unstable")
.arg("--allow-net") .arg("--allow-net")
.arg("--cert") .arg("--cert")
@ -4579,11 +4578,13 @@ async fn websocket_server_idletimeout() {
.unwrap(); .unwrap();
let stdout = child.stdout.as_mut().unwrap(); let stdout = child.stdout.as_mut().unwrap();
let mut buffer = [0; 5]; let mut buf: Vec<u8> = vec![];
let read = stdout.read(&mut buffer).unwrap(); while !String::from_utf8(buf.clone()).unwrap().contains("READY") {
assert_eq!(read, 5); let mut buffer = [0; 64];
let msg = std::str::from_utf8(&buffer).unwrap(); let read = stdout.read(&mut buffer).unwrap();
assert_eq!(msg, "READY"); buf.extend_from_slice(&buffer[0..read]);
eprintln!("buf = {buf:?}");
}
let stream = tokio::net::TcpStream::connect("localhost:4509") let stream = tokio::net::TcpStream::connect("localhost:4509")
.await .await
@ -4604,8 +4605,7 @@ async fn websocket_server_idletimeout() {
fastwebsockets::handshake::client(&SpawnExecutor, req, stream) fastwebsockets::handshake::client(&SpawnExecutor, req, stream)
.await .await
.unwrap(); .unwrap();
assert_eq!(child.wait().unwrap().code(), Some(123));
assert!(child.wait().unwrap().success());
} }
itest!(auto_discover_lockfile { itest!(auto_discover_lockfile {

View file

@ -11,10 +11,12 @@ const { response, socket } = Deno.upgradeWebSocket(request, {
idleTimeout: 1, idleTimeout: 1,
}); });
socket.onerror = (e) => { socket.onerror = (e) => {
console.log(e);
assertEquals((e as ErrorEvent).message, "No response from ping frame."); assertEquals((e as ErrorEvent).message, "No response from ping frame.");
errorDeferred.resolve(); errorDeferred.resolve();
}; };
socket.onclose = (e) => { socket.onclose = (e) => {
console.log(e);
assertEquals(e.reason, "No response from ping frame."); assertEquals(e.reason, "No response from ping frame.");
closeDeferred.resolve(); closeDeferred.resolve();
}; };
@ -22,4 +24,6 @@ await respondWith(response);
await errorDeferred.promise; await errorDeferred.promise;
await closeDeferred.promise; await closeDeferred.promise;
listener.close();
// TODO(mmastrac): this doesn't exit on its own. Why?
Deno.exit(123);

View file

@ -1,6 +1,16 @@
Check [WILDCARD]/test/load_unload.ts Check [WILDCARD]/load_unload.ts
running 1 test from ./test/load_unload.ts ------- pre-test output -------
load
----- pre-test output end -----
running 1 test from [WILDCARD]/load_unload.ts
test ...
------- output -------
test
----- output end -----
test ... ok ([WILDCARD]) test ... ok ([WILDCARD])
------- post-test output -------
unload
----- post-test output end -----
ok | 1 passed | 0 failed ([WILDCARD]) ok | 1 passed | 0 failed ([WILDCARD])

View file

@ -4,6 +4,7 @@ addEventListener("load", () => {
throw new Error("Interval is already set"); throw new Error("Interval is already set");
} }
console.log("load");
interval = setInterval(() => {}, 0); interval = setInterval(() => {}, 0);
}); });
@ -12,10 +13,12 @@ addEventListener("unload", () => {
throw new Error("Interval was not set"); throw new Error("Interval was not set");
} }
console.log("unload");
clearInterval(interval); clearInterval(interval);
}); });
Deno.test("test", () => { Deno.test("test", () => {
console.log("test");
if (!interval) { if (!interval) {
throw new Error("Interval was not set"); throw new Error("Interval was not set");
} }

View file

@ -30,7 +30,9 @@ macro_rules! timeout {
let timeout = *timeout.get(0).unwrap_or(&300); let timeout = *timeout.get(0).unwrap_or(&300);
::std::thread::spawn(move || { ::std::thread::spawn(move || {
if rx.recv_timeout(::std::time::Duration::from_secs(timeout)) == Err(::std::sync::mpsc::RecvTimeoutError::Timeout) { if rx.recv_timeout(::std::time::Duration::from_secs(timeout)) == Err(::std::sync::mpsc::RecvTimeoutError::Timeout) {
use std::io::Write;
eprintln!("Test {function} timed out after {timeout} seconds, aborting"); eprintln!("Test {function} timed out after {timeout} seconds, aborting");
_ = std::io::stderr().flush();
::std::process::exit(1); ::std::process::exit(1);
} }
}); });