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