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, 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
126/// A helper for displaying test instances with formatting.
127pub struct DisplayTestInstance<'a> {
128    stress_index: Option<StressIndex>,
129    display_counter_index: Option<DisplayCounterIndex>,
130    instance: TestInstanceId<'a>,
131    styles: &'a Styles,
132    max_width: Option<usize>,
133}
134
135impl<'a> DisplayTestInstance<'a> {
136    /// Creates a new display formatter for a test instance.
137    pub fn new(
138        stress_index: Option<StressIndex>,
139        display_counter_index: Option<DisplayCounterIndex>,
140        instance: TestInstanceId<'a>,
141        styles: &'a Styles,
142    ) -> Self {
143        Self {
144            stress_index,
145            display_counter_index,
146            instance,
147            styles,
148            max_width: None,
149        }
150    }
151
152    pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
153        self.max_width = Some(max_width);
154        self
155    }
156}
157
158impl fmt::Display for DisplayTestInstance<'_> {
159    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160        // Figure out the widths for each component.
161        let stress_index_str = if let Some(stress_index) = self.stress_index {
162            format!(
163                "[{}] ",
164                DisplayStressIndex {
165                    stress_index,
166                    count_style: self.styles.count,
167                }
168            )
169        } else {
170            String::new()
171        };
172        let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
173            format!("{display_counter_index} ")
174        } else {
175            String::new()
176        };
177        let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
178        let test_name_str = format!(
179            "{}",
180            DisplayTestName::new(self.instance.test_name, self.styles)
181        );
182
183        // If a max width is defined, trim strings until they fit into it.
184        if let Some(max_width) = self.max_width {
185            // We have to be careful while computing string width -- the strings
186            // above include ANSI escape codes which have a display width of
187            // zero.
188            let stress_index_width = text_width(&stress_index_str);
189            let counter_index_width = text_width(&counter_index_str);
190            let binary_id_width = text_width(&binary_id_str);
191            let test_name_width = text_width(&test_name_str);
192
193            // Truncate components in order, from most important to keep to least:
194            //
195            // * stress-index (left-aligned)
196            // * counter index (left-aligned)
197            // * binary ID (left-aligned)
198            // * test name (right-aligned)
199            let mut stress_index_resolved_width = stress_index_width;
200            let mut counter_index_resolved_width = counter_index_width;
201            let mut binary_id_resolved_width = binary_id_width;
202            let mut test_name_resolved_width = test_name_width;
203
204            // Truncate stress-index first.
205            if stress_index_resolved_width > max_width {
206                stress_index_resolved_width = max_width;
207            }
208
209            // Truncate counter index next.
210            let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
211            if counter_index_resolved_width > remaining_width {
212                counter_index_resolved_width = remaining_width;
213            }
214
215            // Truncate binary ID next.
216            let remaining_width = max_width
217                .saturating_sub(stress_index_resolved_width)
218                .saturating_sub(counter_index_resolved_width);
219            if binary_id_resolved_width > remaining_width {
220                binary_id_resolved_width = remaining_width;
221            }
222
223            // Truncate test name last.
224            let remaining_width = max_width
225                .saturating_sub(stress_index_resolved_width)
226                .saturating_sub(counter_index_resolved_width)
227                .saturating_sub(binary_id_resolved_width);
228            if test_name_resolved_width > remaining_width {
229                test_name_resolved_width = remaining_width;
230            }
231
232            // Now truncate the strings if applicable.
233            let test_name_truncated_str = if test_name_resolved_width == test_name_width {
234                test_name_str
235            } else {
236                // Right-align the test name.
237                truncate_ansi_aware(
238                    &test_name_str,
239                    test_name_width.saturating_sub(test_name_resolved_width),
240                    test_name_width,
241                )
242            };
243            let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
244                binary_id_str
245            } else {
246                // Left-align the binary ID.
247                truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
248            };
249            let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
250            {
251                counter_index_str
252            } else {
253                // Left-align the counter index.
254                truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
255            };
256            let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
257                stress_index_str
258            } else {
259                // Left-align the stress index.
260                truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
261            };
262
263            write!(
264                f,
265                "{}{}{}{}",
266                stress_index_truncated_str,
267                counter_index_truncated_str,
268                binary_id_truncated_str,
269                test_name_truncated_str,
270            )
271        } else {
272            write!(
273                f,
274                "{}{}{}{}",
275                stress_index_str, counter_index_str, binary_id_str, test_name_str
276            )
277        }
278    }
279}
280
281fn text_width(text: &str) -> usize {
282    // Technically, the width of a string may not be the same as the sum of the
283    // widths of its characters. But managing truncation is pretty difficult. See
284    // https://docs.rs/unicode-width/latest/unicode_width/#rules-for-determining-width.
285    //
286    // This is quite difficult to manage truncation for, so we just use the sum
287    // of the widths of the string's characters (both here and in
288    // truncate_ansi_aware below).
289    strip_ansi_escapes::strip_str(text)
290        .chars()
291        .map(|c| c.width().unwrap_or(0))
292        .sum()
293}
294
295fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
296    let mut pos = 0;
297    let mut res = String::new();
298    for (s, is_ansi) in AnsiCodeIterator::new(text) {
299        if is_ansi {
300            res.push_str(s);
301            continue;
302        } else if pos >= end {
303            // We retain ANSI escape codes, so this is `continue` rather than
304            // `break`.
305            continue;
306        }
307
308        for c in s.chars() {
309            let c_width = c.width().unwrap_or(0);
310            if start <= pos && pos + c_width <= end {
311                res.push(c);
312            }
313            pos += c_width;
314            if pos > end {
315                // no need to iterate over the rest of s
316                break;
317            }
318        }
319    }
320
321    res
322}
323
324pub(crate) struct DisplayScriptInstance {
325    stress_index: Option<StressIndex>,
326    script_id: ScriptId,
327    full_command: String,
328    script_id_style: Style,
329    count_style: Style,
330}
331
332impl DisplayScriptInstance {
333    pub(crate) fn new(
334        stress_index: Option<StressIndex>,
335        script_id: ScriptId,
336        command: &str,
337        args: &[String],
338        script_id_style: Style,
339        count_style: Style,
340    ) -> Self {
341        let full_command =
342            shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
343
344        Self {
345            stress_index,
346            script_id,
347            full_command,
348            script_id_style,
349            count_style,
350        }
351    }
352}
353
354impl fmt::Display for DisplayScriptInstance {
355    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
356        if let Some(stress_index) = self.stress_index {
357            write!(
358                f,
359                "[{}] ",
360                DisplayStressIndex {
361                    stress_index,
362                    count_style: self.count_style,
363                }
364            )?;
365        }
366        write!(
367            f,
368            "{}: {}",
369            self.script_id.style(self.script_id_style),
370            self.full_command,
371        )
372    }
373}
374
375struct DisplayStressIndex {
376    stress_index: StressIndex,
377    count_style: Style,
378}
379
380impl fmt::Display for DisplayStressIndex {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        match self.stress_index.total {
383            Some(total) => {
384                write!(
385                    f,
386                    "{:>width$}/{}",
387                    (self.stress_index.current + 1).style(self.count_style),
388                    total.style(self.count_style),
389                    width = u32_decimal_char_width(total.get()),
390                )
391            }
392            None => {
393                write!(
394                    f,
395                    "{}",
396                    (self.stress_index.current + 1).style(self.count_style)
397                )
398            }
399        }
400    }
401}
402
403/// Counter index display for test instances.
404pub enum DisplayCounterIndex {
405    /// A counter with current and total counts.
406    Counter {
407        /// Current count.
408        current: usize,
409        /// Total count.
410        total: usize,
411    },
412    /// A padded display.
413    Padded {
414        /// Character to use for padding.
415        character: char,
416        /// Width to pad to.
417        width: usize,
418    },
419}
420
421impl DisplayCounterIndex {
422    /// Creates a new counter display.
423    pub fn new_counter(current: usize, total: usize) -> Self {
424        Self::Counter { current, total }
425    }
426
427    /// Creates a new padded display.
428    pub fn new_padded(character: char, width: usize) -> Self {
429        Self::Padded { character, width }
430    }
431}
432
433impl fmt::Display for DisplayCounterIndex {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        match self {
436            Self::Counter { current, total } => {
437                write!(
438                    f,
439                    "({:>width$}/{})",
440                    current,
441                    total,
442                    width = usize_decimal_char_width(*total)
443                )
444            }
445            Self::Padded { character, width } => {
446                // Rendered as:
447                //
448                // (  20/5000)
449                // (---------)
450                let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
451                write!(f, "({s})")
452            }
453        }
454    }
455}
456
457pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
458    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
459    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
460    // actual number of digits.
461    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
462}
463
464pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
465    // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And
466    // None for 0 which we unwrap to the same as 1). Add 1 to it to get the
467    // actual number of digits.
468    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
469}
470
471/// Write out a test name.
472pub(crate) fn write_test_name(
473    name: &TestCaseName,
474    style: &Styles,
475    writer: &mut dyn WriteStr,
476) -> io::Result<()> {
477    let (module_path, trailing) = name.module_path_and_name();
478    if let Some(module_path) = module_path {
479        write!(
480            writer,
481            "{}{}",
482            module_path.style(style.module_path),
483            "::".style(style.module_path)
484        )?;
485    }
486    write!(writer, "{}", trailing.style(style.test_name))?;
487
488    Ok(())
489}
490
491/// Wrapper for displaying a test name with styling.
492pub(crate) struct DisplayTestName<'a> {
493    name: &'a TestCaseName,
494    styles: &'a Styles,
495}
496
497impl<'a> DisplayTestName<'a> {
498    pub(crate) fn new(name: &'a TestCaseName, styles: &'a Styles) -> Self {
499        Self { name, styles }
500    }
501}
502
503impl fmt::Display for DisplayTestName<'_> {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        let (module_path, trailing) = self.name.module_path_and_name();
506        if let Some(module_path) = module_path {
507            write!(
508                f,
509                "{}{}",
510                module_path.style(self.styles.module_path),
511                "::".style(self.styles.module_path)
512            )?;
513        }
514        write!(f, "{}", trailing.style(self.styles.test_name))?;
515
516        Ok(())
517    }
518}
519
520pub(crate) fn convert_build_platform(
521    platform: nextest_metadata::BuildPlatform,
522) -> guppy::graph::cargo::BuildPlatform {
523    match platform {
524        nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
525        nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
526    }
527}
528
529// ---
530// Functions below copied from cargo-util to avoid pulling in a bunch of dependencies
531// ---
532
533/// Returns the name of the environment variable used for searching for
534/// dynamic libraries.
535pub(crate) fn dylib_path_envvar() -> &'static str {
536    if cfg!(windows) {
537        "PATH"
538    } else if cfg!(target_os = "macos") {
539        // When loading and linking a dynamic library or bundle, dlopen
540        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
541        // DYLD_FALLBACK_LIBRARY_PATH.
542        // In the Mach-O format, a dynamic library has an "install path."
543        // Clients linking against the library record this path, and the
544        // dynamic linker, dyld, uses it to locate the library.
545        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
546        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
547        // find the library in the install path.
548        // Setting DYLD_LIBRARY_PATH can easily have unintended
549        // consequences.
550        //
551        // Also, DYLD_LIBRARY_PATH appears to have significant performance
552        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
553        // slow with it on CI.
554        "DYLD_FALLBACK_LIBRARY_PATH"
555    } else {
556        "LD_LIBRARY_PATH"
557    }
558}
559
560/// Returns a list of directories that are searched for dynamic libraries.
561///
562/// Note that some operating systems will have defaults if this is empty that
563/// will need to be dealt with.
564pub(crate) fn dylib_path() -> Vec<PathBuf> {
565    match std::env::var_os(dylib_path_envvar()) {
566        Some(var) => std::env::split_paths(&var).collect(),
567        None => Vec::new(),
568    }
569}
570
571/// On Windows, convert relative paths to always use forward slashes.
572#[cfg(windows)]
573pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
574    if !rel_path.is_relative() {
575        panic!("path for conversion to forward slash '{rel_path}' is not relative");
576    }
577    rel_path.as_str().replace('\\', "/").into()
578}
579
580#[cfg(not(windows))]
581pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
582    rel_path.to_path_buf()
583}
584
585/// On Windows, convert relative paths to use the main separator.
586#[cfg(windows)]
587pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
588    if !rel_path.is_relative() {
589        panic!("path for conversion to backslash '{rel_path}' is not relative");
590    }
591    rel_path.as_str().replace('/', "\\").into()
592}
593
594#[cfg(not(windows))]
595pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
596    rel_path.to_path_buf()
597}
598
599/// Join relative paths using forward slashes.
600pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
601    assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
602    assert!(path.is_relative(), "path {path} is relative",);
603    format!("{rel_path}/{path}").into()
604}
605
606#[derive(Debug)]
607pub(crate) struct FormattedDuration(pub(crate) Duration);
608
609impl fmt::Display for FormattedDuration {
610    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611        let duration = self.0.as_secs_f64();
612        if duration > 60.0 {
613            write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
614        } else {
615            write!(f, "{duration:.2}s")
616        }
617    }
618}
619
620// "exited with"/"terminated via"
621pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
622    match AbortStatus::extract(exit_status) {
623        Some(abort_status) => display_abort_status(abort_status),
624        None => match exit_status.code() {
625            Some(code) => format!("exited with exit code {code}"),
626            None => "exited with an unknown error".to_owned(),
627        },
628    }
629}
630
631/// Displays the abort status.
632pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
633    match abort_status {
634        #[cfg(unix)]
635        AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
636            Some(s) => {
637                format!("aborted with signal {sig} (SIG{s})")
638            }
639            None => {
640                format!("aborted with signal {sig}")
641            }
642        },
643        #[cfg(windows)]
644        AbortStatus::WindowsNtStatus(nt_status) => {
645            format!(
646                "aborted with code {}",
647                // TODO: pass down a style here
648                crate::helpers::display_nt_status(nt_status, Style::new())
649            )
650        }
651        #[cfg(windows)]
652        AbortStatus::JobObject => "terminated via job object".to_string(),
653    }
654}
655
656#[cfg(unix)]
657pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
658    // These signal numbers are the same on at least Linux, macOS, FreeBSD and illumos.
659    //
660    // TODO: glibc has sigabbrev_np, and POSIX-1.2024 adds sig2str which has been available on
661    // illumos for many years:
662    // https://pubs.opengroup.org/onlinepubs/9799919799/functions/sig2str.html. We should use these
663    // if available.
664    match signal {
665        1 => Some("HUP"),
666        2 => Some("INT"),
667        3 => Some("QUIT"),
668        4 => Some("ILL"),
669        5 => Some("TRAP"),
670        6 => Some("ABRT"),
671        8 => Some("FPE"),
672        9 => Some("KILL"),
673        11 => Some("SEGV"),
674        13 => Some("PIPE"),
675        14 => Some("ALRM"),
676        15 => Some("TERM"),
677        _ => None,
678    }
679}
680
681#[cfg(windows)]
682pub(crate) fn display_nt_status(
683    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
684    bold_style: Style,
685) -> String {
686    // 10 characters ("0x" + 8 hex digits) is how an NTSTATUS with the high bit
687    // set is going to be displayed anyway. This makes all possible displays
688    // uniform.
689    let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
690    // Convert the NTSTATUS to a Win32 error code.
691    let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
692
693    if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
694        // The Win32 code was not found.
695        return bolded_status;
696    }
697
698    format!(
699        "{bolded_status}: {}",
700        io::Error::from_raw_os_error(win32_code as i32)
701    )
702}
703
704#[derive(Copy, Clone, Debug)]
705pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
706
707impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
708where
709    T: fmt::Display,
710{
711    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
712        write!(f, "'{}'", self.0)
713    }
714}
715
716// From https://twitter.com/8051Enthusiast/status/1571909110009921538
717unsafe extern "C" {
718    fn __nextest_external_symbol_that_does_not_exist();
719}
720
721/// Formats an interceptor (debugger or tracer) error message for too many tests.
722pub fn format_interceptor_too_many_tests(
723    cli_opt_name: &str,
724    mode: NextestRunMode,
725    test_count: usize,
726    test_instances: &[OwnedTestInstanceId],
727    list_styles: &Styles,
728    count_style: Style,
729) -> String {
730    let mut msg = format!(
731        "--{} requires exactly one {}, but {} {} were selected:",
732        cli_opt_name,
733        plural::tests_plural_if(mode, false),
734        test_count.style(count_style),
735        plural::tests_str(mode, test_count)
736    );
737
738    for test_instance in test_instances {
739        let display = DisplayTestInstance::new(None, None, test_instance.as_ref(), list_styles);
740        swrite!(msg, "\n  {}", display);
741    }
742
743    if test_count > test_instances.len() {
744        let remaining = test_count - test_instances.len();
745        swrite!(
746            msg,
747            "\n  ... and {} more {}",
748            remaining.style(count_style),
749            plural::tests_str(mode, remaining)
750        );
751    }
752
753    msg
754}
755
756#[inline]
757#[expect(dead_code)]
758pub(crate) fn statically_unreachable() -> ! {
759    unsafe {
760        __nextest_external_symbol_that_does_not_exist();
761    }
762    unreachable!("linker symbol above cannot be resolved")
763}
764
765#[cfg(test)]
766mod test {
767    use super::*;
768
769    #[test]
770    fn test_decimal_char_width() {
771        assert_eq!(1, usize_decimal_char_width(0));
772        assert_eq!(1, usize_decimal_char_width(1));
773        assert_eq!(1, usize_decimal_char_width(5));
774        assert_eq!(1, usize_decimal_char_width(9));
775        assert_eq!(2, usize_decimal_char_width(10));
776        assert_eq!(2, usize_decimal_char_width(11));
777        assert_eq!(2, usize_decimal_char_width(99));
778        assert_eq!(3, usize_decimal_char_width(100));
779        assert_eq!(3, usize_decimal_char_width(999));
780    }
781}