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::{Styles, TestInstanceId},
9    reporter::events::{AbortStatus, StressIndex},
10    write_str::WriteStr,
11};
12use camino::{Utf8Path, Utf8PathBuf};
13use owo_colors::{OwoColorize, Style};
14use std::{fmt, io, path::PathBuf, process::ExitStatus, time::Duration};
15
16/// Utilities for pluralizing various words based on count or plurality.
17pub mod plural {
18    /// Returns "were" if `plural` is true, otherwise "was".
19    pub fn were_plural_if(plural: bool) -> &'static str {
20        if plural { "were" } else { "was" }
21    }
22
23    /// Returns "setup script" if `count` is 1, otherwise "setup scripts".
24    pub fn setup_scripts_str(count: usize) -> &'static str {
25        if count == 1 {
26            "setup script"
27        } else {
28            "setup scripts"
29        }
30    }
31
32    /// Returns "test" if `count` is 1, otherwise "tests".
33    pub fn tests_str(count: usize) -> &'static str {
34        tests_plural_if(count != 1)
35    }
36
37    /// Returns "tests" if `plural` is true, otherwise "test".
38    pub fn tests_plural_if(plural: bool) -> &'static str {
39        if plural { "tests" } else { "test" }
40    }
41
42    /// Returns "binary" if `count` is 1, otherwise "binaries".
43    pub fn binaries_str(count: usize) -> &'static str {
44        if count == 1 { "binary" } else { "binaries" }
45    }
46
47    /// Returns "path" if `count` is 1, otherwise "paths".
48    pub fn paths_str(count: usize) -> &'static str {
49        if count == 1 { "path" } else { "paths" }
50    }
51
52    /// Returns "file" if `count` is 1, otherwise "files".
53    pub fn files_str(count: usize) -> &'static str {
54        if count == 1 { "file" } else { "files" }
55    }
56
57    /// Returns "directory" if `count` is 1, otherwise "directories".
58    pub fn directories_str(count: usize) -> &'static str {
59        if count == 1 {
60            "directory"
61        } else {
62            "directories"
63        }
64    }
65
66    /// Returns "this crate" if `count` is 1, otherwise "these crates".
67    pub fn this_crate_str(count: usize) -> &'static str {
68        if count == 1 {
69            "this crate"
70        } else {
71            "these crates"
72        }
73    }
74
75    /// Returns "library" if `count` is 1, otherwise "libraries".
76    pub fn libraries_str(count: usize) -> &'static str {
77        if count == 1 { "library" } else { "libraries" }
78    }
79
80    /// Returns "filter" if `count` is 1, otherwise "filters".
81    pub fn filters_str(count: usize) -> &'static str {
82        if count == 1 { "filter" } else { "filters" }
83    }
84
85    /// Returns "section" if `count` is 1, otherwise "sections".
86    pub fn sections_str(count: usize) -> &'static str {
87        if count == 1 { "section" } else { "sections" }
88    }
89
90    /// Returns "iteration" if `count` is 1, otherwise "iterations".
91    pub fn iterations_str(count: u32) -> &'static str {
92        if count == 1 {
93            "iteration"
94        } else {
95            "iterations"
96        }
97    }
98}
99
100pub(crate) struct DisplayTestInstance<'a> {
101    stress_index: Option<StressIndex>,
102    instance: TestInstanceId<'a>,
103    styles: &'a Styles,
104}
105
106impl<'a> DisplayTestInstance<'a> {
107    pub(crate) fn new(
108        stress_index: Option<StressIndex>,
109        instance: TestInstanceId<'a>,
110        styles: &'a Styles,
111    ) -> Self {
112        Self {
113            stress_index,
114            instance,
115            styles,
116        }
117    }
118}
119
120impl fmt::Display for DisplayTestInstance<'_> {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        if let Some(stress_index) = self.stress_index {
123            write!(
124                f,
125                "[{}] ",
126                DisplayStressIndex {
127                    stress_index,
128                    count_style: self.styles.count,
129                }
130            )?;
131        }
132
133        write!(
134            f,
135            "{} ",
136            self.instance.binary_id.style(self.styles.binary_id),
137        )?;
138        fmt_write_test_name(self.instance.test_name, self.styles, f)
139    }
140}
141
142pub(crate) struct DisplayScriptInstance {
143    stress_index: Option<StressIndex>,
144    script_id: ScriptId,
145    full_command: String,
146    script_id_style: Style,
147    count_style: Style,
148}
149
150impl DisplayScriptInstance {
151    pub(crate) fn new(
152        stress_index: Option<StressIndex>,
153        script_id: ScriptId,
154        command: &str,
155        args: &[String],
156        script_id_style: Style,
157        count_style: Style,
158    ) -> Self {
159        let full_command =
160            shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
161
162        Self {
163            stress_index,
164            script_id,
165            full_command,
166            script_id_style,
167            count_style,
168        }
169    }
170}
171
172impl fmt::Display for DisplayScriptInstance {
173    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174        if let Some(stress_index) = self.stress_index {
175            write!(
176                f,
177                "[{}] ",
178                DisplayStressIndex {
179                    stress_index,
180                    count_style: self.count_style,
181                }
182            )?;
183        }
184        write!(
185            f,
186            "{}: {}",
187            self.script_id.style(self.script_id_style),
188            self.full_command,
189        )
190    }
191}
192
193struct DisplayStressIndex {
194    stress_index: StressIndex,
195    count_style: Style,
196}
197
198impl fmt::Display for DisplayStressIndex {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self.stress_index.total {
201            Some(total) => {
202                write!(
203                    f,
204                    "{}/{}",
205                    (self.stress_index.current + 1).style(self.count_style),
206                    total.style(self.count_style)
207                )
208            }
209            None => {
210                write!(
211                    f,
212                    "{}",
213                    (self.stress_index.current + 1).style(self.count_style)
214                )
215            }
216        }
217    }
218}
219
220/// Write out a test name.
221pub(crate) fn write_test_name(
222    name: &str,
223    style: &Styles,
224    writer: &mut dyn WriteStr,
225) -> io::Result<()> {
226    // Look for the part of the test after the last ::, if any.
227    let mut splits = name.rsplitn(2, "::");
228    let trailing = splits.next().expect("test should have at least 1 element");
229    if let Some(rest) = splits.next() {
230        write!(
231            writer,
232            "{}{}",
233            rest.style(style.module_path),
234            "::".style(style.module_path)
235        )?;
236    }
237    write!(writer, "{}", trailing.style(style.test_name))?;
238
239    Ok(())
240}
241
242/// Write out a test name, `std::fmt::Write` version.
243pub(crate) fn fmt_write_test_name(
244    name: &str,
245    style: &Styles,
246    writer: &mut dyn fmt::Write,
247) -> fmt::Result {
248    // Look for the part of the test after the last ::, if any.
249    let mut splits = name.rsplitn(2, "::");
250    let trailing = splits.next().expect("test should have at least 1 element");
251    if let Some(rest) = splits.next() {
252        write!(
253            writer,
254            "{}{}",
255            rest.style(style.module_path),
256            "::".style(style.module_path)
257        )?;
258    }
259    write!(writer, "{}", trailing.style(style.test_name))?;
260
261    Ok(())
262}
263
264pub(crate) fn convert_build_platform(
265    platform: nextest_metadata::BuildPlatform,
266) -> guppy::graph::cargo::BuildPlatform {
267    match platform {
268        nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
269        nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
270    }
271}
272
273// ---
274// Functions below copied from cargo-util to avoid pulling in a bunch of dependencies
275// ---
276
277/// Returns the name of the environment variable used for searching for
278/// dynamic libraries.
279pub(crate) fn dylib_path_envvar() -> &'static str {
280    if cfg!(windows) {
281        "PATH"
282    } else if cfg!(target_os = "macos") {
283        // When loading and linking a dynamic library or bundle, dlopen
284        // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and
285        // DYLD_FALLBACK_LIBRARY_PATH.
286        // In the Mach-O format, a dynamic library has an "install path."
287        // Clients linking against the library record this path, and the
288        // dynamic linker, dyld, uses it to locate the library.
289        // dyld searches DYLD_LIBRARY_PATH *before* the install path.
290        // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot
291        // find the library in the install path.
292        // Setting DYLD_LIBRARY_PATH can easily have unintended
293        // consequences.
294        //
295        // Also, DYLD_LIBRARY_PATH appears to have significant performance
296        // penalty starting in 10.13. Cargo's testsuite ran more than twice as
297        // slow with it on CI.
298        "DYLD_FALLBACK_LIBRARY_PATH"
299    } else {
300        "LD_LIBRARY_PATH"
301    }
302}
303
304/// Returns a list of directories that are searched for dynamic libraries.
305///
306/// Note that some operating systems will have defaults if this is empty that
307/// will need to be dealt with.
308pub(crate) fn dylib_path() -> Vec<PathBuf> {
309    match std::env::var_os(dylib_path_envvar()) {
310        Some(var) => std::env::split_paths(&var).collect(),
311        None => Vec::new(),
312    }
313}
314
315/// On Windows, convert relative paths to always use forward slashes.
316#[cfg(windows)]
317pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
318    if !rel_path.is_relative() {
319        panic!("path for conversion to forward slash '{rel_path}' is not relative");
320    }
321    rel_path.as_str().replace('\\', "/").into()
322}
323
324#[cfg(not(windows))]
325pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
326    rel_path.to_path_buf()
327}
328
329/// On Windows, convert relative paths to use the main separator.
330#[cfg(windows)]
331pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
332    if !rel_path.is_relative() {
333        panic!("path for conversion to backslash '{rel_path}' is not relative");
334    }
335    rel_path.as_str().replace('/', "\\").into()
336}
337
338#[cfg(not(windows))]
339pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
340    rel_path.to_path_buf()
341}
342
343/// Join relative paths using forward slashes.
344pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
345    assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
346    assert!(path.is_relative(), "path {path} is relative",);
347    format!("{rel_path}/{path}").into()
348}
349
350#[derive(Debug)]
351pub(crate) struct FormattedDuration(pub(crate) Duration);
352
353impl fmt::Display for FormattedDuration {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        let duration = self.0.as_secs_f64();
356        if duration > 60.0 {
357            write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
358        } else {
359            write!(f, "{duration:.2}s")
360        }
361    }
362}
363
364// "exited with"/"terminated via"
365pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
366    match AbortStatus::extract(exit_status) {
367        Some(abort_status) => display_abort_status(abort_status),
368        None => match exit_status.code() {
369            Some(code) => format!("exited with exit code {code}"),
370            None => "exited with an unknown error".to_owned(),
371        },
372    }
373}
374
375/// Displays the abort status.
376pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
377    match abort_status {
378        #[cfg(unix)]
379        AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
380            Some(s) => {
381                format!("aborted with signal {sig} (SIG{s})")
382            }
383            None => {
384                format!("aborted with signal {sig}")
385            }
386        },
387        #[cfg(windows)]
388        AbortStatus::WindowsNtStatus(nt_status) => {
389            format!(
390                "aborted with code {}",
391                // TODO: pass down a style here
392                crate::helpers::display_nt_status(nt_status, Style::new())
393            )
394        }
395        #[cfg(windows)]
396        AbortStatus::JobObject => "terminated via job object".to_string(),
397    }
398}
399
400#[cfg(unix)]
401pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
402    // These signal numbers are the same on at least Linux, macOS, FreeBSD and illumos.
403    //
404    // TODO: glibc has sigabbrev_np, and POSIX-1.2024 adds sig2str which has been available on
405    // illumos for many years:
406    // https://pubs.opengroup.org/onlinepubs/9799919799/functions/sig2str.html. We should use these
407    // if available.
408    match signal {
409        1 => Some("HUP"),
410        2 => Some("INT"),
411        3 => Some("QUIT"),
412        4 => Some("ILL"),
413        5 => Some("TRAP"),
414        6 => Some("ABRT"),
415        8 => Some("FPE"),
416        9 => Some("KILL"),
417        11 => Some("SEGV"),
418        13 => Some("PIPE"),
419        14 => Some("ALRM"),
420        15 => Some("TERM"),
421        _ => None,
422    }
423}
424
425#[cfg(windows)]
426pub(crate) fn display_nt_status(
427    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
428    bold_style: Style,
429) -> String {
430    // 10 characters ("0x" + 8 hex digits) is how an NTSTATUS with the high bit
431    // set is going to be displayed anyway. This makes all possible displays
432    // uniform.
433    let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
434    // Convert the NTSTATUS to a Win32 error code.
435    let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
436
437    if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
438        // The Win32 code was not found.
439        return bolded_status;
440    }
441
442    format!(
443        "{bolded_status}: {}",
444        io::Error::from_raw_os_error(win32_code as i32)
445    )
446}
447
448#[derive(Copy, Clone, Debug)]
449pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
450
451impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
452where
453    T: fmt::Display,
454{
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        write!(f, "'{}'", self.0)
457    }
458}
459
460// From https://twitter.com/8051Enthusiast/status/1571909110009921538
461unsafe extern "C" {
462    fn __nextest_external_symbol_that_does_not_exist();
463}
464
465#[inline]
466#[expect(dead_code)]
467pub(crate) fn statically_unreachable() -> ! {
468    unsafe {
469        __nextest_external_symbol_that_does_not_exist();
470    }
471    unreachable!("linker symbol above cannot be resolved")
472}