diff --git a/crates/fe/src/test/mod.rs b/crates/fe/src/test/mod.rs index 2f8a0f42df..a729c9dbbc 100644 --- a/crates/fe/src/test/mod.rs +++ b/crates/fe/src/test/mod.rs @@ -629,6 +629,7 @@ struct SingleRunResult { suite_key: String, symbol_name: String, result: TestResult, + output: String, measurement: Option, elapsed: Duration, } @@ -888,7 +889,15 @@ impl OutcomeCollectorState { } JobOutcome::SingleFinished(single) => { let single = *single; - let suite_key = single.suite_key.clone(); + let suite_key = single.suite_key; + let kind = if single.result.passed { + StreamStatusKind::Pass + } else { + StreamStatusKind::Fail + }; + let elapsed = single.elapsed; + let name = single.result.name.clone(); + let output = single.output; let suite_is_complete = { let state = self.pending_suites.get_mut(&suite_key).ok_or_else(|| { format!("received single test outcome for unknown suite `{suite_key}`") @@ -902,6 +911,7 @@ impl OutcomeCollectorState { } state.completed_singles == state.expected_singles }; + self.emit_single_output(&suite_key, kind, elapsed, &name, &output, ctx)?; if suite_is_complete { self.finalize_pending_suite(&suite_key, ctx)?; } @@ -910,6 +920,47 @@ impl OutcomeCollectorState { Ok(()) } + fn emit_single_output( + &mut self, + suite_key: &str, + kind: StreamStatusKind, + elapsed: Duration, + name: &str, + output: &str, + ctx: &OutcomeContext<'_>, + ) -> Result<(), String> { + if self.buffer_grouped_output { + let suite_idx = *self.suite_index_by_key.get(suite_key).ok_or_else(|| { + format!("received single test output for unknown suite `{suite_key}`") + })?; + self.grouped_lines[suite_idx].push(format_streamed_status_line( + true, + ctx.suite_label_width, + suite_key, + kind, + Some(elapsed), + name, + )); + append_streamed_multi_output_lines( + &mut self.grouped_lines[suite_idx], + ctx.suite_label_width, + suite_key, + output, + ); + } else { + print_streamed_status( + ctx.multi, + ctx.suite_label_width, + suite_key, + kind, + Some(elapsed), + name, + ); + print_streamed_output(ctx.multi, ctx.suite_label_width, suite_key, output); + } + Ok(()) + } + fn flush_grouped_suite_lines(&mut self, suite_idx: usize) -> Result<(), String> { let lines = self .grouped_lines @@ -1479,23 +1530,7 @@ fn emit_single_outcome( outcome_tx: &Sender, shared: &WorkerSharedConfig, ) { - let (output, single) = run_single_test_job(job, shared); - let _ = outcome_tx.send(JobOutcome::Status { - suite_key: single.suite_key.clone(), - kind: if single.result.passed { - StreamStatusKind::Pass - } else { - StreamStatusKind::Fail - }, - elapsed: Some(single.elapsed), - message: single.result.name.clone(), - }); - if !output.is_empty() { - let _ = outcome_tx.send(JobOutcome::Text { - suite_key: single.suite_key.clone(), - text: output, - }); - } + let single = run_single_test_job(job, shared); let _ = outcome_tx.send(JobOutcome::SingleFinished(Box::new(single))); } @@ -1563,7 +1598,7 @@ fn emit_grouped_suite_outcome( aggregate_suite_staging: prepared.aggregate_suite_staging, }; for single_job in prepared.single_jobs { - let (output, single) = run_single_test_job(single_job, cfg.shared.as_ref()); + let single = run_single_test_job(single_job, cfg.shared.as_ref()); let _ = outcome_tx.send(JobOutcome::Status { suite_key: single.suite_key.clone(), kind: if single.result.passed { @@ -1574,10 +1609,10 @@ fn emit_grouped_suite_outcome( elapsed: Some(single.elapsed), message: single.result.name.clone(), }); - if !output.is_empty() { + if !single.output.is_empty() { let _ = outcome_tx.send(JobOutcome::Text { suite_key: single.suite_key.clone(), - text: output, + text: single.output, }); } state.completed_singles += 1; @@ -1850,10 +1885,7 @@ fn prepare_suite_job( ) } -fn run_single_test_job( - job: SingleTestJob, - shared: &WorkerSharedConfig, -) -> (String, SingleRunResult) { +fn run_single_test_job(job: SingleTestJob, shared: &WorkerSharedConfig) -> SingleRunResult { let report_ctx = job.report_root.as_ref().map(|root| ReportContext { root_dir: root.clone(), }); @@ -1880,14 +1912,14 @@ fn run_single_test_job( &shared.backend, &shared.debug, ); - let result = SingleRunResult { + SingleRunResult { suite_key: job.suite_key, symbol_name: case.symbol_name, result: outcome.result, + output, measurement: Some(measurement), elapsed, - }; - (output, result) + } } fn finalize_pending_suite( diff --git a/crates/fe/tests/cli_output.rs b/crates/fe/tests/cli_output.rs index cf0496ae62..50c00b058b 100644 --- a/crates/fe/tests/cli_output.rs +++ b/crates/fe/tests/cli_output.rs @@ -1442,6 +1442,43 @@ fn test_fe_test_runner(fixture: Fixture<&str>) { snap_test!(output, fixture.path()); } +#[test] +fn test_fe_test_parallel_failure_details_follow_status_lines() { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/fe_test_runner/checked_arithmetic_reverts.fe"); + let fixture = fixture + .to_str() + .unwrap_or_else(|| panic!("fixture path is not utf-8: {}", fixture.display())); + let out = run_fe_main_impl(&["test", "--jobs", "8", fixture], None, &[]); + + assert_ne!( + out.exit_code, + 0, + "expected failing tests:\n{}", + out.combined() + ); + let lines = out.stdout.lines().collect::>(); + let fail_lines = lines + .iter() + .enumerate() + .filter(|(_, line)| line.starts_with("FAIL [")) + .collect::>(); + assert_eq!( + fail_lines.len(), + 8, + "expected all fixture tests to fail:\n{}", + out.stdout + ); + for (line_idx, line) in fail_lines { + let next = lines.get(line_idx + 1).copied().unwrap_or_default(); + assert!( + next.trim_start().starts_with("Test reverted:"), + "expected failure detail immediately after status `{line}`, got `{next}`:\n{}", + out.stdout + ); + } +} + #[dir_test( dir: "$CARGO_MANIFEST_DIR/tests/fixtures/cli_output/ingots/library", glob: "**/app/fe.toml", diff --git a/newsfragments/1420.bugfix.md b/newsfragments/1420.bugfix.md new file mode 100644 index 0000000000..76466887ba --- /dev/null +++ b/newsfragments/1420.bugfix.md @@ -0,0 +1 @@ +Fixed `fe test` output so failure details are printed immediately after the corresponding failed test status when tests run in parallel.