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 {
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
367 #[test]
368 fn test_heuristic_should_panic() {
369 let tests: &[(&str, &str)] = &[(
370 "running 1 test
371test test_failure_should_panic - should panic ... FAILED
372
373failures:
374
375---- test_failure_should_panic stdout ----
376note: test did not panic as expected
377
378failures:
379 test_failure_should_panic
380
381test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.00s",
382 "note: test did not panic as expected",
383 )];
384
385 for (input, output) in tests {
386 let extracted = heuristic_should_panic(input.as_bytes())
387 .expect("should-panic message should have been found");
388 assert_eq!(
389 DisplayWrapper(extracted.slice),
390 DisplayWrapper(output.as_bytes())
391 );
392 assert_eq!(
393 extracted.start,
394 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
395 );
396 }
397 }
398
399 #[test]
400 fn test_heuristic_panic_message() {
401 let tests: &[(&str, &str)] = &[
402 (
403 "thread 'main' panicked at 'foo', src/lib.rs:1\n",
404 "thread 'main' panicked at 'foo', src/lib.rs:1",
405 ),
406 (
407 "foobar\n\
408 thread 'main' panicked at 'foo', src/lib.rs:1\n\n",
409 "thread 'main' panicked at 'foo', src/lib.rs:1",
410 ),
411 (
412 r#"
413text: foo
414Error: Custom { kind: InvalidData, error: "this is an error" }
415thread 'test_result_failure' panicked at 'assertion failed: `(left == right)`
416 left: `1`,
417 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
418note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
419more text at the end, followed by some newlines
420
421
422 "#,
423 r#"Error: Custom { kind: InvalidData, error: "this is an error" }
424thread 'test_result_failure' panicked at 'assertion failed: `(left == right)`
425 left: `1`,
426 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
427note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
428more text at the end, followed by some newlines"#,
429 ),
430 (
432 r"
433thread 'main' panicked at src/lib.rs:1:
434foo
435thread 'main' panicked at src/lib.rs:2:
436bar
437",
438 r"thread 'main' panicked at src/lib.rs:2:
439bar",
440 ), (
442 r"
443some initial text
444line 2
445line 3
446thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
447test
448stack backtrace:
449 0: rust_begin_unwind
450 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/panicking.rs:652:5
451 1: core::panicking::panic_fmt
452 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/panicking.rs:72:14
453 2: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace
454 at ./src/reporter/helpers.rs:237:9
455 3: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace::{{closure}}
456 at ./src/reporter/helpers.rs:236:36
457 4: core::ops::function::FnOnce::call_once
458 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
459 5: core::ops::function::FnOnce::call_once
460 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
461note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
462more text at the end, followed by some newlines
463
464
465",
466 r"thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
467test
468stack backtrace:
469 0: rust_begin_unwind
470 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/panicking.rs:652:5
471 1: core::panicking::panic_fmt
472 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/panicking.rs:72:14
473 2: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace
474 at ./src/reporter/helpers.rs:237:9
475 3: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace::{{closure}}
476 at ./src/reporter/helpers.rs:236:36
477 4: core::ops::function::FnOnce::call_once
478 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
479 5: core::ops::function::FnOnce::call_once
480 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
481note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
482more text at the end, followed by some newlines",
483 ),
484 (
486 r"
487some initial text
488thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
489test
490stack backtrace:
491 0: 0x61e6da135fe5 - std::backtrace_rs::backtrace::libunwind::trace::h23054e327d0d4b55
492 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
493 1: 0x61e6da135fe5 - std::backtrace_rs::backtrace::trace_unsynchronized::h0cc587407d7f7f64
494 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
495 2: 0x61e6da135fe5 - std::sys_common::backtrace::_print_fmt::h4feeb59774730d6b
496 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:68:5
497 3: 0x61e6da135fe5 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hd736fd5964392270
498 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:44:22
499 4: 0x61e6da16433b - core::fmt::rt::Argument::fmt::h105051d8ea1ade1e
500 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/rt.rs:165:63
501 5: 0x61e6da16433b - core::fmt::write::hc6043626647b98ea
502 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/mod.rs:1168:21
503some more text at the end, followed by some newlines
504
505
506",
507 r"thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
508test
509stack backtrace:
510 0: 0x61e6da135fe5 - std::backtrace_rs::backtrace::libunwind::trace::h23054e327d0d4b55
511 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
512 1: 0x61e6da135fe5 - std::backtrace_rs::backtrace::trace_unsynchronized::h0cc587407d7f7f64
513 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
514 2: 0x61e6da135fe5 - std::sys_common::backtrace::_print_fmt::h4feeb59774730d6b
515 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:68:5
516 3: 0x61e6da135fe5 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hd736fd5964392270
517 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:44:22
518 4: 0x61e6da16433b - core::fmt::rt::Argument::fmt::h105051d8ea1ade1e
519 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/rt.rs:165:63
520 5: 0x61e6da16433b - core::fmt::write::hc6043626647b98ea
521 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/mod.rs:1168:21
522some more text at the end, followed by some newlines",
523 ),
524 ];
525
526 for (input, output) in tests {
527 let extracted = heuristic_panic_message(input.as_bytes())
528 .expect("stack trace should have been found");
529 assert_eq!(
530 DisplayWrapper(extracted.slice),
531 DisplayWrapper(output.as_bytes())
532 );
533 assert_eq!(
534 extracted.start,
535 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
536 );
537 }
538 }
539
540 #[test]
541 fn test_heuristic_error_str() {
542 let tests: &[(&str, &str)] = &[(
543 "foobar\nError: \"this is an error\"\n",
544 "Error: \"this is an error\"",
545 )];
546
547 for (input, output) in tests {
548 let extracted =
549 heuristic_error_str(input.as_bytes()).expect("error string should have been found");
550 assert_eq!(
551 DisplayWrapper(extracted.slice),
552 DisplayWrapper(output.as_bytes())
553 );
554 assert_eq!(
555 extracted.start,
556 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
557 );
558 }
559 }
560
561 #[derive(Eq, PartialEq)]
563 struct DisplayWrapper<'a>(&'a [u8]);
564
565 impl fmt::Debug for DisplayWrapper<'_> {
566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567 write!(f, "{}", String::from_utf8_lossy(self.0))
568 }
569 }
570}