1use bstr::ByteSlice;
5use owo_colors::Style;
6
7pub fn highlight_end(slice: &[u8]) -> usize {
11 let mut iter = slice.find_iter(b"\n");
13 match iter.next() {
14 Some(_) => {
15 match iter.next() {
16 Some(second) => second,
17 None => slice.len(),
19 }
20 }
21 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 pub(super) run_id_prefix: Style,
38 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
57pub(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 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 (b as i8) >= -0x40
82}
83
84pub(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 let max = floor_char_boundary(remaining, max_chunk_bytes);
107
108 let last_newline = remaining[..max].rfind('\n');
110
111 if let Some(index) = last_newline {
112 callback(&remaining[..=index]);
114 remaining = &remaining[index + 1..];
115 } else {
116 let next_newline = remaining[max..].find('\n');
118
119 if let Some(index) = next_newline {
120 callback(&remaining[..=index + max]);
122 remaining = &remaining[index + max + 1..];
123 } else {
124 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 ("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}