nextest_runner/
helpers.rs

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