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