Skip to main content

nextest_runner/
helpers.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! General support code for nextest-runner.
5
6use crate::{
7    config::scripts::ScriptId,
8    list::{OwnedTestInstanceId, Styles, TestInstanceId},
9    reporter::events::{AbortStatus, StressIndex},
10    run_mode::NextestRunMode,
11    write_str::WriteStr,
12};
13use camino::{Utf8Path, Utf8PathBuf};
14use console::AnsiCodeIterator;
15use nextest_metadata::TestCaseName;
16use owo_colors::{OwoColorize, Style};
17use std::{fmt, io, ops::ControlFlow, path::PathBuf, process::ExitStatus, time::Duration};
18use swrite::{SWrite, swrite};
19use unicode_width::UnicodeWidthChar;
20
21/// Utilities for pluralizing various words based on count or plurality.
22pub mod plural {
23    use crate::run_mode::NextestRunMode;
24
25    /// Returns "were" if `plural` is true, otherwise "was".
26    pub fn were_plural_if(plural: bool) -> &'static str {
27        if plural { "were" } else { "was" }
28    }
29
30    /// Returns "setup script" if `count` is 1, otherwise "setup scripts".
31    pub fn setup_scripts_str(count: usize) -> &'static str {
32        if count == 1 {
33            "setup script"
34        } else {
35            "setup scripts"
36        }
37    }
38
39    /// Returns:
40    ///
41    /// * If `mode` is `Test`: "test" if `count` is 1, otherwise "tests".
42    /// * If `mode` is `Benchmark`: "benchmark" if `count` is 1, otherwise "benchmarks".
43    pub fn tests_str(mode: NextestRunMode, count: usize) -> &'static str {
44        tests_plural_if(mode, count != 1)
45    }
46
47    /// Returns:
48    ///
49    /// * If `mode` is `Test`: "tests" if `plural` is true, otherwise "test".
50    /// * If `mode` is `Benchmark`: "benchmarks" if `plural` is true, otherwise "benchmark".
51    pub fn tests_plural_if(mode: NextestRunMode, plural: bool) -> &'static str {
52        match (mode, plural) {
53            (NextestRunMode::Test, true) => "tests",
54            (NextestRunMode::Test, false) => "test",
55            (NextestRunMode::Benchmark, true) => "benchmarks",
56            (NextestRunMode::Benchmark, false) => "benchmark",
57        }
58    }
59
60    /// Returns "tests" or "benchmarks" based on the run mode.
61    pub fn tests_plural(mode: NextestRunMode) -> &'static str {
62        match mode {
63            NextestRunMode::Test => "tests",
64            NextestRunMode::Benchmark => "benchmarks",
65        }
66    }
67
68    /// Returns "binary" if `count` is 1, otherwise "binaries".
69    pub fn binaries_str(count: usize) -> &'static str {
70        if count == 1 { "binary" } else { "binaries" }
71    }
72
73    /// Returns "path" if `count` is 1, otherwise "paths".
74    pub fn paths_str(count: usize) -> &'static str {
75        if count == 1 { "path" } else { "paths" }
76    }
77
78    /// Returns "file" if `count` is 1, otherwise "files".
79    pub fn files_str(count: usize) -> &'static str {
80        if count == 1 { "file" } else { "files" }
81    }
82
83    /// Returns "directory" if `count` is 1, otherwise "directories".
84    pub fn directories_str(count: usize) -> &'static str {
85        if count == 1 {
86            "directory"
87        } else {
88            "directories"
89        }
90    }
91
92    /// Returns "this crate" if `count` is 1, otherwise "these crates".
93    pub fn this_crate_str(count: usize) -> &'static str {
94        if count == 1 {
95            "this crate"
96        } else {
97            "these crates"
98        }
99    }
100
101    /// Returns "library" if `count` is 1, otherwise "libraries".
102    pub fn libraries_str(count: usize) -> &'static str {
103        if count == 1 { "library" } else { "libraries" }
104    }
105
106    /// Returns "filter" if `count` is 1, otherwise "filters".
107    pub fn filters_str(count: usize) -> &'static str {
108        if count == 1 { "filter" } else { "filters" }
109    }
110
111    /// Returns "section" if `count` is 1, otherwise "sections".
112    pub fn sections_str(count: usize) -> &'static str {
113        if count == 1 { "section" } else { "sections" }
114    }
115
116    /// Returns "iteration" if `count` is 1, otherwise "iterations".
117    pub fn iterations_str(count: u32) -> &'static str {
118        if count == 1 {
119            "iteration"
120        } else {
121            "iterations"
122        }
123    }
124
125    /// Returns "run" if `count` is 1, otherwise "runs".
126    pub fn runs_str(count: usize) -> &'static str {
127        if count == 1 { "run" } else { "runs" }
128    }
129
130    /// Returns "orphan" if `count` is 1, otherwise "orphans".
131    pub fn orphans_str(count: usize) -> &'static str {
132        if count == 1 { "orphan" } else { "orphans" }
133    }
134
135    /// Returns "error" if `count` is 1, otherwise "errors".
136    pub fn errors_str(count: usize) -> &'static str {
137        if count == 1 { "error" } else { "errors" }
138    }
139
140    /// Returns "exists" if `count` is 1, otherwise "exist".
141    pub fn exist_str(count: usize) -> &'static str {
142        if count == 1 { "exists" } else { "exist" }
143    }
144
145    /// Returns "ends" if `count` is 1, otherwise "end".
146    pub fn end_str(count: usize) -> &'static str {
147        if count == 1 { "ends" } else { "end" }
148    }
149
150    /// Returns "remains" if `count` is 1, otherwise "remain".
151    pub fn remain_str(count: usize) -> &'static str {
152        if count == 1 { "remains" } else { "remain" }
153    }
154}
155
156/// A helper for displaying test instances with formatting.
157pub struct DisplayTestInstance<'a> {
158    stress_index: Option<StressIndex>,
159    display_counter_index: Option<DisplayCounterIndex>,
160    instance: TestInstanceId<'a>,
161    styles: &'a Styles,
162    max_width: Option<usize>,
163}
164
165impl<'a> DisplayTestInstance<'a> {
166    /// Creates a new display formatter for a test instance.
167    pub fn new(
168        stress_index: Option<StressIndex>,
169        display_counter_index: Option<DisplayCounterIndex>,
170        instance: TestInstanceId<'a>,
171        styles: &'a Styles,
172    ) -> Self {
173        Self {
174            stress_index,
175            display_counter_index,
176            instance,
177            styles,
178            max_width: None,
179        }
180    }
181
182    pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
183        self.max_width = Some(max_width);
184        self
185    }
186}
187
188impl fmt::Display for DisplayTestInstance<'_> {
189    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
190        // Figure out the widths for each component.
191        let stress_index_str = if let Some(stress_index) = self.stress_index {
192            format!(
193                "[{}] ",
194                DisplayStressIndex {
195                    stress_index,
196                    count_style: self.styles.count,
197                }
198            )
199        } else {
200            String::new()
201        };
202        let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
203            format!("{display_counter_index} ")
204        } else {
205            String::new()
206        };
207        let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
208        let test_name_str = format!(
209            "{}",
210            DisplayTestName::new(self.instance.test_name, self.styles)
211        );
212
213        // If a max width is defined, trim strings until they fit into it.
214        if let Some(max_width) = self.max_width {
215            // We have to be careful while computing string width -- the strings
216            // above include ANSI escape codes which have a display width of
217            // zero.
218            let stress_index_width = text_width(&stress_index_str);
219            let counter_index_width = text_width(&counter_index_str);
220            let binary_id_width = text_width(&binary_id_str);
221            let test_name_width = text_width(&test_name_str);
222
223            // Truncate components in order, from most important to keep to least:
224            //
225            // * stress-index (left-aligned)
226            // * counter index (left-aligned)
227            // * binary ID (left-aligned)
228            // * test name (right-aligned)
229            let mut stress_index_resolved_width = stress_index_width;
230            let mut counter_index_resolved_width = counter_index_width;
231            let mut binary_id_resolved_width = binary_id_width;
232            let mut test_name_resolved_width = test_name_width;
233
234            // Truncate stress-index first.
235            if stress_index_resolved_width > max_width {
236                stress_index_resolved_width = max_width;
237            }
238
239            // Truncate counter index next.
240            let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
241            if counter_index_resolved_width > remaining_width {
242                counter_index_resolved_width = remaining_width;
243            }
244
245            // Truncate binary ID next.
246            let remaining_width = max_width
247                .saturating_sub(stress_index_resolved_width)
248                .saturating_sub(counter_index_resolved_width);
249            if binary_id_resolved_width > remaining_width {
250                binary_id_resolved_width = remaining_width;
251            }
252
253            // Truncate test name last.
254            let remaining_width = max_width
255                .saturating_sub(stress_index_resolved_width)
256                .saturating_sub(counter_index_resolved_width)
257                .saturating_sub(binary_id_resolved_width);
258            if test_name_resolved_width > remaining_width {
259                test_name_resolved_width = remaining_width;
260            }
261
262            // Now truncate the strings if applicable.
263            let test_name_truncated_str = if test_name_resolved_width == test_name_width {
264                test_name_str
265            } else {
266                // Right-align the test name.
267                truncate_ansi_aware(
268                    &test_name_str,
269                    test_name_width.saturating_sub(test_name_resolved_width),
270                    test_name_width,
271                )
272            };
273            let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
274                binary_id_str
275            } else {
276                // Left-align the binary ID.
277                truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
278            };
279            let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
280            {
281                counter_index_str
282            } else {
283                // Left-align the counter index.
284                truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
285            };
286            let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
287                stress_index_str
288            } else {
289                // Left-align the stress index.
290                truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
291            };
292
293            write!(
294                f,
295                "{}{}{}{}",
296                stress_index_truncated_str,
297                counter_index_truncated_str,
298                binary_id_truncated_str,
299                test_name_truncated_str,
300            )
301        } else {
302            write!(
303                f,
304                "{}{}{}{}",
305                stress_index_str, counter_index_str, binary_id_str, test_name_str
306            )
307        }
308    }
309}
310
311fn text_width(text: &str) -> usize {
312    // Technically, the width of a string may not be the same as the sum of the
313    // widths of its characters. But managing truncation is pretty difficult. See
314    // https://docs.rs/unicode-width/latest/unicode_width/#rules-for-determining-width.
315    //
316    // This is quite difficult to manage truncation for, so we just use the sum
317    // of the widths of the string's characters (both here and in
318    // truncate_ansi_aware below).
319    strip_ansi_escapes::strip_str(text)
320        .chars()
321        .map(|c| c.width().unwrap_or(0))
322        .sum()
323}
324
325fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
326    let mut pos = 0;
327    let mut res = String::new();
328    for (s, is_ansi) in AnsiCodeIterator::new(text) {
329        if is_ansi {
330            res.push_str(s);
331            continue;
332        } else if pos >= end {
333            // We retain ANSI escape codes, so this is `continue` rather than
334            // `break`.
335            continue;
336        }
337
338        for c in s.chars() {
339            let c_width = c.width().unwrap_or(0);
340            if start <= pos && pos + c_width <= end {
341                res.push(c);
342            }
343            pos += c_width;
344            if pos > end {
345                // no need to iterate over the rest of s
346                break;
347            }
348        }
349    }
350
351    res
352}
353
354pub(crate) struct DisplayScriptInstance {
355    stress_index: Option<StressIndex>,
356    script_id: ScriptId,
357    full_command: String,
358    script_id_style: Style,
359    count_style: Style,
360}
361
362impl DisplayScriptInstance {
363    pub(crate) fn new(
364        stress_index: Option<StressIndex>,
365        script_id: ScriptId,
366        command: &str,
367        args: &[String],
368        script_id_style: Style,
369        count_style: Style,
370    ) -> Self {
371        let full_command =
372            shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
373
374        Self {
375            stress_index,
376            script_id,
377            full_command,
378            script_id_style,
379            count_style,
380        }
381    }
382}
383
384impl fmt::Display for DisplayScriptInstance {
385    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
386        if let Some(stress_index) = self.stress_index {
387            write!(
388                f,
389                "[{}] ",
390                DisplayStressIndex {
391                    stress_index,
392                    count_style: self.count_style,
393                }
394            )?;
395        }
396        write!(
397            f,
398            "{}: {}",
399            self.script_id.style(self.script_id_style),
400            self.full_command,
401        )
402    }
403}
404
405struct DisplayStressIndex {
406    stress_index: StressIndex,
407    count_style: Style,
408}
409
410impl fmt::Display for DisplayStressIndex {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self.stress_index.total {
413            Some(total) => {
414                write!(
415                    f,
416                    "{:>width$}/{}",
417                    (self.stress_index.current + 1).style(self.count_style),
418                    total.style(self.count_style),
419                    width = u32_decimal_char_width(total.get()),
420                )
421            }
422            None => {
423                write!(
424                    f,
425                    "{}",
426                    (self.stress_index.current + 1).style(self.count_style)
427                )
428            }
429        }
430    }
431}
432
433/// Counter index display for test instances.
434pub enum DisplayCounterIndex {
435    /// A counter with current and total counts.
436    Counter {
437        /// Current count.
438        current: usize,
439        /// Total count.
440        total: usize,
441    },
442    /// A padded display.
443    Padded {
444        /// Character to use for padding.
445        character: char,
446        /// Width to pad to.
447        width: usize,
448    },
449}
450
451impl DisplayCounterIndex {
452    /// Creates a new counter display.
453    pub fn new_counter(current: usize, total: usize) -> Self {
454        Self::Counter { current, total }
455    }
456
457    /// Creates a new padded display.
458    pub fn new_padded(character: char, width: usize) -> Self {
459        Self::Padded { character, width }
460    }
461}
462
463impl fmt::Display for DisplayCounterIndex {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        match self {
466            Self::Counter { current, total } => {
467                write!(
468                    f,
469                    "({:>width$}/{})",
470                    current,
471                    total,
472                    width = usize_decimal_char_width(*total)
473                )
474            }
475            Self::Padded { character, width } => {
476                // Rendered as:
477                //
478                // (  20/5000)
479                // (---------)
480                let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
481                write!(f, "({s})")
482            }
483        }
484    }
485}
486
487pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
488    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
489    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
490    // actual number of digits.
491    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
492}
493
494pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
495    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
496    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
497    // actual number of digits.
498    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
499}
500
501pub(crate) fn u64_decimal_char_width(n: u64) -> usize {
502    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
503    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
504    // actual number of digits.
505    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
506}
507
508/// Write out a test name.
509pub(crate) fn write_test_name(
510    name: &TestCaseName,
511    style: &Styles,
512    writer: &mut dyn WriteStr,
513) -> io::Result<()> {
514    let (module_path, trailing) = name.module_path_and_name();
515    if let Some(module_path) = module_path {
516        write!(
517            writer,
518            "{}{}",
519            module_path.style(style.module_path),
520            "::".style(style.module_path)
521        )?;
522    }
523    write!(writer, "{}", trailing.style(style.test_name))?;
524
525    Ok(())
526}
527
528/// Wrapper for displaying a test name with styling.
529pub(crate) struct DisplayTestName<'a> {
530    name: &'a TestCaseName,
531    styles: &'a Styles,
532}
533
534impl<'a> DisplayTestName<'a> {
535    pub(crate) fn new(name: &'a TestCaseName, styles: &'a Styles) -> Self {
536        Self { name, styles }
537    }
538}
539
540impl fmt::Display for DisplayTestName<'_> {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        let (module_path, trailing) = self.name.module_path_and_name();
543        if let Some(module_path) = module_path {
544            write!(
545                f,
546                "{}{}",
547                module_path.style(self.styles.module_path),
548                "::".style(self.styles.module_path)
549            )?;
550        }
551        write!(f, "{}", trailing.style(self.styles.test_name))?;
552
553        Ok(())
554    }
555}
556
557pub(crate) fn convert_build_platform(
558    platform: nextest_metadata::BuildPlatform,
559) -> guppy::graph::cargo::BuildPlatform {
560    match platform {
561        nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
562        nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
563    }
564}
565
566// ---
567// Functions below copied from cargo-util to avoid pulling in a bunch of dependencies
568// ---
569
570/// Returns the name of the environment variable used for searching for
571/// dynamic libraries.
572pub(crate) fn dylib_path_envvar() -> &'static str {
573    if cfg!(windows) {
574        "PATH"
575    } else if cfg!(target_os = "macos") {
576        // When loading and linking a dynamic library or bundle, dlopen
577        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
578        // DYLD_FALLBACK_LIBRARY_PATH.
579        // In the Mach-O format, a dynamic library has an "install path."
580        // Clients linking against the library record this path, and the
581        // dynamic linker, dyld, uses it to locate the library.
582        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
583        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
584        // find the library in the install path.
585        // Setting DYLD_LIBRARY_PATH can easily have unintended
586        // consequences.
587        //
588        // Also, DYLD_LIBRARY_PATH appears to have significant performance
589        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
590        // slow with it on CI.
591        "DYLD_FALLBACK_LIBRARY_PATH"
592    } else {
593        "LD_LIBRARY_PATH"
594    }
595}
596
597/// Returns a list of directories that are searched for dynamic libraries.
598///
599/// Note that some operating systems will have defaults if this is empty that
600/// will need to be dealt with.
601pub(crate) fn dylib_path() -> Vec<PathBuf> {
602    match std::env::var_os(dylib_path_envvar()) {
603        Some(var) => std::env::split_paths(&var).collect(),
604        None => Vec::new(),
605    }
606}
607
608/// On Windows, convert relative paths to always use forward slashes.
609#[cfg(windows)]
610pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
611    if !rel_path.is_relative() {
612        panic!("path for conversion to forward slash '{rel_path}' is not relative");
613    }
614    rel_path.as_str().replace('\\', "/").into()
615}
616
617#[cfg(not(windows))]
618pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
619    rel_path.to_path_buf()
620}
621
622/// On Windows, convert relative paths to use the main separator.
623#[cfg(windows)]
624pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
625    if !rel_path.is_relative() {
626        panic!("path for conversion to backslash '{rel_path}' is not relative");
627    }
628    rel_path.as_str().replace('/', "\\").into()
629}
630
631#[cfg(not(windows))]
632pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
633    rel_path.to_path_buf()
634}
635
636/// Join relative paths using forward slashes.
637pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
638    assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
639    assert!(path.is_relative(), "path {path} is relative",);
640    format!("{rel_path}/{path}").into()
641}
642
643#[derive(Debug)]
644pub(crate) struct FormattedDuration(pub(crate) Duration);
645
646impl fmt::Display for FormattedDuration {
647    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648        let duration = self.0.as_secs_f64();
649        if duration > 60.0 {
650            write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
651        } else {
652            write!(f, "{duration:.2}s")
653        }
654    }
655}
656
657#[derive(Debug)]
658pub(crate) struct FormattedRelativeDuration(pub(crate) Duration);
659
660impl fmt::Display for FormattedRelativeDuration {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        // Adapted from
663        // https://github.com/atuinsh/atuin/blob/bd2a54e1b1/crates/atuin/src/command/client/search/duration.rs#L5,
664        // and used under the MIT license.
665        fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
666            if value > 0 {
667                ControlFlow::Break((unit, value))
668            } else {
669                ControlFlow::Continue(())
670            }
671        }
672
673        // impl taken and modified from
674        // https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331
675        // Copyright (c) 2016 The humantime Developers
676        fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
677            let secs = f.as_secs();
678            let nanos = f.subsec_nanos();
679
680            let years = secs / 31_557_600; // 365.25d
681            let year_days = secs % 31_557_600;
682            let months = year_days / 2_630_016; // 30.44d
683            let month_days = year_days % 2_630_016;
684            let days = month_days / 86400;
685            let day_secs = month_days % 86400;
686            let hours = day_secs / 3600;
687            let minutes = day_secs % 3600 / 60;
688            let seconds = day_secs % 60;
689
690            let millis = nanos / 1_000_000;
691            let micros = nanos / 1_000;
692
693            // a difference between our impl and the original is that
694            // we only care about the most-significant segment of the duration.
695            // If the item call returns `Break`, then the `?` will early-return.
696            // This allows for a very concise impl
697            item("y", years)?;
698            item("mo", months)?;
699            item("d", days)?;
700            item("h", hours)?;
701            item("m", minutes)?;
702            item("s", seconds)?;
703            item("ms", u64::from(millis))?;
704            item("us", u64::from(micros))?;
705            item("ns", u64::from(nanos))?;
706            ControlFlow::Continue(())
707        }
708
709        match fmt(self.0) {
710            ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
711            ControlFlow::Continue(()) => write!(f, "0s"),
712        }
713    }
714}
715
716/// Characters used for terminal output theming.
717///
718/// Provides both ASCII and Unicode variants for horizontal bars, progress indicators,
719/// spinners, and tree display characters.
720#[derive(Clone, Debug)]
721pub struct ThemeCharacters {
722    hbar: char,
723    progress_chars: &'static str,
724    use_unicode: bool,
725}
726
727impl Default for ThemeCharacters {
728    fn default() -> Self {
729        Self {
730            hbar: '-',
731            progress_chars: "=> ",
732            use_unicode: false,
733        }
734    }
735}
736
737impl ThemeCharacters {
738    /// Switches to Unicode characters for richer terminal output.
739    pub fn use_unicode(&mut self) {
740        self.hbar = '─';
741        // https://mike42.me/blog/2018-06-make-better-cli-progress-bars-with-unicode-block-characters
742        self.progress_chars = "█▉▊▋▌▍▎▏ ";
743        self.use_unicode = true;
744    }
745
746    /// Returns the horizontal bar character.
747    pub fn hbar_char(&self) -> char {
748        self.hbar
749    }
750
751    /// Returns a horizontal bar of the specified width.
752    pub fn hbar(&self, width: usize) -> String {
753        std::iter::repeat_n(self.hbar, width).collect()
754    }
755
756    /// Returns the progress bar characters.
757    pub fn progress_chars(&self) -> &'static str {
758        self.progress_chars
759    }
760
761    /// Returns the tree branch character for non-last children: `├─` or `|-`.
762    pub fn tree_branch(&self) -> &'static str {
763        if self.use_unicode { "├─" } else { "|-" }
764    }
765
766    /// Returns the tree branch character for the last child: `└─` or `\-`.
767    pub fn tree_last(&self) -> &'static str {
768        if self.use_unicode { "└─" } else { "\\-" }
769    }
770
771    /// Returns the tree continuation line: `│ ` or `| `.
772    pub fn tree_continuation(&self) -> &'static str {
773        if self.use_unicode { "│ " } else { "| " }
774    }
775
776    /// Returns the tree space (no continuation): `  `.
777    pub fn tree_space(&self) -> &'static str {
778        "  "
779    }
780}
781
782// "exited with"/"terminated via"
783pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
784    match AbortStatus::extract(exit_status) {
785        Some(abort_status) => display_abort_status(abort_status),
786        None => match exit_status.code() {
787            Some(code) => format!("exited with exit code {code}"),
788            None => "exited with an unknown error".to_owned(),
789        },
790    }
791}
792
793/// Displays the abort status.
794pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
795    match abort_status {
796        #[cfg(unix)]
797        AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
798            Some(s) => {
799                format!("aborted with signal {sig} (SIG{s})")
800            }
801            None => {
802                format!("aborted with signal {sig}")
803            }
804        },
805        #[cfg(windows)]
806        AbortStatus::WindowsNtStatus(nt_status) => {
807            format!(
808                "aborted with code {}",
809                // TODO: pass down a style here
810                crate::helpers::display_nt_status(nt_status, Style::new())
811            )
812        }
813        #[cfg(windows)]
814        AbortStatus::JobObject => "terminated via job object".to_string(),
815    }
816}
817
818#[cfg(unix)]
819pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
820    // These signal numbers are the same on at least Linux, macOS, FreeBSD and illumos.
821    //
822    // TODO: glibc has sigabbrev_np, and POSIX-1.2024 adds sig2str which has been available on
823    // illumos for many years:
824    // https://pubs.opengroup.org/onlinepubs/9799919799/functions/sig2str.html. We should use these
825    // if available.
826    match signal {
827        1 => Some("HUP"),
828        2 => Some("INT"),
829        3 => Some("QUIT"),
830        4 => Some("ILL"),
831        5 => Some("TRAP"),
832        6 => Some("ABRT"),
833        8 => Some("FPE"),
834        9 => Some("KILL"),
835        11 => Some("SEGV"),
836        13 => Some("PIPE"),
837        14 => Some("ALRM"),
838        15 => Some("TERM"),
839        _ => None,
840    }
841}
842
843#[cfg(windows)]
844pub(crate) fn display_nt_status(
845    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
846    bold_style: Style,
847) -> String {
848    // 10 characters ("0x" + 8 hex digits) is how an NTSTATUS with the high bit
849    // set is going to be displayed anyway. This makes all possible displays
850    // uniform.
851    let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
852
853    match windows_nt_status_message(nt_status) {
854        Some(message) => format!("{bolded_status}: {message}"),
855        None => bolded_status,
856    }
857}
858
859/// Returns the human-readable message for a Windows NT status code, if available.
860#[cfg(windows)]
861pub(crate) fn windows_nt_status_message(
862    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
863) -> Option<smol_str::SmolStr> {
864    // Convert the NTSTATUS to a Win32 error code.
865    let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
866
867    if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
868        // The Win32 code was not found.
869        return None;
870    }
871
872    Some(smol_str::SmolStr::new(
873        io::Error::from_raw_os_error(win32_code as i32).to_string(),
874    ))
875}
876
877#[derive(Copy, Clone, Debug)]
878pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
879
880impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
881where
882    T: fmt::Display,
883{
884    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885        write!(f, "'{}'", self.0)
886    }
887}
888
889// From https://twitter.com/8051Enthusiast/status/1571909110009921538
890unsafe extern "C" {
891    fn __nextest_external_symbol_that_does_not_exist();
892}
893
894/// Formats an interceptor (debugger or tracer) error message for too many tests.
895pub fn format_interceptor_too_many_tests(
896    cli_opt_name: &str,
897    mode: NextestRunMode,
898    test_count: usize,
899    test_instances: &[OwnedTestInstanceId],
900    list_styles: &Styles,
901    count_style: Style,
902) -> String {
903    let mut msg = format!(
904        "--{} requires exactly one {}, but {} {} were selected:",
905        cli_opt_name,
906        plural::tests_plural_if(mode, false),
907        test_count.style(count_style),
908        plural::tests_str(mode, test_count)
909    );
910
911    for test_instance in test_instances {
912        let display = DisplayTestInstance::new(None, None, test_instance.as_ref(), list_styles);
913        swrite!(msg, "\n  {}", display);
914    }
915
916    if test_count > test_instances.len() {
917        let remaining = test_count - test_instances.len();
918        swrite!(
919            msg,
920            "\n  ... and {} more {}",
921            remaining.style(count_style),
922            plural::tests_str(mode, remaining)
923        );
924    }
925
926    msg
927}
928
929#[inline]
930#[expect(dead_code)]
931pub(crate) fn statically_unreachable() -> ! {
932    unsafe {
933        __nextest_external_symbol_that_does_not_exist();
934    }
935    unreachable!("linker symbol above cannot be resolved")
936}
937
938#[cfg(test)]
939mod test {
940    use super::*;
941
942    #[test]
943    fn test_decimal_char_width() {
944        assert_eq!(1, usize_decimal_char_width(0));
945        assert_eq!(1, usize_decimal_char_width(1));
946        assert_eq!(1, usize_decimal_char_width(5));
947        assert_eq!(1, usize_decimal_char_width(9));
948        assert_eq!(2, usize_decimal_char_width(10));
949        assert_eq!(2, usize_decimal_char_width(11));
950        assert_eq!(2, usize_decimal_char_width(99));
951        assert_eq!(3, usize_decimal_char_width(100));
952        assert_eq!(3, usize_decimal_char_width(999));
953    }
954
955    #[test]
956    fn test_u64_decimal_char_width() {
957        assert_eq!(1, u64_decimal_char_width(0));
958        assert_eq!(1, u64_decimal_char_width(1));
959        assert_eq!(1, u64_decimal_char_width(9));
960        assert_eq!(2, u64_decimal_char_width(10));
961        assert_eq!(2, u64_decimal_char_width(99));
962        assert_eq!(3, u64_decimal_char_width(100));
963        assert_eq!(3, u64_decimal_char_width(999));
964        assert_eq!(6, u64_decimal_char_width(999_999));
965        assert_eq!(7, u64_decimal_char_width(1_000_000));
966        assert_eq!(8, u64_decimal_char_width(10_000_000));
967        assert_eq!(8, u64_decimal_char_width(11_000_000));
968    }
969}