nextest_runner/reporter/
helpers.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use bstr::ByteSlice;
5use owo_colors::Style;
6
7/// Given a slice, find the index of the point at which highlighting should end.
8///
9/// Returns a value in the range [0, slice.len()].
10pub fn highlight_end(slice: &[u8]) -> usize {
11    // We want to highlight the first two lines of the output.
12    let mut iter = slice.find_iter(b"\n");
13    match iter.next() {
14        Some(_) => {
15            match iter.next() {
16                Some(second) => second,
17                // No second newline found, so highlight the entire slice.
18                None => slice.len(),
19            }
20        }
21        // No newline found, so highlight the entire slice.
22        None => slice.len(),
23    }
24}
25
26#[derive(Debug, Default, Clone)]
27pub(super) struct Styles {
28    pub(super) is_colorized: bool,
29    pub(super) count: Style,
30    pub(super) pass: Style,
31    pub(super) retry: Style,
32    pub(super) fail: Style,
33    pub(super) skip: Style,
34    pub(super) script_id: Style,
35    pub(super) list_styles: crate::list::Styles,
36    /// Style for the unique prefix portion of run IDs (highlighted).
37    pub(super) run_id_prefix: Style,
38    /// Style for the non-unique rest portion of run IDs (dimmed).
39    pub(super) run_id_rest: Style,
40}
41
42impl Styles {
43    pub(super) fn colorize(&mut self) {
44        self.is_colorized = true;
45        self.count = Style::new().bold();
46        self.pass = Style::new().green().bold();
47        self.retry = Style::new().magenta().bold();
48        self.fail = Style::new().red().bold();
49        self.skip = Style::new().yellow().bold();
50        self.script_id = Style::new().blue().bold();
51        self.list_styles.colorize();
52        self.run_id_prefix = Style::new().bold().purple();
53        self.run_id_rest = Style::new().bright_black();
54    }
55}
56
57// Port of std::str::floor_char_boundary to Rust < 1.91.0. Remove after MSRV
58// has been bumped to 1.91 or above.
59pub(crate) const fn floor_char_boundary(s: &str, index: usize) -> usize {
60    if index >= s.len() {
61        s.len()
62    } else {
63        let mut i = index;
64        while i > 0 {
65            if is_utf8_char_boundary(s.as_bytes()[i]) {
66                break;
67            }
68            i -= 1;
69        }
70
71        //  The character boundary will be within four bytes of the index
72        debug_assert!(i >= index.saturating_sub(3));
73
74        i
75    }
76}
77
78#[inline]
79const fn is_utf8_char_boundary(b: u8) -> bool {
80    // This is bit magic equivalent to: b < 128 || b >= 192
81    (b as i8) >= -0x40
82}
83
84/// Calls the provided callback with chunks of text, breaking at newline
85/// boundaries when possible.
86///
87/// This function processes text in chunks to avoid performance issues when
88/// printing large amounts of text at once. It attempts to break chunks at
89/// newline boundaries within the specified maximum chunk size, but will
90/// handle lines longer than the maximum by searching forward for the next
91/// newline.
92///
93/// # Parameters
94/// - `text`: The text to process
95/// - `max_chunk_bytes`: Maximum size of each chunk in bytes
96/// - `callback`: Function called with each chunk of text (includes trailing newlines)
97pub(crate) fn print_lines_in_chunks(
98    text: &str,
99    max_chunk_bytes: usize,
100    mut callback: impl FnMut(&str),
101) {
102    let mut remaining = text;
103    while !remaining.is_empty() {
104        // Find the maximum index to search for the last newline, respecting
105        // UTF-8 character boundaries.
106        let max = floor_char_boundary(remaining, max_chunk_bytes);
107
108        // Search backwards for the last \n within the chunk.
109        let last_newline = remaining[..max].rfind('\n');
110
111        if let Some(index) = last_newline {
112            // Found a newline within max_chunk_bytes. Include it in the chunk.
113            callback(&remaining[..=index]);
114            remaining = &remaining[index + 1..];
115        } else {
116            // No newline within max_chunk_bytes. Search forward for the next newline.
117            let next_newline = remaining[max..].find('\n');
118
119            if let Some(index) = next_newline {
120                // Found a newline after max_chunk_bytes. Include it in the chunk.
121                callback(&remaining[..=index + max]);
122                remaining = &remaining[index + max + 1..];
123            } else {
124                // No more newlines, so print everything that's left.
125                callback(remaining);
126                remaining = "";
127            }
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_highlight_end() {
138        let tests: &[(&str, usize)] = &[
139            ("", 0),
140            ("\n", 1),
141            ("foo", 3),
142            ("foo\n", 4),
143            ("foo\nbar", 7),
144            ("foo\nbar\n", 7),
145            ("foo\nbar\nbaz", 7),
146            ("foo\nbar\nbaz\n", 7),
147            ("\nfoo\nbar\nbaz", 4),
148        ];
149
150        for (input, output) in tests {
151            assert_eq!(
152                highlight_end(input.as_bytes()),
153                *output,
154                "for input {input:?}"
155            );
156        }
157    }
158
159    #[test]
160    fn test_print_lines_in_chunks() {
161        let tests: &[(&str, &str, usize, &[&str])] = &[
162            // (description, input, chunk_size, expected_chunks)
163            ("empty string", "", 1024, &[]),
164            ("single line no newline", "hello", 1024, &["hello"]),
165            ("single line with newline", "hello\n", 1024, &["hello\n"]),
166            (
167                "multiple lines small",
168                "line1\nline2\nline3\n",
169                1024,
170                &["line1\nline2\nline3\n"],
171            ),
172            (
173                "breaks at newline",
174                "line1\nline2\nline3\n",
175                10,
176                &["line1\n", "line2\n", "line3\n"],
177            ),
178            (
179                "multiple lines per chunk",
180                "line1\nline2\nline3\n",
181                13,
182                &["line1\nline2\n", "line3\n"],
183            ),
184            (
185                "long line with newline",
186                &format!("{}\n", "a".repeat(2000)),
187                1024,
188                &[&format!("{}\n", "a".repeat(2000))],
189            ),
190            (
191                "long line no newline",
192                &"a".repeat(2000),
193                1024,
194                &[&"a".repeat(2000)],
195            ),
196            ("exact boundary", "123456789\n", 10, &["123456789\n"]),
197            (
198                "newline at boundary",
199                "12345678\nabcdefgh\n",
200                10,
201                &["12345678\n", "abcdefgh\n"],
202            ),
203            (
204                "utf8 emoji",
205                "hello๐Ÿ˜€\nworld\n",
206                10,
207                &["hello๐Ÿ˜€\n", "world\n"],
208            ),
209            (
210                "utf8 near boundary",
211                "1234๐Ÿ˜€\nabcd\n",
212                7,
213                &["1234๐Ÿ˜€\n", "abcd\n"],
214            ),
215            (
216                "consecutive newlines",
217                "line1\n\n\nline2\n",
218                10,
219                &["line1\n\n\n", "line2\n"],
220            ),
221            (
222                "no trailing newline",
223                "line1\nline2\nline3",
224                10,
225                &["line1\n", "line2\n", "line3"],
226            ),
227            (
228                "mixed lengths",
229                "short\nvery_long_line_that_exceeds_chunk_size\nmedium_line\nok\n",
230                20,
231                &[
232                    "short\n",
233                    "very_long_line_that_exceeds_chunk_size\n",
234                    "medium_line\nok\n",
235                ],
236            ),
237        ];
238
239        for (description, input, chunk_size, expected) in tests {
240            let mut chunks = Vec::new();
241            print_lines_in_chunks(input, *chunk_size, |chunk| chunks.push(chunk.to_string()));
242            assert_eq!(
243                chunks,
244                expected.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
245                "test case: {}",
246                description
247            );
248        }
249    }
250}