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, ExecutionResultDescription};
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: &ExecutionResultDescription,
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 actual = status_levels.compute_output_on_test_finished(
217            display,
218            cancel_status,
219            test_status_level,
220            test_final_status_level,
221            &ExecutionResultDescription::Pass,
222        );
223
224        assert!(!actual.write_status_line);
225    }
226
227    #[proptest(cases = 64)]
228    fn on_test_finished_write_status_line(
229        display: TestOutputDisplay,
230        cancel_status: Option<CancelReason>,
231        #[filter(StatusLevel::Pass >= #test_status_level)] test_status_level: StatusLevel,
232        test_final_status_level: FinalStatusLevel,
233    ) {
234        let status_levels = StatusLevels {
235            status_level: StatusLevel::Pass,
236            final_status_level: FinalStatusLevel::Fail,
237        };
238
239        let actual = status_levels.compute_output_on_test_finished(
240            display,
241            cancel_status,
242            test_status_level,
243            test_final_status_level,
244            &ExecutionResultDescription::Pass,
245        );
246        assert!(actual.write_status_line);
247    }
248
249    #[proptest(cases = 64)]
250    fn on_test_finished_with_interrupt(
251        // We always hide output on interrupt.
252        display: TestOutputDisplay,
253        // cancel_status is fixed to Interrupt.
254
255        // In this case, the status levels are not relevant for is_immediate and is_final.
256        test_status_level: StatusLevel,
257        test_final_status_level: FinalStatusLevel,
258    ) {
259        let status_levels = StatusLevels {
260            status_level: StatusLevel::Pass,
261            final_status_level: FinalStatusLevel::Fail,
262        };
263
264        let actual = status_levels.compute_output_on_test_finished(
265            display,
266            Some(CancelReason::Interrupt),
267            test_status_level,
268            test_final_status_level,
269            &ExecutionResultDescription::Pass,
270        );
271        assert!(!actual.show_immediate);
272        assert_eq!(actual.store_final, OutputStoreFinal::No);
273    }
274
275    #[proptest(cases = 64)]
276    fn on_test_finished_dont_show_immediate(
277        #[filter(!#display.is_immediate())] display: TestOutputDisplay,
278        cancel_status: Option<CancelReason>,
279        // The status levels are not relevant for show_immediate.
280        test_status_level: StatusLevel,
281        test_final_status_level: FinalStatusLevel,
282    ) {
283        let status_levels = StatusLevels {
284            status_level: StatusLevel::Pass,
285            final_status_level: FinalStatusLevel::Fail,
286        };
287
288        let actual = status_levels.compute_output_on_test_finished(
289            display,
290            cancel_status,
291            test_status_level,
292            test_final_status_level,
293            &ExecutionResultDescription::Pass,
294        );
295        assert!(!actual.show_immediate);
296    }
297
298    #[proptest(cases = 64)]
299    fn on_test_finished_show_immediate(
300        #[filter(#display.is_immediate())] display: TestOutputDisplay,
301        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
302        // The status levels are not relevant for show_immediate.
303        test_status_level: StatusLevel,
304        test_final_status_level: FinalStatusLevel,
305    ) {
306        let status_levels = StatusLevels {
307            status_level: StatusLevel::Pass,
308            final_status_level: FinalStatusLevel::Fail,
309        };
310
311        let actual = status_levels.compute_output_on_test_finished(
312            display,
313            cancel_status,
314            test_status_level,
315            test_final_status_level,
316            &ExecutionResultDescription::Pass,
317        );
318        assert!(actual.show_immediate);
319    }
320
321    // Where we don't store final output: if display.is_final() is false, and if the test final
322    // status level is too high.
323    #[proptest(cases = 64)]
324    fn on_test_finished_dont_store_final(
325        #[filter(!#display.is_final())] display: TestOutputDisplay,
326        cancel_status: Option<CancelReason>,
327        // The status level is not relevant for store_final.
328        test_status_level: StatusLevel,
329        // But the final status level is.
330        #[filter(FinalStatusLevel::Fail < #test_final_status_level)]
331        test_final_status_level: FinalStatusLevel,
332    ) {
333        let status_levels = StatusLevels {
334            status_level: StatusLevel::Pass,
335            final_status_level: FinalStatusLevel::Fail,
336        };
337
338        let actual = status_levels.compute_output_on_test_finished(
339            display,
340            cancel_status,
341            test_status_level,
342            test_final_status_level,
343            &ExecutionResultDescription::Pass,
344        );
345        assert_eq!(actual.store_final, OutputStoreFinal::No);
346    }
347
348    // Case 1 where we store final output: if display is exactly TestOutputDisplay::Final, and if
349    // the cancel status is not Interrupt.
350    #[proptest(cases = 64)]
351    fn on_test_finished_store_final_1(
352        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
353        // In this case, it isn't relevant what test_status_level and test_final_status_level are.
354        test_status_level: StatusLevel,
355        test_final_status_level: FinalStatusLevel,
356    ) {
357        let status_levels = StatusLevels {
358            status_level: StatusLevel::Pass,
359            final_status_level: FinalStatusLevel::Fail,
360        };
361
362        let actual = status_levels.compute_output_on_test_finished(
363            TestOutputDisplay::Final,
364            cancel_status,
365            test_status_level,
366            test_final_status_level,
367            &ExecutionResultDescription::Pass,
368        );
369        assert_eq!(
370            actual.store_final,
371            OutputStoreFinal::Yes {
372                display_output: true
373            }
374        );
375    }
376
377    // Case 2 where we store final output: if display is TestOutputDisplay::ImmediateFinal and the
378    // cancel status is not Signal or Interrupt
379    #[proptest(cases = 64)]
380    fn on_test_finished_store_final_2(
381        #[filter(#cancel_status < Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
382        test_status_level: StatusLevel,
383        test_final_status_level: FinalStatusLevel,
384    ) {
385        let status_levels = StatusLevels {
386            status_level: StatusLevel::Pass,
387            final_status_level: FinalStatusLevel::Fail,
388        };
389
390        let actual = status_levels.compute_output_on_test_finished(
391            TestOutputDisplay::ImmediateFinal,
392            cancel_status,
393            test_status_level,
394            test_final_status_level,
395            &ExecutionResultDescription::Pass,
396        );
397        assert_eq!(
398            actual.store_final,
399            OutputStoreFinal::Yes {
400                display_output: true
401            }
402        );
403    }
404
405    // Case 3 where we store final output: if display is TestOutputDisplay::ImmediateFinal and the
406    // cancel status is exactly Signal. In this special case, we don't display the output.
407    #[proptest(cases = 64)]
408    fn on_test_finished_store_final_3(
409        test_status_level: StatusLevel,
410        test_final_status_level: FinalStatusLevel,
411    ) {
412        let status_levels = StatusLevels {
413            status_level: StatusLevel::Pass,
414            final_status_level: FinalStatusLevel::Fail,
415        };
416
417        let actual = status_levels.compute_output_on_test_finished(
418            TestOutputDisplay::ImmediateFinal,
419            Some(CancelReason::Signal),
420            test_status_level,
421            test_final_status_level,
422            &ExecutionResultDescription::Pass,
423        );
424        assert_eq!(
425            actual.store_final,
426            OutputStoreFinal::Yes {
427                display_output: false,
428            }
429        );
430    }
431
432    // Case 4: if display.is_final() is *false* but the test_final_status_level is low enough.
433    #[proptest(cases = 64)]
434    fn on_test_finished_store_final_4(
435        #[filter(!#display.is_final())] display: TestOutputDisplay,
436        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
437        // The status level is not relevant for store_final.
438        test_status_level: StatusLevel,
439        // But the final status level is.
440        #[filter(FinalStatusLevel::Fail >= #test_final_status_level)]
441        test_final_status_level: FinalStatusLevel,
442    ) {
443        let status_levels = StatusLevels {
444            status_level: StatusLevel::Pass,
445            final_status_level: FinalStatusLevel::Fail,
446        };
447
448        let actual = status_levels.compute_output_on_test_finished(
449            display,
450            cancel_status,
451            test_status_level,
452            test_final_status_level,
453            &ExecutionResultDescription::Pass,
454        );
455        assert_eq!(
456            actual.store_final,
457            OutputStoreFinal::Yes {
458                display_output: false,
459            }
460        );
461    }
462
463    #[test]
464    fn on_test_finished_terminated_by_nextest() {
465        use crate::reporter::events::{AbortDescription, FailureDescription, SIGTERM};
466
467        let status_levels = StatusLevels {
468            status_level: StatusLevel::Pass,
469            final_status_level: FinalStatusLevel::Fail,
470        };
471
472        // Test 1: Terminated by nextest (SIGTERM) during TestFailureImmediate - should hide
473        {
474            let execution_result = ExecutionResultDescription::Fail {
475                failure: FailureDescription::Abort {
476                    abort: AbortDescription::UnixSignal {
477                        signal: SIGTERM,
478                        name: Some("TERM".into()),
479                    },
480                },
481                leaked: false,
482            };
483
484            let actual = status_levels.compute_output_on_test_finished(
485                TestOutputDisplay::ImmediateFinal,
486                Some(CancelReason::TestFailureImmediate),
487                StatusLevel::Fail,
488                FinalStatusLevel::Fail,
489                &execution_result,
490            );
491
492            assert!(
493                !actual.show_immediate,
494                "should not show immediate for SIGTERM during TestFailureImmediate"
495            );
496            assert_eq!(
497                actual.store_final,
498                OutputStoreFinal::No,
499                "should not store final for SIGTERM during TestFailureImmediate"
500            );
501        }
502
503        // Test 2: Terminated by nextest (JobObject) during TestFailureImmediate - should hide
504        {
505            let execution_result = ExecutionResultDescription::Fail {
506                failure: FailureDescription::Abort {
507                    abort: AbortDescription::WindowsJobObject,
508                },
509                leaked: false,
510            };
511
512            let actual = status_levels.compute_output_on_test_finished(
513                TestOutputDisplay::ImmediateFinal,
514                Some(CancelReason::TestFailureImmediate),
515                StatusLevel::Fail,
516                FinalStatusLevel::Fail,
517                &execution_result,
518            );
519
520            assert!(
521                !actual.show_immediate,
522                "should not show immediate for JobObject during TestFailureImmediate"
523            );
524            assert_eq!(
525                actual.store_final,
526                OutputStoreFinal::No,
527                "should not store final for JobObject during TestFailureImmediate"
528            );
529        }
530
531        // Test 3: Natural failure (exit code) during TestFailureImmediate - should show
532        let execution_result = ExecutionResultDescription::Fail {
533            failure: FailureDescription::ExitCode { code: 1 },
534            leaked: false,
535        };
536
537        let actual = status_levels.compute_output_on_test_finished(
538            TestOutputDisplay::ImmediateFinal,
539            Some(CancelReason::TestFailureImmediate),
540            StatusLevel::Fail,
541            FinalStatusLevel::Fail,
542            &execution_result,
543        );
544
545        assert!(
546            actual.show_immediate,
547            "should show immediate for natural failure during TestFailureImmediate"
548        );
549        assert_eq!(
550            actual.store_final,
551            OutputStoreFinal::Yes {
552                display_output: true
553            },
554            "should store final for natural failure"
555        );
556
557        // Test 4: SIGTERM but not during TestFailureImmediate (user sent signal) - should show
558        {
559            let execution_result = ExecutionResultDescription::Fail {
560                failure: FailureDescription::Abort {
561                    abort: AbortDescription::UnixSignal {
562                        signal: SIGTERM,
563                        name: Some("TERM".into()),
564                    },
565                },
566                leaked: false,
567            };
568
569            let actual = status_levels.compute_output_on_test_finished(
570                TestOutputDisplay::ImmediateFinal,
571                Some(CancelReason::Signal), // Regular signal, not TestFailureImmediate
572                StatusLevel::Fail,
573                FinalStatusLevel::Fail,
574                &execution_result,
575            );
576
577            assert!(
578                actual.show_immediate,
579                "should show immediate for user-initiated SIGTERM"
580            );
581            assert_eq!(
582                actual.store_final,
583                OutputStoreFinal::Yes {
584                    display_output: false
585                },
586                "should store but not display final"
587            );
588        }
589    }
590}