nextest_runner/
test_output.rs

1//! Utilities for capture output from tests run in a child process
2
3use crate::{
4    errors::{ChildError, ChildStartError, ErrorList},
5    reporter::events::ExecutionResult,
6};
7use bstr::{ByteSlice, Lines};
8use bytes::Bytes;
9use std::{borrow::Cow, sync::OnceLock};
10
11/// The strategy used to capture test executable output
12#[derive(Copy, Clone, PartialEq, Default, Debug)]
13pub enum CaptureStrategy {
14    /// Captures `stdout` and `stderr` separately
15    ///
16    /// * pro: output from `stdout` and `stderr` can be identified and easily split
17    /// * con: ordering between the streams cannot be guaranteed
18    #[default]
19    Split,
20    /// Captures `stdout` and `stderr` in a single stream
21    ///
22    /// * pro: output is guaranteed to be ordered as it would in a terminal emulator
23    /// * con: distinction between `stdout` and `stderr` is lost, all output is attributed to `stdout`
24    Combined,
25    /// Output is not captured
26    ///
27    /// This mode is used when using --no-capture, causing nextest to execute
28    /// tests serially without capturing output
29    None,
30}
31
32/// A single output for a test or setup script: standard output, standard error, or a combined
33/// buffer.
34///
35/// This is a wrapper around a [`Bytes`] that provides some convenience methods.
36#[derive(Clone, Debug)]
37pub struct ChildSingleOutput {
38    /// The raw output buffer
39    pub buf: Bytes,
40
41    /// A string representation of the output, computed on first access.
42    ///
43    /// `None` means the output is valid UTF-8.
44    as_str: OnceLock<Option<Box<str>>>,
45}
46
47impl From<Bytes> for ChildSingleOutput {
48    #[inline]
49    fn from(buf: Bytes) -> Self {
50        Self {
51            buf,
52            as_str: OnceLock::new(),
53        }
54    }
55}
56
57impl ChildSingleOutput {
58    /// Gets this output as a lossy UTF-8 string.
59    #[inline]
60    pub fn as_str_lossy(&self) -> &str {
61        let s = self
62            .as_str
63            .get_or_init(|| match String::from_utf8_lossy(&self.buf) {
64                // A borrowed string from `from_utf8_lossy` is always valid UTF-8. We can't store
65                // the `Cow` directly because that would be a self-referential struct. (Well, we
66                // could via a library like ouroboros, but that's really unnecessary.)
67                Cow::Borrowed(_) => None,
68                Cow::Owned(s) => Some(s.into_boxed_str()),
69            });
70
71        match s {
72            Some(s) => s,
73            // SAFETY: Immediately above, we've established that `None` means `buf` is valid UTF-8.
74            None => unsafe { std::str::from_utf8_unchecked(&self.buf) },
75        }
76    }
77
78    /// Iterates over lines in this output.
79    #[inline]
80    pub fn lines(&self) -> Lines<'_> {
81        self.buf.lines()
82    }
83
84    /// Returns true if the output is empty.
85    #[inline]
86    pub fn is_empty(&self) -> bool {
87        self.buf.is_empty()
88    }
89}
90
91/// The result of executing a child process: either that the process was run and
92/// at least some output was captured, or that the process could not be started
93/// at all.
94#[derive(Clone, Debug)]
95pub enum ChildExecutionOutput {
96    /// The process was run and the output was captured.
97    Output {
98        /// If the process has finished executing, the final state it is in.
99        ///
100        /// `None` means execution is currently in progress.
101        result: Option<ExecutionResult>,
102
103        /// The captured output.
104        output: ChildOutput,
105
106        /// Errors that occurred while waiting on the child process or parsing
107        /// its output.
108        errors: Option<ErrorList<ChildError>>,
109    },
110
111    /// There was a failure to start the process.
112    StartError(ChildStartError),
113}
114
115impl ChildExecutionOutput {
116    /// Returns true if there are any errors in this output.
117    pub(crate) fn has_errors(&self) -> bool {
118        match self {
119            ChildExecutionOutput::Output { errors, result, .. } => {
120                if errors.is_some() {
121                    return true;
122                }
123                if let Some(result) = result {
124                    return !result.is_success();
125                }
126
127                false
128            }
129            ChildExecutionOutput::StartError(_) => true,
130        }
131    }
132}
133
134/// The output of a child process: stdout and/or stderr.
135///
136/// Part of [`ChildExecutionOutput`], and can be used independently as well.
137#[derive(Clone, Debug)]
138pub enum ChildOutput {
139    /// The output was split into stdout and stderr.
140    Split(ChildSplitOutput),
141
142    /// The output was combined into stdout and stderr.
143    Combined {
144        /// The captured output.
145        output: ChildSingleOutput,
146    },
147}
148
149/// The output of a child process (test or setup script) with split stdout and stderr.
150///
151/// One of the variants of [`ChildOutput`].
152#[derive(Clone, Debug)]
153pub struct ChildSplitOutput {
154    /// The captured stdout, or `None` if the output was not captured.
155    pub stdout: Option<ChildSingleOutput>,
156
157    /// The captured stderr, or `None` if the output was not captured.
158    pub stderr: Option<ChildSingleOutput>,
159}