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 = DisplayTestName::new(self.instance.test_name, self.styles).to_string();
209
210        // If a max width is defined, trim strings until they fit into it.
211        if let Some(max_width) = self.max_width {
212            // We have to be careful while computing string width -- the strings
213            // above include ANSI escape codes which have a display width of
214            // zero.
215            let stress_index_width = text_width(&stress_index_str);
216            let counter_index_width = text_width(&counter_index_str);
217            let binary_id_width = text_width(&binary_id_str);
218            let test_name_width = text_width(&test_name_str);
219
220            // Truncate components in order, from most important to keep to least:
221            //
222            // * stress-index (left-aligned)
223            // * counter index (left-aligned)
224            // * binary ID (left-aligned)
225            // * test name (right-aligned)
226            let mut stress_index_resolved_width = stress_index_width;
227            let mut counter_index_resolved_width = counter_index_width;
228            let mut binary_id_resolved_width = binary_id_width;
229            let mut test_name_resolved_width = test_name_width;
230
231            // Truncate stress-index first.
232            if stress_index_resolved_width > max_width {
233                stress_index_resolved_width = max_width;
234            }
235
236            // Truncate counter index next.
237            let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
238            if counter_index_resolved_width > remaining_width {
239                counter_index_resolved_width = remaining_width;
240            }
241
242            // Truncate binary ID next.
243            let remaining_width = max_width
244                .saturating_sub(stress_index_resolved_width)
245                .saturating_sub(counter_index_resolved_width);
246            if binary_id_resolved_width > remaining_width {
247                binary_id_resolved_width = remaining_width;
248            }
249
250            // Truncate test name last.
251            let remaining_width = max_width
252                .saturating_sub(stress_index_resolved_width)
253                .saturating_sub(counter_index_resolved_width)
254                .saturating_sub(binary_id_resolved_width);
255            if test_name_resolved_width > remaining_width {
256                test_name_resolved_width = remaining_width;
257            }
258
259            // Now truncate the strings if applicable.
260            let test_name_truncated_str = if test_name_resolved_width == test_name_width {
261                test_name_str
262            } else {
263                // Right-align the test name.
264                truncate_ansi_aware(
265                    &test_name_str,
266                    test_name_width.saturating_sub(test_name_resolved_width),
267                    test_name_width,
268                )
269            };
270            let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
271                binary_id_str
272            } else {
273                // Left-align the binary ID.
274                truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
275            };
276            let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
277            {
278                counter_index_str
279            } else {
280                // Left-align the counter index.
281                truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
282            };
283            let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
284                stress_index_str
285            } else {
286                // Left-align the stress index.
287                truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
288            };
289
290            write!(
291                f,
292                "{}{}{}{}",
293                stress_index_truncated_str,
294                counter_index_truncated_str,
295                binary_id_truncated_str,
296                test_name_truncated_str,
297            )
298        } else {
299            write!(
300                f,
301                "{}{}{}{}",
302                stress_index_str, counter_index_str, binary_id_str, test_name_str
303            )
304        }
305    }
306}
307
308fn text_width(text: &str) -> usize {
309    // Technically, the width of a string may not be the same as the sum of the
310    // widths of its characters. But managing truncation is pretty difficult. See
311    // https://docs.rs/unicode-width/latest/unicode_width/#rules-for-determining-width.
312    //
313    // This is quite difficult to manage truncation for, so we just use the sum
314    // of the widths of the string's characters (both here and in
315    // truncate_ansi_aware below).
316    strip_ansi_escapes::strip_str(text)
317        .chars()
318        .map(|c| c.width().unwrap_or(0))
319        .sum()
320}
321
322fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
323    let mut pos = 0;
324    let mut res = String::new();
325    for (s, is_ansi) in AnsiCodeIterator::new(text) {
326        if is_ansi {
327            res.push_str(s);
328            continue;
329        } else if pos >= end {
330            // We retain ANSI escape codes, so this is `continue` rather than
331            // `break`.
332            continue;
333        }
334
335        for c in s.chars() {
336            let c_width = c.width().unwrap_or(0);
337            if start <= pos && pos + c_width <= end {
338                res.push(c);
339            }
340            pos += c_width;
341            if pos > end {
342                // no need to iterate over the rest of s
343                break;
344            }
345        }
346    }
347
348    res
349}
350
351pub(crate) struct DisplayScriptInstance {
352    stress_index: Option<StressIndex>,
353    script_id: ScriptId,
354    full_command: String,
355    script_id_style: Style,
356    count_style: Style,
357}
358
359impl DisplayScriptInstance {
360    pub(crate) fn new(
361        stress_index: Option<StressIndex>,
362        script_id: ScriptId,
363        command: &str,
364        args: &[String],
365        script_id_style: Style,
366        count_style: Style,
367    ) -> Self {
368        let full_command =
369            shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
370
371        Self {
372            stress_index,
373            script_id,
374            full_command,
375            script_id_style,
376            count_style,
377        }
378    }
379}
380
381impl fmt::Display for DisplayScriptInstance {
382    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
383        if let Some(stress_index) = self.stress_index {
384            write!(
385                f,
386                "[{}] ",
387                DisplayStressIndex {
388                    stress_index,
389                    count_style: self.count_style,
390                }
391            )?;
392        }
393        write!(
394            f,
395            "{}: {}",
396            self.script_id.style(self.script_id_style),
397            self.full_command,
398        )
399    }
400}
401
402struct DisplayStressIndex {
403    stress_index: StressIndex,
404    count_style: Style,
405}
406
407impl fmt::Display for DisplayStressIndex {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        match self.stress_index.total {
410            Some(total) => {
411                write!(
412                    f,
413                    "{:>width$}/{}",
414                    (self.stress_index.current + 1).style(self.count_style),
415                    total.style(self.count_style),
416                    width = decimal_char_width(total.get()),
417                )
418            }
419            None => {
420                write!(
421                    f,
422                    "{}",
423                    (self.stress_index.current + 1).style(self.count_style)
424                )
425            }
426        }
427    }
428}
429
430/// Counter index display for test instances.
431pub enum DisplayCounterIndex {
432    /// A counter with current and total counts.
433    Counter {
434        /// Current count.
435        current: usize,
436        /// Total count.
437        total: usize,
438    },
439    /// A padded display.
440    Padded {
441        /// Character to use for padding.
442        character: char,
443        /// Width to pad to.
444        width: usize,
445    },
446}
447
448impl DisplayCounterIndex {
449    /// Creates a new counter display.
450    pub fn new_counter(current: usize, total: usize) -> Self {
451        Self::Counter { current, total }
452    }
453
454    /// Creates a new padded display.
455    pub fn new_padded(character: char, width: usize) -> Self {
456        Self::Padded { character, width }
457    }
458}
459
460impl fmt::Display for DisplayCounterIndex {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        match self {
463            Self::Counter { current, total } => {
464                write!(
465                    f,
466                    "({:>width$}/{})",
467                    current,
468                    total,
469                    width = decimal_char_width(*total)
470                )
471            }
472            Self::Padded { character, width } => {
473                // Rendered as:
474                //
475                // (  20/5000)
476                // (---------)
477                let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
478                write!(f, "({s})")
479            }
480        }
481    }
482}
483
484/// Returns the number of decimal digits needed to display `n`.
485///
486/// Works for any unsigned integer type that supports `checked_ilog10`.
487pub(crate) fn decimal_char_width<T>(n: T) -> usize
488where
489    T: TryInto<u128> + Copy,
490{
491    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
492    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
493    // actual number of digits.
494    let n: u128 = n.try_into().ok().expect("converted to u128");
495    (n.checked_ilog10().unwrap_or(0) + 1) as usize
496}
497
498/// Write out a test name.
499pub(crate) fn write_test_name(
500    name: &TestCaseName,
501    style: &Styles,
502    writer: &mut dyn WriteStr,
503) -> io::Result<()> {
504    let (module_path, trailing) = name.module_path_and_name();
505    if let Some(module_path) = module_path {
506        write!(
507            writer,
508            "{}{}",
509            module_path.style(style.module_path),
510            "::".style(style.module_path)
511        )?;
512    }
513    write!(writer, "{}", trailing.style(style.test_name))?;
514
515    Ok(())
516}
517
518/// Wrapper for displaying a test name with styling.
519pub(crate) struct DisplayTestName<'a> {
520    name: &'a TestCaseName,
521    styles: &'a Styles,
522}
523
524impl<'a> DisplayTestName<'a> {
525    pub(crate) fn new(name: &'a TestCaseName, styles: &'a Styles) -> Self {
526        Self { name, styles }
527    }
528}
529
530impl fmt::Display for DisplayTestName<'_> {
531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532        let (module_path, trailing) = self.name.module_path_and_name();
533        if let Some(module_path) = module_path {
534            write!(
535                f,
536                "{}{}",
537                module_path.style(self.styles.module_path),
538                "::".style(self.styles.module_path)
539            )?;
540        }
541        write!(f, "{}", trailing.style(self.styles.test_name))?;
542
543        Ok(())
544    }
545}
546
547pub(crate) fn convert_build_platform(
548    platform: nextest_metadata::BuildPlatform,
549) -> guppy::graph::cargo::BuildPlatform {
550    match platform {
551        nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
552        nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
553    }
554}
555
556// ---
557// Functions below copied from cargo-util to avoid pulling in a bunch of dependencies
558// ---
559
560/// Returns the name of the environment variable used for searching for
561/// dynamic libraries.
562pub(crate) fn dylib_path_envvar() -> &'static str {
563    if cfg!(windows) {
564        "PATH"
565    } else if cfg!(target_os = "macos") {
566        // When loading and linking a dynamic library or bundle, dlopen
567        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
568        // DYLD_FALLBACK_LIBRARY_PATH.
569        // In the Mach-O format, a dynamic library has an "install path."
570        // Clients linking against the library record this path, and the
571        // dynamic linker, dyld, uses it to locate the library.
572        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
573        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
574        // find the library in the install path.
575        // Setting DYLD_LIBRARY_PATH can easily have unintended
576        // consequences.
577        //
578        // Also, DYLD_LIBRARY_PATH appears to have significant performance
579        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
580        // slow with it on CI.
581        "DYLD_FALLBACK_LIBRARY_PATH"
582    } else {
583        "LD_LIBRARY_PATH"
584    }
585}
586
587/// Returns a list of directories that are searched for dynamic libraries.
588///
589/// Note that some operating systems will have defaults if this is empty that
590/// will need to be dealt with.
591pub(crate) fn dylib_path() -> Vec<PathBuf> {
592    match std::env::var_os(dylib_path_envvar()) {
593        Some(var) => std::env::split_paths(&var).collect(),
594        None => Vec::new(),
595    }
596}
597
598/// On Windows, convert relative paths to always use forward slashes.
599#[cfg(windows)]
600pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
601    if !rel_path.is_relative() {
602        panic!("path for conversion to forward slash '{rel_path}' is not relative");
603    }
604    rel_path.as_str().replace('\\', "/").into()
605}
606
607#[cfg(not(windows))]
608pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
609    rel_path.to_path_buf()
610}
611
612/// On Windows, convert relative paths to use the main separator.
613#[cfg(windows)]
614pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
615    if !rel_path.is_relative() {
616        panic!("path for conversion to backslash '{rel_path}' is not relative");
617    }
618    rel_path.as_str().replace('/', "\\").into()
619}
620
621#[cfg(not(windows))]
622pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
623    rel_path.to_path_buf()
624}
625
626/// Join relative paths using forward slashes.
627pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
628    assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
629    assert!(path.is_relative(), "path {path} is relative",);
630    format!("{rel_path}/{path}").into()
631}
632
633#[derive(Debug)]
634pub(crate) struct FormattedDuration(pub(crate) Duration);
635
636/// Controls how sub-second precision is handled when formatting durations.
637#[derive(Copy, Clone, Debug)]
638pub(crate) enum DurationRounding {
639    /// Truncate sub-second precision (floor). Use for elapsed time.
640    Floor,
641
642    /// Round up to the next second when sub-second milliseconds are present
643    /// (ceiling). Use for remaining time so that elapsed + remaining doesn't
644    /// appear to exceed the total.
645    Ceiling,
646}
647
648/// Formats a duration as `HH:MM:SS`.
649#[derive(Debug)]
650pub(crate) struct FormattedHhMmSs {
651    pub(crate) duration: Duration,
652    pub(crate) rounding: DurationRounding,
653}
654
655impl fmt::Display for FormattedHhMmSs {
656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657        let total_secs = self.duration.as_secs();
658        let total_secs = match self.rounding {
659            DurationRounding::Ceiling if self.duration.subsec_millis() > 0 => total_secs + 1,
660            _ => total_secs,
661        };
662        let secs = total_secs % 60;
663        let total_mins = total_secs / 60;
664        let mins = total_mins % 60;
665        let hours = total_mins / 60;
666
667        write!(f, "{hours:02}:{mins:02}:{secs:02}")
668    }
669}
670
671impl fmt::Display for FormattedDuration {
672    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
673        let duration = self.0.as_secs_f64();
674        if duration > 60.0 {
675            write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
676        } else {
677            write!(f, "{duration:.2}s")
678        }
679    }
680}
681
682#[derive(Debug)]
683pub(crate) struct FormattedRelativeDuration(pub(crate) Duration);
684
685impl fmt::Display for FormattedRelativeDuration {
686    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
687        // Adapted from
688        // https://github.com/atuinsh/atuin/blob/bd2a54e1b1/crates/atuin/src/command/client/search/duration.rs#L5,
689        // and used under the MIT license.
690        fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
691            if value > 0 {
692                ControlFlow::Break((unit, value))
693            } else {
694                ControlFlow::Continue(())
695            }
696        }
697
698        // impl taken and modified from
699        // https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331
700        // Copyright (c) 2016 The humantime Developers
701        fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
702            let secs = f.as_secs();
703            let nanos = f.subsec_nanos();
704
705            let years = secs / 31_557_600; // 365.25d
706            let year_days = secs % 31_557_600;
707            let months = year_days / 2_630_016; // 30.44d
708            let month_days = year_days % 2_630_016;
709            let days = month_days / 86400;
710            let day_secs = month_days % 86400;
711            let hours = day_secs / 3600;
712            let minutes = day_secs % 3600 / 60;
713            let seconds = day_secs % 60;
714
715            let millis = nanos / 1_000_000;
716            let micros = nanos / 1_000;
717
718            // a difference between our impl and the original is that
719            // we only care about the most-significant segment of the duration.
720            // If the item call returns `Break`, then the `?` will early-return.
721            // This allows for a very concise impl
722            item("y", years)?;
723            item("mo", months)?;
724            item("d", days)?;
725            item("h", hours)?;
726            item("m", minutes)?;
727            item("s", seconds)?;
728            item("ms", u64::from(millis))?;
729            item("us", u64::from(micros))?;
730            item("ns", u64::from(nanos))?;
731            ControlFlow::Continue(())
732        }
733
734        match fmt(self.0) {
735            ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
736            ControlFlow::Continue(()) => write!(f, "0s"),
737        }
738    }
739}
740
741/// Characters used for terminal output theming.
742///
743/// Provides both ASCII and Unicode variants for horizontal bars, progress indicators,
744/// spinners, and tree display characters.
745#[derive(Clone, Debug)]
746pub struct ThemeCharacters {
747    hbar: char,
748    progress_chars: &'static str,
749    use_unicode: bool,
750}
751
752impl Default for ThemeCharacters {
753    fn default() -> Self {
754        Self {
755            hbar: '-',
756            progress_chars: "=> ",
757            use_unicode: false,
758        }
759    }
760}
761
762impl ThemeCharacters {
763    /// Creates a `ThemeCharacters` with Unicode auto-detected for the given
764    /// stream.
765    pub fn detect(stream: supports_unicode::Stream) -> Self {
766        let mut this = Self::default();
767        if supports_unicode::on(stream) {
768            this.use_unicode();
769        }
770        this
771    }
772
773    /// Switches to Unicode characters for richer terminal output.
774    pub fn use_unicode(&mut self) {
775        self.hbar = '─';
776        // https://mike42.me/blog/2018-06-make-better-cli-progress-bars-with-unicode-block-characters
777        self.progress_chars = "█▉▊▋▌▍▎▏ ";
778        self.use_unicode = true;
779    }
780
781    /// Returns the horizontal bar character.
782    pub fn hbar_char(&self) -> char {
783        self.hbar
784    }
785
786    /// Returns a horizontal bar of the specified width.
787    pub fn hbar(&self, width: usize) -> String {
788        std::iter::repeat_n(self.hbar, width).collect()
789    }
790
791    /// Returns the progress bar characters.
792    pub fn progress_chars(&self) -> &'static str {
793        self.progress_chars
794    }
795
796    /// Returns the tree branch character for non-last children: `├─` or `|-`.
797    pub fn tree_branch(&self) -> &'static str {
798        if self.use_unicode { "├─" } else { "|-" }
799    }
800
801    /// Returns the tree branch character for the last child: `└─` or `\-`.
802    pub fn tree_last(&self) -> &'static str {
803        if self.use_unicode { "└─" } else { "\\-" }
804    }
805
806    /// Returns the tree continuation line: `│ ` or `| `.
807    pub fn tree_continuation(&self) -> &'static str {
808        if self.use_unicode { "│ " } else { "| " }
809    }
810
811    /// Returns the tree space (no continuation): `  `.
812    pub fn tree_space(&self) -> &'static str {
813        "  "
814    }
815}
816
817// "exited with"/"terminated via"
818pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
819    match AbortStatus::extract(exit_status) {
820        Some(abort_status) => display_abort_status(abort_status),
821        None => match exit_status.code() {
822            Some(code) => format!("exited with exit code {code}"),
823            None => "exited with an unknown error".to_owned(),
824        },
825    }
826}
827
828/// Displays the abort status.
829pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
830    match abort_status {
831        #[cfg(unix)]
832        AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
833            Some(s) => {
834                format!("aborted with signal {sig} (SIG{s})")
835            }
836            None => {
837                format!("aborted with signal {sig}")
838            }
839        },
840        #[cfg(windows)]
841        AbortStatus::WindowsNtStatus(nt_status) => {
842            format!(
843                "aborted with code {}",
844                // TODO: pass down a style here
845                crate::helpers::display_nt_status(nt_status, Style::new())
846            )
847        }
848        #[cfg(windows)]
849        AbortStatus::JobObject => "terminated via job object".to_string(),
850    }
851}
852
853#[cfg(unix)]
854pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
855    // These signal numbers are the same on at least Linux, macOS, FreeBSD and illumos.
856    //
857    // TODO: glibc has sigabbrev_np, and POSIX-1.2024 adds sig2str which has been available on
858    // illumos for many years:
859    // https://pubs.opengroup.org/onlinepubs/9799919799/functions/sig2str.html. We should use these
860    // if available.
861    match signal {
862        1 => Some("HUP"),
863        2 => Some("INT"),
864        3 => Some("QUIT"),
865        4 => Some("ILL"),
866        5 => Some("TRAP"),
867        6 => Some("ABRT"),
868        8 => Some("FPE"),
869        9 => Some("KILL"),
870        11 => Some("SEGV"),
871        13 => Some("PIPE"),
872        14 => Some("ALRM"),
873        15 => Some("TERM"),
874        _ => None,
875    }
876}
877
878#[cfg(windows)]
879pub(crate) fn display_nt_status(
880    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
881    bold_style: Style,
882) -> String {
883    // 10 characters ("0x" + 8 hex digits) is how an NTSTATUS with the high bit
884    // set is going to be displayed anyway. This makes all possible displays
885    // uniform.
886    let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
887
888    match windows_nt_status_message(nt_status) {
889        Some(message) => format!("{bolded_status}: {message}"),
890        None => bolded_status,
891    }
892}
893
894/// Returns the human-readable message for a Windows NT status code, if available.
895#[cfg(windows)]
896pub(crate) fn windows_nt_status_message(
897    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
898) -> Option<smol_str::SmolStr> {
899    // Convert the NTSTATUS to a Win32 error code.
900    let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
901
902    if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
903        // The Win32 code was not found.
904        return None;
905    }
906
907    Some(smol_str::SmolStr::new(
908        io::Error::from_raw_os_error(win32_code as i32).to_string(),
909    ))
910}
911
912#[derive(Copy, Clone, Debug)]
913pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
914
915impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
916where
917    T: fmt::Display,
918{
919    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
920        write!(f, "'{}'", self.0)
921    }
922}
923
924// From https://twitter.com/8051Enthusiast/status/1571909110009921538
925unsafe extern "C" {
926    fn __nextest_external_symbol_that_does_not_exist();
927}
928
929/// Formats an interceptor (debugger or tracer) error message for too many tests.
930pub fn format_interceptor_too_many_tests(
931    cli_opt_name: &str,
932    mode: NextestRunMode,
933    test_count: usize,
934    test_instances: &[OwnedTestInstanceId],
935    list_styles: &Styles,
936    count_style: Style,
937) -> String {
938    let mut msg = format!(
939        "--{} requires exactly one {}, but {} {} were selected:",
940        cli_opt_name,
941        plural::tests_plural_if(mode, false),
942        test_count.style(count_style),
943        plural::tests_str(mode, test_count)
944    );
945
946    for test_instance in test_instances {
947        let display = DisplayTestInstance::new(None, None, test_instance.as_ref(), list_styles);
948        swrite!(msg, "\n  {}", display);
949    }
950
951    if test_count > test_instances.len() {
952        let remaining = test_count - test_instances.len();
953        swrite!(
954            msg,
955            "\n  ... and {} more {}",
956            remaining.style(count_style),
957            plural::tests_str(mode, remaining)
958        );
959    }
960
961    msg
962}
963
964#[inline]
965#[expect(dead_code)]
966pub(crate) fn statically_unreachable() -> ! {
967    unsafe {
968        __nextest_external_symbol_that_does_not_exist();
969    }
970    unreachable!("linker symbol above cannot be resolved")
971}
972
973#[cfg(test)]
974mod test {
975    use super::*;
976
977    #[test]
978    fn test_decimal_char_width() {
979        // Test with usize values.
980        assert_eq!(1, decimal_char_width(0_usize));
981        assert_eq!(1, decimal_char_width(1_usize));
982        assert_eq!(1, decimal_char_width(5_usize));
983        assert_eq!(1, decimal_char_width(9_usize));
984        assert_eq!(2, decimal_char_width(10_usize));
985        assert_eq!(2, decimal_char_width(11_usize));
986        assert_eq!(2, decimal_char_width(99_usize));
987        assert_eq!(3, decimal_char_width(100_usize));
988        assert_eq!(3, decimal_char_width(999_usize));
989
990        // Test with u32 values.
991        assert_eq!(1, decimal_char_width(0_u32));
992        assert_eq!(3, decimal_char_width(100_u32));
993
994        // Test with u64 values.
995        assert_eq!(1, decimal_char_width(0_u64));
996        assert_eq!(1, decimal_char_width(1_u64));
997        assert_eq!(1, decimal_char_width(9_u64));
998        assert_eq!(2, decimal_char_width(10_u64));
999        assert_eq!(2, decimal_char_width(99_u64));
1000        assert_eq!(3, decimal_char_width(100_u64));
1001        assert_eq!(3, decimal_char_width(999_u64));
1002        assert_eq!(6, decimal_char_width(999_999_u64));
1003        assert_eq!(7, decimal_char_width(1_000_000_u64));
1004        assert_eq!(8, decimal_char_width(10_000_000_u64));
1005        assert_eq!(8, decimal_char_width(11_000_000_u64));
1006    }
1007}