nextest_runner/reporter/displayer/
status_level.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Status levels: filters for which test statuses are displayed.
5//!
6//! Status levels play a role that's similar to log levels in typical loggers.
7
8use super::TestOutputDisplay;
9use crate::reporter::events::{CancelReason, ExecutionResult};
10use serde::Deserialize;
11
12/// Status level to show in the reporter output.
13///
14/// Status levels are incremental: each level causes all the statuses listed above it to be output. For example,
15/// [`Slow`](Self::Slow) implies [`Retry`](Self::Retry) and [`Fail`](Self::Fail).
16#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
17#[cfg_attr(test, derive(test_strategy::Arbitrary))]
18#[serde(rename_all = "kebab-case")]
19#[non_exhaustive]
20pub enum StatusLevel {
21    /// No output.
22    None,
23
24    /// Only output test failures.
25    Fail,
26
27    /// Output retries and failures.
28    Retry,
29
30    /// Output information about slow tests, and all variants above.
31    Slow,
32
33    /// Output information about leaky tests, and all variants above.
34    Leak,
35
36    /// Output passing tests in addition to all variants above.
37    Pass,
38
39    /// Output skipped tests in addition to all variants above.
40    Skip,
41
42    /// Currently has the same meaning as [`Skip`](Self::Skip).
43    All,
44}
45
46/// Status level to show at the end of test runs in the reporter output.
47///
48/// Status levels are incremental.
49///
50/// This differs from [`StatusLevel`] in two ways:
51/// * It has a "flaky" test indicator that's different from "retry" (though "retry" works as an alias.)
52/// * It has a different ordering: skipped tests are prioritized over passing ones.
53#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
54#[cfg_attr(test, derive(test_strategy::Arbitrary))]
55#[serde(rename_all = "kebab-case")]
56#[non_exhaustive]
57pub enum FinalStatusLevel {
58    /// No output.
59    None,
60
61    /// Only output test failures.
62    Fail,
63
64    /// Output flaky tests.
65    #[serde(alias = "retry")]
66    Flaky,
67
68    /// Output information about slow tests, and all variants above.
69    Slow,
70
71    /// Output skipped tests in addition to all variants above.
72    Skip,
73
74    /// Output leaky tests in addition to all variants above.
75    Leak,
76
77    /// Output passing tests in addition to all variants above.
78    Pass,
79
80    /// Currently has the same meaning as [`Pass`](Self::Pass).
81    All,
82}
83
84pub(crate) struct StatusLevels {
85    pub(crate) status_level: StatusLevel,
86    pub(crate) final_status_level: FinalStatusLevel,
87}
88
89impl StatusLevels {
90    pub(super) fn compute_output_on_test_finished(
91        &self,
92        display: TestOutputDisplay,
93        cancel_status: Option<CancelReason>,
94        test_status_level: StatusLevel,
95        test_final_status_level: FinalStatusLevel,
96        execution_result: ExecutionResult,
97    ) -> OutputOnTestFinished {
98        let write_status_line = self.status_level >= test_status_level;
99
100        let is_immediate = display.is_immediate();
101        // We store entries in the final output map if either the final status level is high enough or
102        // if `display` says we show the output at the end.
103        let is_final = display.is_final() || self.final_status_level >= test_final_status_level;
104
105        // Check if this test was terminated by nextest during immediate termination mode.
106        // This is a heuristic: we check if the test failed with SIGTERM (Unix) or JobObject (Windows)
107        // during TestFailureImmediate cancellation. This suppresses output spam from tests we killed.
108        let terminated_by_nextest = cancel_status == Some(CancelReason::TestFailureImmediate)
109            && execution_result.is_termination_failure();
110
111        // This table is tested below. The basic invariant is that we generally follow what
112        // is_immediate and is_final suggests, except:
113        //
114        // - if the run is cancelled due to a non-interrupt signal, we display test output at most
115        //   once.
116        // - if the run is cancelled due to an interrupt, we hide the output because dumping a bunch
117        //   of output at the end is likely to not be helpful (though in the future we may want to
118        //   at least dump outputs into files and write their names out, or whenever nextest gains
119        //   the ability to replay test runs to be able to display it then.)
120        // - if the run is cancelled due to immediate test failure termination, we hide output for
121        //   tests that were terminated by nextest (via SIGTERM/job object), but still show output
122        //   for tests that failed naturally (e.g. due to assertion failures or other exit codes).
123        //
124        // is_immediate  is_final      cancel_status     terminated_by_nextest  |  show_immediate  store_final
125        //
126        //     false      false          <= Signal                *             |      false          false
127        //     false       true          <= Signal                *             |      false           true  [1]
128        //      true      false          <= Signal                *             |       true          false  [1]
129        //      true       true           < Signal                *             |       true           true
130        //      true       true             Signal                *             |       true          false  [2]
131        //       *          *            Interrupt                *             |      false          false  [3]
132        //       *          *       TestFailureImmediate         true           |      false          false  [4]
133        //       *          *       TestFailureImmediate        false           |  (use rules above)  [5]
134        //
135        // [1] In non-interrupt cases, we want to display output if specified once.
136        //
137        // [2] If there's a signal, we shouldn't display output twice at the end since it's
138        //     redundant -- instead, just show the output as part of the immediate display.
139        //
140        // [3] For interrupts, hide all output to avoid spam.
141        //
142        // [4] For tests terminated by nextest during immediate mode, hide output to avoid spam.
143        //
144        // [5] For tests that failed naturally during immediate mode (race condition), show output
145        //     normally since these are real failures.
146        let show_immediate =
147            is_immediate && cancel_status <= Some(CancelReason::Signal) && !terminated_by_nextest;
148
149        let store_final = if cancel_status == Some(CancelReason::Interrupt) || terminated_by_nextest
150        {
151            // Hide output completely for interrupt and nextest-initiated termination.
152            OutputStoreFinal::No
153        } else if is_final && cancel_status < Some(CancelReason::Signal)
154            || !is_immediate && is_final && cancel_status == Some(CancelReason::Signal)
155        {
156            OutputStoreFinal::Yes {
157                display_output: display.is_final(),
158            }
159        } else if is_immediate && is_final && cancel_status == Some(CancelReason::Signal) {
160            // In this special case, we already display the output once as the test is being
161            // cancelled, so don't display it again at the end since that's redundant.
162            OutputStoreFinal::Yes {
163                display_output: false,
164            }
165        } else {
166            OutputStoreFinal::No
167        };
168
169        OutputOnTestFinished {
170            write_status_line,
171            show_immediate,
172            store_final,
173        }
174    }
175}
176
177#[derive(Debug, PartialEq, Eq)]
178pub(super) struct OutputOnTestFinished {
179    pub(super) write_status_line: bool,
180    pub(super) show_immediate: bool,
181    pub(super) store_final: OutputStoreFinal,
182}
183
184#[derive(Debug, PartialEq, Eq)]
185pub(super) enum OutputStoreFinal {
186    /// Do not store the output.
187    No,
188
189    /// Store the output. display_output controls whether stdout and stderr should actually be
190    /// displayed at the end.
191    Yes { display_output: bool },
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use test_strategy::proptest;
198
199    // ---
200    // The proptests here are probabilistically exhaustive, and it's just easier to express them
201    // as property-based tests. We could also potentially use a model checker like Kani here.
202    // ---
203
204    #[proptest(cases = 64)]
205    fn on_test_finished_dont_write_status_line(
206        display: TestOutputDisplay,
207        cancel_status: Option<CancelReason>,
208        #[filter(StatusLevel::Pass < #test_status_level)] test_status_level: StatusLevel,
209        test_final_status_level: FinalStatusLevel,
210    ) {
211        let status_levels = StatusLevels {
212            status_level: StatusLevel::Pass,
213            final_status_level: FinalStatusLevel::Fail,
214        };
215
216        let execution_result = ExecutionResult::Pass;
217        let actual = status_levels.compute_output_on_test_finished(
218            display,
219            cancel_status,
220            test_status_level,
221            test_final_status_level,
222            execution_result,
223        );
224
225        assert!(!actual.write_status_line);
226    }
227
228    #[proptest(cases = 64)]
229    fn on_test_finished_write_status_line(
230        display: TestOutputDisplay,
231        cancel_status: Option<CancelReason>,
232        #[filter(StatusLevel::Pass >= #test_status_level)] test_status_level: StatusLevel,
233        test_final_status_level: FinalStatusLevel,
234    ) {
235        let status_levels = StatusLevels {
236            status_level: StatusLevel::Pass,
237            final_status_level: FinalStatusLevel::Fail,
238        };
239
240        let execution_result = ExecutionResult::Pass;
241        let actual = status_levels.compute_output_on_test_finished(
242            display,
243            cancel_status,
244            test_status_level,
245            test_final_status_level,
246            execution_result,
247        );
248        assert!(actual.write_status_line);
249    }
250
251    #[proptest(cases = 64)]
252    fn on_test_finished_with_interrupt(
253        // We always hide output on interrupt.
254        display: TestOutputDisplay,
255        // cancel_status is fixed to Interrupt.
256
257        // In this case, the status levels are not relevant for is_immediate and is_final.
258        test_status_level: StatusLevel,
259        test_final_status_level: FinalStatusLevel,
260    ) {
261        let status_levels = StatusLevels {
262            status_level: StatusLevel::Pass,
263            final_status_level: FinalStatusLevel::Fail,
264        };
265
266        let execution_result = ExecutionResult::Pass;
267        let actual = status_levels.compute_output_on_test_finished(
268            display,
269            Some(CancelReason::Interrupt),
270            test_status_level,
271            test_final_status_level,
272            execution_result,
273        );
274        assert!(!actual.show_immediate);
275        assert_eq!(actual.store_final, OutputStoreFinal::No);
276    }
277
278    #[proptest(cases = 64)]
279    fn on_test_finished_dont_show_immediate(
280        #[filter(!#display.is_immediate())] display: TestOutputDisplay,
281        cancel_status: Option<CancelReason>,
282        // The status levels are not relevant for show_immediate.
283        test_status_level: StatusLevel,
284        test_final_status_level: FinalStatusLevel,
285    ) {
286        let status_levels = StatusLevels {
287            status_level: StatusLevel::Pass,
288            final_status_level: FinalStatusLevel::Fail,
289        };
290
291        let execution_result = ExecutionResult::Pass;
292        let actual = status_levels.compute_output_on_test_finished(
293            display,
294            cancel_status,
295            test_status_level,
296            test_final_status_level,
297            execution_result,
298        );
299        assert!(!actual.show_immediate);
300    }
301
302    #[proptest(cases = 64)]
303    fn on_test_finished_show_immediate(
304        #[filter(#display.is_immediate())] display: TestOutputDisplay,
305        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
306        // The status levels are not relevant for show_immediate.
307        test_status_level: StatusLevel,
308        test_final_status_level: FinalStatusLevel,
309    ) {
310        let status_levels = StatusLevels {
311            status_level: StatusLevel::Pass,
312            final_status_level: FinalStatusLevel::Fail,
313        };
314
315        let execution_result = ExecutionResult::Pass;
316        let actual = status_levels.compute_output_on_test_finished(
317            display,
318            cancel_status,
319            test_status_level,
320            test_final_status_level,
321            execution_result,
322        );
323        assert!(actual.show_immediate);
324    }
325
326    // Where we don't store final output: if display.is_final() is false, and if the test final
327    // status level is too high.
328    #[proptest(cases = 64)]
329    fn on_test_finished_dont_store_final(
330        #[filter(!#display.is_final())] display: TestOutputDisplay,
331        cancel_status: Option<CancelReason>,
332        // The status level is not relevant for store_final.
333        test_status_level: StatusLevel,
334        // But the final status level is.
335        #[filter(FinalStatusLevel::Fail < #test_final_status_level)]
336        test_final_status_level: FinalStatusLevel,
337    ) {
338        let status_levels = StatusLevels {
339            status_level: StatusLevel::Pass,
340            final_status_level: FinalStatusLevel::Fail,
341        };
342
343        let execution_result = ExecutionResult::Pass;
344        let actual = status_levels.compute_output_on_test_finished(
345            display,
346            cancel_status,
347            test_status_level,
348            test_final_status_level,
349            execution_result,
350        );
351        assert_eq!(actual.store_final, OutputStoreFinal::No);
352    }
353
354    // Case 1 where we store final output: if display is exactly TestOutputDisplay::Final, and if
355    // the cancel status is not Interrupt.
356    #[proptest(cases = 64)]
357    fn on_test_finished_store_final_1(
358        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
359        // In this case, it isn't relevant what test_status_level and test_final_status_level are.
360        test_status_level: StatusLevel,
361        test_final_status_level: FinalStatusLevel,
362    ) {
363        let status_levels = StatusLevels {
364            status_level: StatusLevel::Pass,
365            final_status_level: FinalStatusLevel::Fail,
366        };
367
368        let execution_result = ExecutionResult::Pass;
369        let actual = status_levels.compute_output_on_test_finished(
370            TestOutputDisplay::Final,
371            cancel_status,
372            test_status_level,
373            test_final_status_level,
374            execution_result,
375        );
376        assert_eq!(
377            actual.store_final,
378            OutputStoreFinal::Yes {
379                display_output: true
380            }
381        );
382    }
383
384    // Case 2 where we store final output: if display is TestOutputDisplay::ImmediateFinal and the
385    // cancel status is not Signal or Interrupt
386    #[proptest(cases = 64)]
387    fn on_test_finished_store_final_2(
388        #[filter(#cancel_status < Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
389        test_status_level: StatusLevel,
390        test_final_status_level: FinalStatusLevel,
391    ) {
392        let status_levels = StatusLevels {
393            status_level: StatusLevel::Pass,
394            final_status_level: FinalStatusLevel::Fail,
395        };
396
397        let execution_result = ExecutionResult::Pass;
398        let actual = status_levels.compute_output_on_test_finished(
399            TestOutputDisplay::ImmediateFinal,
400            cancel_status,
401            test_status_level,
402            test_final_status_level,
403            execution_result,
404        );
405        assert_eq!(
406            actual.store_final,
407            OutputStoreFinal::Yes {
408                display_output: true
409            }
410        );
411    }
412
413    // Case 3 where we store final output: if display is TestOutputDisplay::ImmediateFinal and the
414    // cancel status is exactly Signal. In this special case, we don't display the output.
415    #[proptest(cases = 64)]
416    fn on_test_finished_store_final_3(
417        test_status_level: StatusLevel,
418        test_final_status_level: FinalStatusLevel,
419    ) {
420        let status_levels = StatusLevels {
421            status_level: StatusLevel::Pass,
422            final_status_level: FinalStatusLevel::Fail,
423        };
424
425        let execution_result = ExecutionResult::Pass;
426        let actual = status_levels.compute_output_on_test_finished(
427            TestOutputDisplay::ImmediateFinal,
428            Some(CancelReason::Signal),
429            test_status_level,
430            test_final_status_level,
431            execution_result,
432        );
433        assert_eq!(
434            actual.store_final,
435            OutputStoreFinal::Yes {
436                display_output: false,
437            }
438        );
439    }
440
441    // Case 4: if display.is_final() is *false* but the test_final_status_level is low enough.
442    #[proptest(cases = 64)]
443    fn on_test_finished_store_final_4(
444        #[filter(!#display.is_final())] display: TestOutputDisplay,
445        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
446        // The status level is not relevant for store_final.
447        test_status_level: StatusLevel,
448        // But the final status level is.
449        #[filter(FinalStatusLevel::Fail >= #test_final_status_level)]
450        test_final_status_level: FinalStatusLevel,
451    ) {
452        let status_levels = StatusLevels {
453            status_level: StatusLevel::Pass,
454            final_status_level: FinalStatusLevel::Fail,
455        };
456
457        let execution_result = ExecutionResult::Pass;
458        let actual = status_levels.compute_output_on_test_finished(
459            display,
460            cancel_status,
461            test_status_level,
462            test_final_status_level,
463            execution_result,
464        );
465        assert_eq!(
466            actual.store_final,
467            OutputStoreFinal::Yes {
468                display_output: false,
469            }
470        );
471    }
472
473    #[test]
474    fn on_test_finished_terminated_by_nextest() {
475        use crate::reporter::events::{AbortStatus, FailureStatus};
476
477        let status_levels = StatusLevels {
478            status_level: StatusLevel::Pass,
479            final_status_level: FinalStatusLevel::Fail,
480        };
481
482        // Test 1: Terminated by nextest (SIGTERM) during TestFailureImmediate - should hide
483        #[cfg(unix)]
484        {
485            let execution_result = ExecutionResult::Fail {
486                failure_status: FailureStatus::Abort(AbortStatus::UnixSignal(libc::SIGTERM)),
487                leaked: false,
488            };
489
490            let actual = status_levels.compute_output_on_test_finished(
491                TestOutputDisplay::ImmediateFinal,
492                Some(CancelReason::TestFailureImmediate),
493                StatusLevel::Fail,
494                FinalStatusLevel::Fail,
495                execution_result,
496            );
497
498            assert!(
499                !actual.show_immediate,
500                "should not show immediate for SIGTERM during TestFailureImmediate"
501            );
502            assert_eq!(
503                actual.store_final,
504                OutputStoreFinal::No,
505                "should not store final for SIGTERM during TestFailureImmediate"
506            );
507        }
508
509        // Test 2: Terminated by nextest (JobObject) during TestFailureImmediate - should hide
510        #[cfg(windows)]
511        {
512            let execution_result = ExecutionResult::Fail {
513                failure_status: FailureStatus::Abort(AbortStatus::JobObject),
514                leaked: false,
515            };
516
517            let actual = status_levels.compute_output_on_test_finished(
518                TestOutputDisplay::ImmediateFinal,
519                Some(CancelReason::TestFailureImmediate),
520                StatusLevel::Fail,
521                FinalStatusLevel::Fail,
522                execution_result,
523            );
524
525            assert!(
526                !actual.show_immediate,
527                "should not show immediate for JobObject during TestFailureImmediate"
528            );
529            assert_eq!(
530                actual.store_final,
531                OutputStoreFinal::No,
532                "should not store final for JobObject during TestFailureImmediate"
533            );
534        }
535
536        // Test 3: Natural failure (exit code) during TestFailureImmediate - should show
537        let execution_result = ExecutionResult::Fail {
538            failure_status: FailureStatus::ExitCode(1),
539            leaked: false,
540        };
541
542        let actual = status_levels.compute_output_on_test_finished(
543            TestOutputDisplay::ImmediateFinal,
544            Some(CancelReason::TestFailureImmediate),
545            StatusLevel::Fail,
546            FinalStatusLevel::Fail,
547            execution_result,
548        );
549
550        assert!(
551            actual.show_immediate,
552            "should show immediate for natural failure during TestFailureImmediate"
553        );
554        assert_eq!(
555            actual.store_final,
556            OutputStoreFinal::Yes {
557                display_output: true
558            },
559            "should store final for natural failure"
560        );
561
562        // Test 4: SIGTERM but not during TestFailureImmediate (user sent signal) - should show
563        #[cfg(unix)]
564        {
565            let execution_result = ExecutionResult::Fail {
566                failure_status: FailureStatus::Abort(AbortStatus::UnixSignal(libc::SIGTERM)),
567                leaked: false,
568            };
569
570            let actual = status_levels.compute_output_on_test_finished(
571                TestOutputDisplay::ImmediateFinal,
572                Some(CancelReason::Signal), // Regular signal, not TestFailureImmediate
573                StatusLevel::Fail,
574                FinalStatusLevel::Fail,
575                execution_result,
576            );
577
578            assert!(
579                actual.show_immediate,
580                "should show immediate for user-initiated SIGTERM"
581            );
582            assert_eq!(
583                actual.store_final,
584                OutputStoreFinal::Yes {
585                    display_output: false
586                },
587                "should store but not display final"
588            );
589        }
590    }
591}