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;
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    ) -> OutputOnTestFinished {
97        let write_status_line = self.status_level >= test_status_level;
98
99        let is_immediate = display.is_immediate();
100        // We store entries in the final output map if either the final status level is high enough or
101        // if `display` says we show the output at the end.
102        let is_final = display.is_final() || self.final_status_level >= test_final_status_level;
103
104        // This table is tested below. The basic invariant is that we generally follow what
105        // is_immediate and is_final suggests, except:
106        //
107        // - if the run is cancelled due to a non-interrupt signal, we display test output at most
108        //   once.
109        // - if the run is cancelled due to an interrupt, we hide the output because dumping a bunch
110        //   of output at the end is likely to not be helpful (though in the future we may want to
111        //   at least dump outputs into files and write their names out, or whenever nextest gains
112        //   the ability to replay test runs to be able to display it then.)
113        //
114        // is_immediate  is_final  cancel_status  |  show_immediate  store_final
115        //
116        //     false      false      <= Signal    |     false          false
117        //     false       true      <= Signal    |     false           true  [1]
118        //      true      false      <= Signal    |      true          false  [1]
119        //      true       true       < Signal    |      true           true
120        //      true       true         Signal    |      true          false  [2]
121        //       *           *       Interrupt    |     false          false
122        //
123        // [1] In non-interrupt cases, we want to display output if specified once.
124        //
125        // [2] If there's a signal, we shouldn't display output twice at the end since it's
126        // redundant -- instead, just show the output as part of the immediate display.
127        let show_immediate = is_immediate && cancel_status <= Some(CancelReason::Signal);
128
129        let store_final = if is_final && cancel_status < Some(CancelReason::Signal)
130            || !is_immediate && is_final && cancel_status == Some(CancelReason::Signal)
131        {
132            OutputStoreFinal::Yes {
133                display_output: display.is_final(),
134            }
135        } else if is_immediate && is_final && cancel_status == Some(CancelReason::Signal) {
136            // In this special case, we already display the output once as the test is being
137            // cancelled, so don't display it again at the end since that's redundant.
138            OutputStoreFinal::Yes {
139                display_output: false,
140            }
141        } else {
142            OutputStoreFinal::No
143        };
144
145        OutputOnTestFinished {
146            write_status_line,
147            show_immediate,
148            store_final,
149        }
150    }
151}
152
153#[derive(Debug, PartialEq, Eq)]
154pub(super) struct OutputOnTestFinished {
155    pub(super) write_status_line: bool,
156    pub(super) show_immediate: bool,
157    pub(super) store_final: OutputStoreFinal,
158}
159
160#[derive(Debug, PartialEq, Eq)]
161pub(super) enum OutputStoreFinal {
162    /// Do not store the output.
163    No,
164
165    /// Store the output. display_output controls whether stdout and stderr should actually be
166    /// displayed at the end.
167    Yes { display_output: bool },
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use test_strategy::proptest;
174
175    // ---
176    // The proptests here are probabilistically exhaustive, and it's just easier to express them
177    // as property-based tests. We could also potentially use a model checker like Kani here.
178    // ---
179
180    #[proptest(cases = 64)]
181    fn on_test_finished_dont_write_status_line(
182        display: TestOutputDisplay,
183        cancel_status: Option<CancelReason>,
184        #[filter(StatusLevel::Pass < #test_status_level)] test_status_level: StatusLevel,
185        test_final_status_level: FinalStatusLevel,
186    ) {
187        let status_levels = StatusLevels {
188            status_level: StatusLevel::Pass,
189            final_status_level: FinalStatusLevel::Fail,
190        };
191
192        let actual = status_levels.compute_output_on_test_finished(
193            display,
194            cancel_status,
195            test_status_level,
196            test_final_status_level,
197        );
198
199        assert!(!actual.write_status_line);
200    }
201
202    #[proptest(cases = 64)]
203    fn on_test_finished_write_status_line(
204        display: TestOutputDisplay,
205        cancel_status: Option<CancelReason>,
206        #[filter(StatusLevel::Pass >= #test_status_level)] test_status_level: StatusLevel,
207        test_final_status_level: FinalStatusLevel,
208    ) {
209        let status_levels = StatusLevels {
210            status_level: StatusLevel::Pass,
211            final_status_level: FinalStatusLevel::Fail,
212        };
213
214        let actual = status_levels.compute_output_on_test_finished(
215            display,
216            cancel_status,
217            test_status_level,
218            test_final_status_level,
219        );
220        assert!(actual.write_status_line);
221    }
222
223    #[proptest(cases = 64)]
224    fn on_test_finished_with_interrupt(
225        // We always hide output on interrupt.
226        display: TestOutputDisplay,
227        // cancel_status is fixed to Interrupt.
228
229        // In this case, the status levels are not relevant for is_immediate and is_final.
230        test_status_level: StatusLevel,
231        test_final_status_level: FinalStatusLevel,
232    ) {
233        let status_levels = StatusLevels {
234            status_level: StatusLevel::Pass,
235            final_status_level: FinalStatusLevel::Fail,
236        };
237
238        let actual = status_levels.compute_output_on_test_finished(
239            display,
240            Some(CancelReason::Interrupt),
241            test_status_level,
242            test_final_status_level,
243        );
244        assert!(!actual.show_immediate);
245        assert_eq!(actual.store_final, OutputStoreFinal::No);
246    }
247
248    #[proptest(cases = 64)]
249    fn on_test_finished_dont_show_immediate(
250        #[filter(!#display.is_immediate())] display: TestOutputDisplay,
251        cancel_status: Option<CancelReason>,
252        // The status levels are not relevant for show_immediate.
253        test_status_level: StatusLevel,
254        test_final_status_level: FinalStatusLevel,
255    ) {
256        let status_levels = StatusLevels {
257            status_level: StatusLevel::Pass,
258            final_status_level: FinalStatusLevel::Fail,
259        };
260
261        let actual = status_levels.compute_output_on_test_finished(
262            display,
263            cancel_status,
264            test_status_level,
265            test_final_status_level,
266        );
267        assert!(!actual.show_immediate);
268    }
269
270    #[proptest(cases = 64)]
271    fn on_test_finished_show_immediate(
272        #[filter(#display.is_immediate())] display: TestOutputDisplay,
273        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
274        // The status levels are not relevant for show_immediate.
275        test_status_level: StatusLevel,
276        test_final_status_level: FinalStatusLevel,
277    ) {
278        let status_levels = StatusLevels {
279            status_level: StatusLevel::Pass,
280            final_status_level: FinalStatusLevel::Fail,
281        };
282
283        let actual = status_levels.compute_output_on_test_finished(
284            display,
285            cancel_status,
286            test_status_level,
287            test_final_status_level,
288        );
289        assert!(actual.show_immediate);
290    }
291
292    // Where we don't store final output: if display.is_final() is false, and if the test final
293    // status level is too high.
294    #[proptest(cases = 64)]
295    fn on_test_finished_dont_store_final(
296        #[filter(!#display.is_final())] display: TestOutputDisplay,
297        cancel_status: Option<CancelReason>,
298        // The status level is not relevant for store_final.
299        test_status_level: StatusLevel,
300        // But the final status level is.
301        #[filter(FinalStatusLevel::Fail < #test_final_status_level)]
302        test_final_status_level: FinalStatusLevel,
303    ) {
304        let status_levels = StatusLevels {
305            status_level: StatusLevel::Pass,
306            final_status_level: FinalStatusLevel::Fail,
307        };
308
309        let actual = status_levels.compute_output_on_test_finished(
310            display,
311            cancel_status,
312            test_status_level,
313            test_final_status_level,
314        );
315        assert_eq!(actual.store_final, OutputStoreFinal::No);
316    }
317
318    // Case 1 where we store final output: if display is exactly TestOutputDisplay::Final, and if
319    // the cancel status is not Interrupt.
320    #[proptest(cases = 64)]
321    fn on_test_finished_store_final_1(
322        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
323        // In this case, it isn't relevant what test_status_level and test_final_status_level are.
324        test_status_level: StatusLevel,
325        test_final_status_level: FinalStatusLevel,
326    ) {
327        let status_levels = StatusLevels {
328            status_level: StatusLevel::Pass,
329            final_status_level: FinalStatusLevel::Fail,
330        };
331
332        let actual = status_levels.compute_output_on_test_finished(
333            TestOutputDisplay::Final,
334            cancel_status,
335            test_status_level,
336            test_final_status_level,
337        );
338        assert_eq!(
339            actual.store_final,
340            OutputStoreFinal::Yes {
341                display_output: true
342            }
343        );
344    }
345
346    // Case 2 where we store final output: if display is TestOutputDisplay::ImmediateFinal and the
347    // cancel status is not Signal or Interrupt
348    #[proptest(cases = 64)]
349    fn on_test_finished_store_final_2(
350        #[filter(#cancel_status < Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
351        test_status_level: StatusLevel,
352        test_final_status_level: FinalStatusLevel,
353    ) {
354        let status_levels = StatusLevels {
355            status_level: StatusLevel::Pass,
356            final_status_level: FinalStatusLevel::Fail,
357        };
358
359        let actual = status_levels.compute_output_on_test_finished(
360            TestOutputDisplay::ImmediateFinal,
361            cancel_status,
362            test_status_level,
363            test_final_status_level,
364        );
365        assert_eq!(
366            actual.store_final,
367            OutputStoreFinal::Yes {
368                display_output: true
369            }
370        );
371    }
372
373    // Case 3 where we store final output: if display is TestOutputDisplay::ImmediateFinal and the
374    // cancel status is exactly Signal. In this special case, we don't display the output.
375    #[proptest(cases = 64)]
376    fn on_test_finished_store_final_3(
377        test_status_level: StatusLevel,
378        test_final_status_level: FinalStatusLevel,
379    ) {
380        let status_levels = StatusLevels {
381            status_level: StatusLevel::Pass,
382            final_status_level: FinalStatusLevel::Fail,
383        };
384
385        let actual = status_levels.compute_output_on_test_finished(
386            TestOutputDisplay::ImmediateFinal,
387            Some(CancelReason::Signal),
388            test_status_level,
389            test_final_status_level,
390        );
391        assert_eq!(
392            actual.store_final,
393            OutputStoreFinal::Yes {
394                display_output: false,
395            }
396        );
397    }
398
399    // Case 4: if display.is_final() is *false* but the test_final_status_level is low enough.
400    #[proptest(cases = 64)]
401    fn on_test_finished_store_final_4(
402        #[filter(!#display.is_final())] display: TestOutputDisplay,
403        #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
404        // The status level is not relevant for store_final.
405        test_status_level: StatusLevel,
406        // But the final status level is.
407        #[filter(FinalStatusLevel::Fail >= #test_final_status_level)]
408        test_final_status_level: FinalStatusLevel,
409    ) {
410        let status_levels = StatusLevels {
411            status_level: StatusLevel::Pass,
412            final_status_level: FinalStatusLevel::Fail,
413        };
414
415        let actual = status_levels.compute_output_on_test_finished(
416            display,
417            cancel_status,
418            test_status_level,
419            test_final_status_level,
420        );
421        assert_eq!(
422            actual.store_final,
423            OutputStoreFinal::Yes {
424                display_output: false,
425            }
426        );
427    }
428}