1use super::events::{AbortStatus, ExecutionResult, FailureStatus, UnitKind};
5use crate::{
6 errors::{ChildError, ChildStartError, ErrorList},
7 helpers::display_abort_status,
8 test_output::{ChildExecutionOutput, ChildOutput},
9};
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 ChildStartError>,
21 output_errors: Option<&'a ErrorList<ChildError>>,
22 abort: Option<UnitAbortDescription>,
23 output_slice: Option<TestOutputErrorSlice<'a>>,
24}
25
26impl<'a> UnitErrorDescription<'a> {
27 pub fn new(kind: UnitKind, output: &'a ChildExecutionOutput) -> 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 ChildExecutionOutput::StartError(error) => {
36 start_error = Some(error);
37 }
38 ChildExecutionOutput::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 ChildOutput::Split(output) => {
50 output_slice = TestOutputErrorSlice::heuristic_extract(
51 output.stdout.as_ref().map(|x| x.buf.as_ref()),
52 output.stderr.as_ref().map(|x| x.buf.as_ref()),
53 );
54 }
55 ChildOutput::Combined { output } => {
56 output_slice = TestOutputErrorSlice::heuristic_extract(
57 Some(output.buf.as_ref()),
58 Some(output.buf.as_ref()),
59 );
60 }
61 }
62 }
63
64 if let ExecutionResult::Fail {
65 failure_status: FailureStatus::Abort(status),
66 leaked,
67 } = result
68 {
69 abort = Some(UnitAbortDescription {
70 status: *status,
71 leaked: *leaked,
72 });
73 }
74 }
75 }
76 }
77
78 Self {
79 kind,
80 start_error,
81 output_errors,
82 abort,
83 output_slice,
84 }
85 }
86
87 pub(crate) fn all_error_list(&self) -> Option<ErrorList<&dyn std::error::Error>> {
89 ErrorList::new(self.kind.executing_message(), self.all_errors().collect())
90 }
91
92 pub(crate) fn exec_fail_error_list(&self) -> Option<ErrorList<&dyn std::error::Error>> {
96 ErrorList::new(
97 self.kind.executing_message(),
98 self.exec_fail_errors().collect(),
99 )
100 }
101
102 pub fn child_process_error_list(&self) -> Option<ErrorList<&dyn std::error::Error>> {
106 ErrorList::new(
107 self.kind.executing_message(),
108 self.child_process_errors().collect(),
109 )
110 }
111
112 pub(crate) fn output_slice(&self) -> Option<TestOutputErrorSlice<'a>> {
113 self.output_slice
114 }
115
116 fn all_errors(&self) -> impl Iterator<Item = &dyn std::error::Error> {
118 self.exec_fail_errors().chain(self.child_process_errors())
119 }
120
121 fn exec_fail_errors(&self) -> impl Iterator<Item = &dyn std::error::Error> {
122 self.start_error
123 .as_ref()
124 .map(|error| error as &dyn std::error::Error)
125 .into_iter()
126 .chain(
127 self.output_errors
128 .as_ref()
129 .into_iter()
130 .flat_map(|errors| errors.iter().map(|error| error as &dyn std::error::Error)),
131 )
132 }
133
134 fn child_process_errors(&self) -> impl Iterator<Item = &dyn std::error::Error> {
135 self.abort
136 .as_ref()
137 .map(|abort| abort as &dyn std::error::Error)
138 .into_iter()
139 .chain(
140 self.output_slice
141 .as_ref()
142 .map(|slice| slice as &dyn std::error::Error),
143 )
144 }
145}
146
147#[derive(Clone, Copy, Debug, Error)]
148struct UnitAbortDescription {
149 status: AbortStatus,
150 leaked: bool,
151}
152
153impl fmt::Display for UnitAbortDescription {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "process {}", display_abort_status(self.status))?;
156 if self.leaked {
157 write!(f, ", and also leaked handles")?;
158 }
159 Ok(())
160 }
161}
162
163#[derive(Clone, Copy, Debug, Error)]
168pub enum TestOutputErrorSlice<'a> {
169 PanicMessage {
173 stderr_subslice: ByteSubslice<'a>,
175 },
176
177 ErrorStr {
181 stderr_subslice: ByteSubslice<'a>,
183 },
184
185 ShouldPanic {
189 stdout_subslice: ByteSubslice<'a>,
191 },
192}
193
194impl<'a> TestOutputErrorSlice<'a> {
195 pub fn heuristic_extract(stdout: Option<&'a [u8]>, stderr: Option<&'a [u8]>) -> Option<Self> {
201 if let Some(stderr) = stderr {
204 if let Some(stderr_subslice) = heuristic_panic_message(stderr) {
205 return Some(TestOutputErrorSlice::PanicMessage { stderr_subslice });
206 }
207 if let Some(stderr_subslice) = heuristic_error_str(stderr) {
208 return Some(TestOutputErrorSlice::ErrorStr { stderr_subslice });
209 }
210 }
211
212 if let Some(stdout) = stdout {
213 if let Some(stdout_subslice) = heuristic_should_panic(stdout) {
214 return Some(TestOutputErrorSlice::ShouldPanic { stdout_subslice });
215 }
216 }
217
218 None
219 }
220
221 pub fn stderr_subslice(&self) -> Option<ByteSubslice<'a>> {
223 match self {
224 Self::PanicMessage { stderr_subslice }
225 | Self::ErrorStr {
226 stderr_subslice, ..
227 } => Some(*stderr_subslice),
228 Self::ShouldPanic { .. } => None,
229 }
230 }
231
232 pub fn stdout_subslice(&self) -> Option<ByteSubslice<'a>> {
234 match self {
235 Self::PanicMessage { .. } => None,
236 Self::ErrorStr { .. } => None,
237 Self::ShouldPanic {
238 stdout_subslice, ..
239 } => Some(*stdout_subslice),
240 }
241 }
242
243 pub fn combined_subslice(&self) -> Option<ByteSubslice<'a>> {
245 match self {
246 Self::PanicMessage { stderr_subslice }
247 | Self::ErrorStr {
248 stderr_subslice, ..
249 } => Some(*stderr_subslice),
250 Self::ShouldPanic {
251 stdout_subslice, ..
252 } => Some(*stdout_subslice),
253 }
254 }
255}
256
257impl fmt::Display for TestOutputErrorSlice<'_> {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 match self {
260 Self::PanicMessage { stderr_subslice } => {
261 write!(f, "{}", String::from_utf8_lossy(stderr_subslice.slice))
262 }
263 Self::ErrorStr { stderr_subslice } => {
264 write!(f, "{}", String::from_utf8_lossy(stderr_subslice.slice))
265 }
266 Self::ShouldPanic { stdout_subslice } => {
267 write!(f, "{}", String::from_utf8_lossy(stdout_subslice.slice))
268 }
269 }
270 }
271}
272
273#[derive(Clone, Copy, Debug)]
277pub struct ByteSubslice<'a> {
278 pub slice: &'a [u8],
280
281 pub start: usize,
283}
284
285fn heuristic_should_panic(stdout: &[u8]) -> Option<ByteSubslice<'_>> {
286 let line = stdout
287 .lines()
288 .find(|line| line.contains_str("note: test did not panic as expected"))?;
289
290 let start = unsafe { line.as_ptr().offset_from(stdout.as_ptr()) };
292
293 let start = usize::try_from(start).unwrap_or_else(|error| {
294 panic!(
295 "negative offset from stdout.as_ptr() ({:x}) to line.as_ptr() ({:x}): {}",
296 stdout.as_ptr() as usize,
297 line.as_ptr() as usize,
298 error
299 )
300 });
301 Some(ByteSubslice { slice: line, start })
302}
303
304fn heuristic_panic_message(stderr: &[u8]) -> Option<ByteSubslice<'_>> {
305 let panicked_at_match = PANICKED_AT_REGEX.find_iter(stderr).last()?;
308 let mut start = panicked_at_match.start();
311 let prefix = stderr[..start].trim_end_with(|c| c == '\n' || c == '\r');
312 if let Some(prev_line_start) = prefix.rfind("\n") {
313 if prefix[prev_line_start..].starts_with_str("\nError:") {
314 start = prev_line_start + 1;
315 }
316 }
317
318 Some(ByteSubslice {
322 slice: stderr[start..].trim_end_with(|c| c.is_whitespace()),
323 start,
324 })
325}
326
327fn heuristic_error_str(stderr: &[u8]) -> Option<ByteSubslice<'_>> {
328 let error_match = ERROR_REGEX.find(stderr)?;
330 let start = error_match.start();
331
332 Some(ByteSubslice {
336 slice: stderr[start..].trim_end_with(|c| c.is_whitespace()),
337 start,
338 })
339}
340
341static PANICKED_AT_REGEX_STR: &str = "^thread '([^']+)' panicked at ";
344static PANICKED_AT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
345 let mut builder = RegexBuilder::new(PANICKED_AT_REGEX_STR);
346 builder.multi_line(true);
347 builder.build().unwrap()
348});
349
350static ERROR_REGEX_STR: &str = "^Error: ";
351static ERROR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
352 let mut builder = RegexBuilder::new(ERROR_REGEX_STR);
353 builder.multi_line(true);
354 builder.build().unwrap()
355});
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_heuristic_should_panic() {
363 let tests: &[(&str, &str)] = &[(
364 "running 1 test
365test test_failure_should_panic - should panic ... FAILED
366
367failures:
368
369---- test_failure_should_panic stdout ----
370note: test did not panic as expected
371
372failures:
373 test_failure_should_panic
374
375test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.00s",
376 "note: test did not panic as expected",
377 )];
378
379 for (input, output) in tests {
380 let extracted = heuristic_should_panic(input.as_bytes())
381 .expect("should-panic message should have been found");
382 assert_eq!(
383 DisplayWrapper(extracted.slice),
384 DisplayWrapper(output.as_bytes())
385 );
386 assert_eq!(
387 extracted.start,
388 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
389 );
390 }
391 }
392
393 #[test]
394 fn test_heuristic_panic_message() {
395 let tests: &[(&str, &str)] = &[
396 (
397 "thread 'main' panicked at 'foo', src/lib.rs:1\n",
398 "thread 'main' panicked at 'foo', src/lib.rs:1",
399 ),
400 (
401 "foobar\n\
402 thread 'main' panicked at 'foo', src/lib.rs:1\n\n",
403 "thread 'main' panicked at 'foo', src/lib.rs:1",
404 ),
405 (
406 r#"
407text: foo
408Error: Custom { kind: InvalidData, error: "this is an error" }
409thread 'test_result_failure' panicked at 'assertion failed: `(left == right)`
410 left: `1`,
411 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
412note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
413more text at the end, followed by some newlines
414
415
416 "#,
417 r#"Error: Custom { kind: InvalidData, error: "this is an error" }
418thread 'test_result_failure' panicked at 'assertion failed: `(left == right)`
419 left: `1`,
420 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
421note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
422more text at the end, followed by some newlines"#,
423 ),
424 (
426 r"
427thread 'main' panicked at src/lib.rs:1:
428foo
429thread 'main' panicked at src/lib.rs:2:
430bar
431",
432 r"thread 'main' panicked at src/lib.rs:2:
433bar",
434 ), (
436 r"
437some initial text
438line 2
439line 3
440thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
441test
442stack backtrace:
443 0: rust_begin_unwind
444 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/panicking.rs:652:5
445 1: core::panicking::panic_fmt
446 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/panicking.rs:72:14
447 2: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace
448 at ./src/reporter/helpers.rs:237:9
449 3: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace::{{closure}}
450 at ./src/reporter/helpers.rs:236:36
451 4: core::ops::function::FnOnce::call_once
452 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
453 5: core::ops::function::FnOnce::call_once
454 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
455note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
456more text at the end, followed by some newlines
457
458
459",
460 r"thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
461test
462stack backtrace:
463 0: rust_begin_unwind
464 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/panicking.rs:652:5
465 1: core::panicking::panic_fmt
466 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/panicking.rs:72:14
467 2: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace
468 at ./src/reporter/helpers.rs:237:9
469 3: nextest_runner::reporter::helpers::tests::test_heuristic_stack_trace::{{closure}}
470 at ./src/reporter/helpers.rs:236:36
471 4: core::ops::function::FnOnce::call_once
472 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
473 5: core::ops::function::FnOnce::call_once
474 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/ops/function.rs:250:5
475note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
476more text at the end, followed by some newlines",
477 ),
478 (
480 r"
481some initial text
482thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
483test
484stack backtrace:
485 0: 0x61e6da135fe5 - std::backtrace_rs::backtrace::libunwind::trace::h23054e327d0d4b55
486 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
487 1: 0x61e6da135fe5 - std::backtrace_rs::backtrace::trace_unsynchronized::h0cc587407d7f7f64
488 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
489 2: 0x61e6da135fe5 - std::sys_common::backtrace::_print_fmt::h4feeb59774730d6b
490 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:68:5
491 3: 0x61e6da135fe5 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hd736fd5964392270
492 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:44:22
493 4: 0x61e6da16433b - core::fmt::rt::Argument::fmt::h105051d8ea1ade1e
494 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/rt.rs:165:63
495 5: 0x61e6da16433b - core::fmt::write::hc6043626647b98ea
496 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/mod.rs:1168:21
497some more text at the end, followed by some newlines
498
499
500",
501 r"thread 'reporter::helpers::tests::test_heuristic_stack_trace' panicked at nextest-runner/src/reporter/helpers.rs:237:9:
502test
503stack backtrace:
504 0: 0x61e6da135fe5 - std::backtrace_rs::backtrace::libunwind::trace::h23054e327d0d4b55
505 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
506 1: 0x61e6da135fe5 - std::backtrace_rs::backtrace::trace_unsynchronized::h0cc587407d7f7f64
507 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
508 2: 0x61e6da135fe5 - std::sys_common::backtrace::_print_fmt::h4feeb59774730d6b
509 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:68:5
510 3: 0x61e6da135fe5 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hd736fd5964392270
511 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/std/src/sys_common/backtrace.rs:44:22
512 4: 0x61e6da16433b - core::fmt::rt::Argument::fmt::h105051d8ea1ade1e
513 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/rt.rs:165:63
514 5: 0x61e6da16433b - core::fmt::write::hc6043626647b98ea
515 at /rustc/3f5fd8dd41153bc5fdca9427e9e05be2c767ba23/library/core/src/fmt/mod.rs:1168:21
516some more text at the end, followed by some newlines",
517 ),
518 ];
519
520 for (input, output) in tests {
521 let extracted = heuristic_panic_message(input.as_bytes())
522 .expect("stack trace should have been found");
523 assert_eq!(
524 DisplayWrapper(extracted.slice),
525 DisplayWrapper(output.as_bytes())
526 );
527 assert_eq!(
528 extracted.start,
529 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
530 );
531 }
532 }
533
534 #[test]
535 fn test_heuristic_error_str() {
536 let tests: &[(&str, &str)] = &[(
537 "foobar\nError: \"this is an error\"\n",
538 "Error: \"this is an error\"",
539 )];
540
541 for (input, output) in tests {
542 let extracted =
543 heuristic_error_str(input.as_bytes()).expect("error string should have been found");
544 assert_eq!(
545 DisplayWrapper(extracted.slice),
546 DisplayWrapper(output.as_bytes())
547 );
548 assert_eq!(
549 extracted.start,
550 extracted.slice.as_ptr() as usize - input.as_bytes().as_ptr() as usize
551 );
552 }
553 }
554
555 #[derive(Eq, PartialEq)]
557 struct DisplayWrapper<'a>(&'a [u8]);
558
559 impl fmt::Debug for DisplayWrapper<'_> {
560 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561 write!(f, "{}", String::from_utf8_lossy(self.0))
562 }
563 }
564}