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