1use super::events::{
5 AbortDescription, ChildErrorDescription, ChildExecutionOutputDescription,
6 ChildOutputDescription, ChildStartErrorDescription, ExecutionResultDescription,
7 FailureDescription, UnitKind,
8};
9use crate::{errors::ErrorList, output_spec::LiveSpec};
10use bstr::ByteSlice;
11use regex::bytes::{Regex, RegexBuilder};
12use std::{fmt, sync::LazyLock};
13use thiserror::Error;
14
15#[derive(Clone, Debug)]
18pub struct UnitErrorDescription<'a> {
19 kind: UnitKind,
20 start_error: Option<&'a ChildStartErrorDescription>,
21 output_errors: Option<&'a ErrorList<ChildErrorDescription>>,
22 abort: Option<UnitAbortDescription>,
23 output_slice: Option<TestOutputErrorSlice<'a>>,
24}
25
26impl<'a> UnitErrorDescription<'a> {
27 pub fn new(kind: UnitKind, output: &'a ChildExecutionOutputDescription<LiveSpec>) -> Self {
29 let mut start_error = None;
30 let mut output_errors = None;
31 let mut abort = None;
32 let mut output_slice = None;
33
34 match output {
35 ChildExecutionOutputDescription::StartError(error) => {
36 start_error = Some(error);
37 }
38 ChildExecutionOutputDescription::Output {
39 result,
40 output,
41 errors,
42 } => {
43 output_errors = errors.as_ref();
44 if let Some(result) = result {
45 if kind == UnitKind::Test && !result.is_success() {
46 match output {
47 ChildOutputDescription::Split { stdout, stderr } => {
50 output_slice = TestOutputErrorSlice::heuristic_extract(
51 stdout.as_ref().map(|x| x.buf().as_ref()),
52 stderr.as_ref().map(|x| x.buf().as_ref()),
53 );
54 }
55 ChildOutputDescription::Combined { output } => {
56 output_slice = TestOutputErrorSlice::heuristic_extract(
57 Some(output.buf().as_ref()),
58 Some(output.buf().as_ref()),
59 );
60 }
61 ChildOutputDescription::NotLoaded => {
62 unreachable!(
63 "attempted to extract errors from output that was not loaded \
64 (the OutputLoadDecider should have returned Load for this \
65 event)"
66 );
67 }
68 }
69 }
70
71 if let ExecutionResultDescription::Fail {
72 failure: FailureDescription::Abort { abort: status },
73 leaked,
74 ..
75 } = result
76 {
77 abort = Some(UnitAbortDescription {
78 description: status.clone(),
79 leaked: *leaked,
80 });
81 }
82 }
83 }
84 }
85
86 Self {
87 kind,
88 start_error,
89 output_errors,
90 abort,
91 output_slice,
92 }
93 }
94
95 pub(crate) fn all_error_list(&self) -> Option<ErrorList<&dyn std::error::Error>> {
97 ErrorList::new(self.kind.executing_message(), self.all_errors().collect())
98 }
99
100 pub(crate) fn exec_fail_error_list(&self) -> Option<ErrorList<&dyn std::error::Error>> {
104 ErrorList::new(
105 self.kind.executing_message(),
106 self.exec_fail_errors().collect(),
107 )
108 }
109
110 pub fn child_process_error_list(&self) -> Option<ErrorList<&dyn std::error::Error>> {
114 ErrorList::new(
115 self.kind.executing_message(),
116 self.child_process_errors().collect(),
117 )
118 }
119
120 pub(crate) fn output_slice(&self) -> Option<TestOutputErrorSlice<'a>> {
121 self.output_slice
122 }
123
124 fn all_errors(&self) -> impl Iterator<Item = &dyn std::error::Error> {
126 self.exec_fail_errors().chain(self.child_process_errors())
127 }
128
129 fn exec_fail_errors(&self) -> impl Iterator<Item = &dyn std::error::Error> {
130 self.start_error
131 .map(|error| error as &dyn std::error::Error)
132 .into_iter()
133 .chain(
134 self.output_errors
135 .into_iter()
136 .flat_map(|errors| errors.iter().map(|error| error as &dyn std::error::Error)),
137 )
138 }
139
140 fn child_process_errors(&self) -> impl Iterator<Item = &dyn std::error::Error> {
141 self.abort
142 .as_ref()
143 .map(|abort| abort as &dyn std::error::Error)
144 .into_iter()
145 .chain(
146 self.output_slice
147 .as_ref()
148 .map(|slice| slice as &dyn std::error::Error),
149 )
150 }
151}
152
153#[derive(Clone, Debug, Error)]
154struct UnitAbortDescription {
155 description: AbortDescription,
156 leaked: bool,
157}
158
159impl fmt::Display for UnitAbortDescription {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "process {}", self.description)?;
162 if self.leaked {
163 write!(f, ", and also leaked handles")?;
164 }
165 Ok(())
166 }
167}
168
169#[derive(Clone, Copy, Debug, Error)]
174pub enum TestOutputErrorSlice<'a> {
175 PanicMessage {
179 stderr_subslice: ByteSubslice<'a>,
181 },
182
183 ErrorStr {
187 stderr_subslice: ByteSubslice<'a>,
189 },
190
191 ShouldPanic {
195 stdout_subslice: ByteSubslice<'a>,
197 },
198}
199
200impl<'a> TestOutputErrorSlice<'a> {
201 pub fn heuristic_extract(stdout: Option<&'a [u8]>, stderr: Option<&'a [u8]>) -> Option<Self> {
207 if let Some(stderr) = stderr {
210 if let Some(stderr_subslice) = heuristic_panic_message(stderr) {
211 return Some(TestOutputErrorSlice::PanicMessage { stderr_subslice });
212 }
213 if let Some(stderr_subslice) = heuristic_error_str(stderr) {
214 return Some(TestOutputErrorSlice::ErrorStr { stderr_subslice });
215 }
216 }
217
218 if let Some(stdout) = stdout
219 && let Some(stdout_subslice) = heuristic_should_panic(stdout)
220 {
221 return Some(TestOutputErrorSlice::ShouldPanic { stdout_subslice });
222 }
223
224 None
225 }
226
227 pub fn stderr_subslice(&self) -> Option<ByteSubslice<'a>> {
229 match self {
230 Self::PanicMessage { stderr_subslice }
231 | Self::ErrorStr {
232 stderr_subslice, ..
233 } => Some(*stderr_subslice),
234 Self::ShouldPanic { .. } => None,
235 }
236 }
237
238 pub fn stdout_subslice(&self) -> Option<ByteSubslice<'a>> {
240 match self {
241 Self::PanicMessage { .. } => None,
242 Self::ErrorStr { .. } => None,
243 Self::ShouldPanic {
244 stdout_subslice, ..
245 } => Some(*stdout_subslice),
246 }
247 }
248
249 pub fn combined_subslice(&self) -> Option<ByteSubslice<'a>> {
251 match self {
252 Self::PanicMessage { stderr_subslice }
253 | Self::ErrorStr {
254 stderr_subslice, ..
255 } => Some(*stderr_subslice),
256 Self::ShouldPanic {
257 stdout_subslice, ..
258 } => Some(*stdout_subslice),
259 }
260 }
261}
262
263impl fmt::Display for TestOutputErrorSlice<'_> {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 match self {
266 Self::PanicMessage { stderr_subslice } => {
267 write!(f, "{}", String::from_utf8_lossy(stderr_subslice.slice))
268 }
269 Self::ErrorStr { stderr_subslice } => {
270 write!(f, "{}", String::from_utf8_lossy(stderr_subslice.slice))
271 }
272 Self::ShouldPanic { stdout_subslice } => {
273 write!(f, "{}", String::from_utf8_lossy(stdout_subslice.slice))
274 }
275 }
276 }
277}
278
279#[derive(Clone, Copy, Debug)]
283pub struct ByteSubslice<'a> {
284 pub slice: &'a [u8],
286
287 pub start: usize,
289}
290
291fn heuristic_should_panic(stdout: &[u8]) -> Option<ByteSubslice<'_>> {
292 let line = stdout
293 .lines()
294 .find(|line| line.contains_str("note: test did not panic as expected"))?;
295
296 let start = unsafe { line.as_ptr().offset_from(stdout.as_ptr()) };
298
299 let start = usize::try_from(start).unwrap_or_else(|error| {
300 panic!(
301 "negative offset from stdout.as_ptr() ({:x}) to line.as_ptr() ({:x}): {}",
302 stdout.as_ptr() as usize,
303 line.as_ptr() as usize,
304 error
305 )
306 });
307 Some(ByteSubslice { slice: line, start })
308}
309
310fn heuristic_panic_message(stderr: &[u8]) -> Option<ByteSubslice<'_>> {
311 let panicked_at_match = PANICKED_AT_REGEX.find_iter(stderr).last()?;
314 let mut start = panicked_at_match.start();
317 let prefix = stderr[..start].trim_end_with(|c| c == '\n' || c == '\r');
318 if let Some(prev_line_start) = prefix.rfind("\n")
319 && prefix[prev_line_start..].starts_with_str("\nError:")
320 {
321 start = prev_line_start + 1;
322 }
323
324 Some(ByteSubslice {
328 slice: stderr[start..].trim_end_with(|c| c.is_whitespace()),
329 start,
330 })
331}
332
333fn heuristic_error_str(stderr: &[u8]) -> Option<ByteSubslice<'_>> {
334 let error_match = ERROR_REGEX.find(stderr)?;
336 let start = error_match.start();
337
338 Some(ByteSubslice {
342 slice: stderr[start..].trim_end_with(|c| c.is_whitespace()),
343 start,
344 })
345}
346
347static PANICKED_AT_REGEX_STR: &str = "^thread '([^']+)' (\\(\\d+\\) )?panicked at ";
350static PANICKED_AT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
351 let mut builder = RegexBuilder::new(PANICKED_AT_REGEX_STR);
352 builder.multi_line(true);
353 builder.build().unwrap()
354});
355
356static ERROR_REGEX_STR: &str = "^Error: ";
357static ERROR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
358 let mut builder = RegexBuilder::new(ERROR_REGEX_STR);
359 builder.multi_line(true);
360 builder.build().unwrap()
361});
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::{
367 reporter::events::ExecutionResult,
368 test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
369 };
370 use bytes::Bytes;
371
372 #[test]
376 fn test_no_error_extraction_for_successful_tests() {
377 let error_str_output: ChildExecutionOutputDescription<LiveSpec> =
380 ChildExecutionOutput::Output {
381 result: Some(ExecutionResult::Pass),
382 output: ChildOutput::Split(ChildSplitOutput {
383 stdout: Some(Bytes::from("test output").into()),
384 stderr: Some(Bytes::from("Error: simulated error for testing\n").into()),
385 }),
386 errors: None,
387 }
388 .into();
389
390 let desc = UnitErrorDescription::new(UnitKind::Test, &error_str_output);
391 assert!(
392 desc.output_slice().is_none(),
393 "output_slice should be None for a passing test with 'Error:' in stderr"
394 );
395 assert!(
396 desc.all_error_list().is_none(),
397 "all_error_list should be None for a passing test with 'Error:' in stderr"
398 );
399
400 let panic_output: ChildExecutionOutputDescription<LiveSpec> =
402 ChildExecutionOutput::Output {
403 result: Some(ExecutionResult::Pass),
404 output: ChildOutput::Split(ChildSplitOutput {
405 stdout: None,
406 stderr: Some(
407 Bytes::from(
408 "thread 'other' panicked at src/lib.rs:10:\n\
409 expected panic for testing\n",
410 )
411 .into(),
412 ),
413 }),
414 errors: None,
415 }
416 .into();
417
418 let desc = UnitErrorDescription::new(UnitKind::Test, &panic_output);
419 assert!(
420 desc.output_slice().is_none(),
421 "output_slice should be None for a passing test with panic-like stderr"
422 );
423 assert!(
424 desc.all_error_list().is_none(),
425 "all_error_list should be None for a passing test with panic-like stderr"
426 );
427
428 let combined_output: ChildExecutionOutputDescription<LiveSpec> =
430 ChildExecutionOutput::Output {
431 result: Some(ExecutionResult::Pass),
432 output: ChildOutput::Combined {
433 output: Bytes::from("some output\nError: not a real error\n").into(),
434 },
435 errors: None,
436 }
437 .into();
438
439 let desc = UnitErrorDescription::new(UnitKind::Test, &combined_output);
440 assert!(
441 desc.output_slice().is_none(),
442 "output_slice should be None for a passing test with 'Error:' in combined output"
443 );
444 assert!(
445 desc.all_error_list().is_none(),
446 "all_error_list should be None for a passing test with 'Error:' in combined output"
447 );
448 }
449
450 #[test]
451 fn test_heuristic_should_panic() {
452 let tests: &[(&str, &str)] = &[(
453 "running 1 test
454test test_failure_should_panic - should panic ... FAILED
455
456failures:
457
458---- test_failure_should_panic stdout ----
459note: test did not panic as expected
460
461failures:
462 test_failure_should_panic
463
464test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.00s",
465 "note: test did not panic as expected",
466 )];
467
468 for (input, output) in tests {
469 let extracted = heuristic_should_panic(input.as_bytes())
470 .expect("should-panic message should have been found");
471 assert_eq!(
472 DisplayWrapper(extracted.slice),
473 DisplayWrapper(output.as_bytes())
474 );
475 assert_eq!(
476 extracted.start,
477 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
478 );
479 }
480 }
481
482 #[test]
483 fn test_heuristic_panic_message() {
484 let tests: &[(&str, &str)] = &[
485 (
486 "thread 'main' panicked at 'foo', src/lib.rs:1\n",
487 "thread 'main' panicked at 'foo', src/lib.rs:1",
488 ),
489 (
490 "foobar\n\
491 thread 'main' panicked at 'foo', src/lib.rs:1\n\n",
492 "thread 'main' panicked at 'foo', src/lib.rs:1",
493 ),
494 (
495 r#"
496text: foo
497Error: Custom { kind: InvalidData, error: "this is an error" }
498thread 'test_result_failure' panicked at 'assertion failed: `(left == right)`
499 left: `1`,
500 right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/test/src/lib.rs:186:5
501note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
502more text at the end, followed by some newlines
503
504
505 "#,
506 r#"Error: Custom { kind: InvalidData, error: "this is an error" }
507thread 'test_result_failure' panicked at 'assertion failed: `(left == right)`
508 left: `1`,
509 right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', /rustc/fe5b13d681f25ee6474be29d748c65adcd91f69e/library/test/src/lib.rs:186:5
510note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
511more text at the end, followed by some newlines"#,
512 ),
513 (
515 r"
516thread 'main' panicked at src/lib.rs:1:
517foo
518thread 'main' panicked at src/lib.rs:2:
519bar
520",
521 r"thread 'main' panicked at src/lib.rs:2:
522bar",
523 ), (
525 r"
526some initial text
527line 2
528line 3
529thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
530test
531stack backtrace:
532 0: rust_begin_unwind
533 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/panicking.rs:652:5
534 1: core::panicking::panic_fmt
535 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/panicking.rs:72:14
536 2: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace
537 at ./src/reporter/helpers.rs:237:9
538 3: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace::{{closure}}
539 at ./src/reporter/helpers.rs:236:36
540 4: core::ops::function::FnOnce::call_once
541 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
542 5: core::ops::function::FnOnce::call_once
543 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
544note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
545more text at the end, followed by some newlines
546
547
548",
549 r"thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
550test
551stack backtrace:
552 0: rust_begin_unwind
553 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/panicking.rs:652:5
554 1: core::panicking::panic_fmt
555 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/panicking.rs:72:14
556 2: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace
557 at ./src/reporter/helpers.rs:237:9
558 3: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace::{{closure}}
559 at ./src/reporter/helpers.rs:236:36
560 4: core::ops::function::FnOnce::call_once
561 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
562 5: core::ops::function::FnOnce::call_once
563 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
564note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
565more text at the end, followed by some newlines",
566 ),
567 (
569 r"
570some initial text
571thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
572test
573stack backtrace:
574 0: 0x61e6da135fe5 - std::backtrace_rs::backtrace::libunwind::trace::h23054e327d0d4b55
575 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
576 1: 0x61e6da135fe5 - std::backtrace_rs::backtrace::trace_unsynchronized::h0cc587407d7f7f64
577 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
578 2: 0x61e6da135fe5 - std::sys_common::backtrace::_print_fmt::h4feeb59774730d6b
579 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:68:5
580 3: 0x61e6da135fe5 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hd736fd5964392270
581 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:44:22
582 4: 0x61e6da16433b - core::fmt::rt::Argument::fmt::h105051d8ea1ade1e
583 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/rt.rs:165:63
584 5: 0x61e6da16433b - core::fmt::write::hc6043626647b98ea
585 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/mod.rs:1168:21
586some more text at the end, followed by some newlines
587
588
589",
590 r"thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
591test
592stack backtrace:
593 0: 0x61e6da135fe5 - std::backtrace_rs::backtrace::libunwind::trace::h23054e327d0d4b55
594 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
595 1: 0x61e6da135fe5 - std::backtrace_rs::backtrace::trace_unsynchronized::h0cc587407d7f7f64
596 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
597 2: 0x61e6da135fe5 - std::sys_common::backtrace::_print_fmt::h4feeb59774730d6b
598 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:68:5
599 3: 0x61e6da135fe5 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hd736fd5964392270
600 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:44:22
601 4: 0x61e6da16433b - core::fmt::rt::Argument::fmt::h105051d8ea1ade1e
602 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/rt.rs:165:63
603 5: 0x61e6da16433b - core::fmt::write::hc6043626647b98ea
604 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/mod.rs:1168:21
605some more text at the end, followed by some newlines",
606 ),
607 ];
608
609 for (input, output) in tests {
610 let extracted = heuristic_panic_message(input.as_bytes())
611 .expect("stack trace should have been found");
612 assert_eq!(
613 DisplayWrapper(extracted.slice),
614 DisplayWrapper(output.as_bytes())
615 );
616 assert_eq!(
617 extracted.start,
618 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
619 );
620 }
621 }
622
623 #[test]
624 fn test_heuristic_error_str() {
625 let tests: &[(&str, &str)] = &[(
626 "foobar\nError: \"this is an error\"\n",
627 "Error: \"this is an error\"",
628 )];
629
630 for (input, output) in tests {
631 let extracted =
632 heuristic_error_str(input.as_bytes()).expect("error string should have been found");
633 assert_eq!(
634 DisplayWrapper(extracted.slice),
635 DisplayWrapper(output.as_bytes())
636 );
637 assert_eq!(
638 extracted.start,
639 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
640 );
641 }
642 }
643
644 #[derive(Eq, PartialEq)]
646 struct DisplayWrapper<'a>(&'a [u8]);
647
648 impl fmt::Debug for DisplayWrapper<'_> {
649 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650 write!(f, "{}", String::from_utf8_lossy(self.0))
651 }
652 }
653}