1use crate::{
7    config::scripts::ScriptId,
8    list::{Styles, TestInstanceId},
9    reporter::events::{AbortStatus, RunStats, 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
16pub mod plural {
18    pub fn were_plural_if(plural: bool) -> &'static str {
20        if plural { "were" } else { "was" }
21    }
22
23    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    pub fn tests_str(count: usize) -> &'static str {
34        tests_plural_if(count != 1)
35    }
36
37    pub fn tests_plural_if(plural: bool) -> &'static str {
39        if plural { "tests" } else { "test" }
40    }
41
42    pub fn binaries_str(count: usize) -> &'static str {
44        if count == 1 { "binary" } else { "binaries" }
45    }
46
47    pub fn paths_str(count: usize) -> &'static str {
49        if count == 1 { "path" } else { "paths" }
50    }
51
52    pub fn files_str(count: usize) -> &'static str {
54        if count == 1 { "file" } else { "files" }
55    }
56
57    pub fn directories_str(count: usize) -> &'static str {
59        if count == 1 {
60            "directory"
61        } else {
62            "directories"
63        }
64    }
65
66    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    pub fn libraries_str(count: usize) -> &'static str {
77        if count == 1 { "library" } else { "libraries" }
78    }
79
80    pub fn filters_str(count: usize) -> &'static str {
82        if count == 1 { "filter" } else { "filters" }
83    }
84
85    pub fn sections_str(count: usize) -> &'static str {
87        if count == 1 { "section" } else { "sections" }
88    }
89
90    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    display_counter_index: Option<DisplayCounterIndex>,
103    instance: TestInstanceId<'a>,
104    styles: &'a Styles,
105}
106
107impl<'a> DisplayTestInstance<'a> {
108    pub(crate) fn new(
109        stress_index: Option<StressIndex>,
110        display_counter_index: Option<DisplayCounterIndex>,
111        instance: TestInstanceId<'a>,
112        styles: &'a Styles,
113    ) -> Self {
114        Self {
115            stress_index,
116            display_counter_index,
117            instance,
118            styles,
119        }
120    }
121}
122
123impl fmt::Display for DisplayTestInstance<'_> {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        if let Some(stress_index) = self.stress_index {
126            write!(
127                f,
128                "[{}] ",
129                DisplayStressIndex {
130                    stress_index,
131                    count_style: self.styles.count,
132                }
133            )?;
134        }
135
136        if let Some(display_counter_index) = &self.display_counter_index {
137            write!(f, "{display_counter_index} ")?
138        }
139
140        write!(
141            f,
142            "{} ",
143            self.instance.binary_id.style(self.styles.binary_id),
144        )?;
145        fmt_write_test_name(self.instance.test_name, self.styles, f)
146    }
147}
148
149pub(crate) struct DisplayScriptInstance {
150    stress_index: Option<StressIndex>,
151    script_id: ScriptId,
152    full_command: String,
153    script_id_style: Style,
154    count_style: Style,
155}
156
157impl DisplayScriptInstance {
158    pub(crate) fn new(
159        stress_index: Option<StressIndex>,
160        script_id: ScriptId,
161        command: &str,
162        args: &[String],
163        script_id_style: Style,
164        count_style: Style,
165    ) -> Self {
166        let full_command =
167            shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
168
169        Self {
170            stress_index,
171            script_id,
172            full_command,
173            script_id_style,
174            count_style,
175        }
176    }
177}
178
179impl fmt::Display for DisplayScriptInstance {
180    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
181        if let Some(stress_index) = self.stress_index {
182            write!(
183                f,
184                "[{}] ",
185                DisplayStressIndex {
186                    stress_index,
187                    count_style: self.count_style,
188                }
189            )?;
190        }
191        write!(
192            f,
193            "{}: {}",
194            self.script_id.style(self.script_id_style),
195            self.full_command,
196        )
197    }
198}
199
200struct DisplayStressIndex {
201    stress_index: StressIndex,
202    count_style: Style,
203}
204
205impl fmt::Display for DisplayStressIndex {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        match self.stress_index.total {
208            Some(total) => {
209                write!(
210                    f,
211                    "{:>width$}/{}",
212                    (self.stress_index.current + 1).style(self.count_style),
213                    total.style(self.count_style),
214                    width = u32_decimal_char_width(total.get()),
215                )
216            }
217            None => {
218                write!(
219                    f,
220                    "{}",
221                    (self.stress_index.current + 1).style(self.count_style)
222                )
223            }
224        }
225    }
226}
227
228pub(super) struct DisplayCounterIndex(usize, usize);
229
230impl DisplayCounterIndex {
231    pub fn new(current_stats: &RunStats) -> Self {
232        Self(
233            current_stats.finished_count,
234            current_stats.initial_run_count,
235        )
236    }
237
238    pub fn width(&self) -> usize {
239        usize_decimal_char_width(self.1) * 2 + 3
240    }
241}
242
243impl fmt::Display for DisplayCounterIndex {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(
246            f,
247            "({:>width$}/{})",
248            self.0,
249            self.1,
250            width = usize_decimal_char_width(self.1)
251        )
252    }
253}
254
255pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
256    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
260}
261
262pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
263    (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
267}
268
269pub(crate) fn write_test_name(
271    name: &str,
272    style: &Styles,
273    writer: &mut dyn WriteStr,
274) -> io::Result<()> {
275    let mut splits = name.rsplitn(2, "::");
277    let trailing = splits.next().expect("test should have at least 1 element");
278    if let Some(rest) = splits.next() {
279        write!(
280            writer,
281            "{}{}",
282            rest.style(style.module_path),
283            "::".style(style.module_path)
284        )?;
285    }
286    write!(writer, "{}", trailing.style(style.test_name))?;
287
288    Ok(())
289}
290
291pub(crate) fn fmt_write_test_name(
293    name: &str,
294    style: &Styles,
295    writer: &mut dyn fmt::Write,
296) -> fmt::Result {
297    let mut splits = name.rsplitn(2, "::");
299    let trailing = splits.next().expect("test should have at least 1 element");
300    if let Some(rest) = splits.next() {
301        write!(
302            writer,
303            "{}{}",
304            rest.style(style.module_path),
305            "::".style(style.module_path)
306        )?;
307    }
308    write!(writer, "{}", trailing.style(style.test_name))?;
309
310    Ok(())
311}
312
313pub(crate) fn convert_build_platform(
314    platform: nextest_metadata::BuildPlatform,
315) -> guppy::graph::cargo::BuildPlatform {
316    match platform {
317        nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
318        nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
319    }
320}
321
322pub(crate) fn dylib_path_envvar() -> &'static str {
329    if cfg!(windows) {
330        "PATH"
331    } else if cfg!(target_os = "macos") {
332        "DYLD_FALLBACK_LIBRARY_PATH"
348    } else {
349        "LD_LIBRARY_PATH"
350    }
351}
352
353pub(crate) fn dylib_path() -> Vec<PathBuf> {
358    match std::env::var_os(dylib_path_envvar()) {
359        Some(var) => std::env::split_paths(&var).collect(),
360        None => Vec::new(),
361    }
362}
363
364#[cfg(windows)]
366pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
367    if !rel_path.is_relative() {
368        panic!("path for conversion to forward slash '{rel_path}' is not relative");
369    }
370    rel_path.as_str().replace('\\', "/").into()
371}
372
373#[cfg(not(windows))]
374pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
375    rel_path.to_path_buf()
376}
377
378#[cfg(windows)]
380pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
381    if !rel_path.is_relative() {
382        panic!("path for conversion to backslash '{rel_path}' is not relative");
383    }
384    rel_path.as_str().replace('/', "\\").into()
385}
386
387#[cfg(not(windows))]
388pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
389    rel_path.to_path_buf()
390}
391
392pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
394    assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
395    assert!(path.is_relative(), "path {path} is relative",);
396    format!("{rel_path}/{path}").into()
397}
398
399#[derive(Debug)]
400pub(crate) struct FormattedDuration(pub(crate) Duration);
401
402impl fmt::Display for FormattedDuration {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        let duration = self.0.as_secs_f64();
405        if duration > 60.0 {
406            write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
407        } else {
408            write!(f, "{duration:.2}s")
409        }
410    }
411}
412
413pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
415    match AbortStatus::extract(exit_status) {
416        Some(abort_status) => display_abort_status(abort_status),
417        None => match exit_status.code() {
418            Some(code) => format!("exited with exit code {code}"),
419            None => "exited with an unknown error".to_owned(),
420        },
421    }
422}
423
424pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
426    match abort_status {
427        #[cfg(unix)]
428        AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
429            Some(s) => {
430                format!("aborted with signal {sig} (SIG{s})")
431            }
432            None => {
433                format!("aborted with signal {sig}")
434            }
435        },
436        #[cfg(windows)]
437        AbortStatus::WindowsNtStatus(nt_status) => {
438            format!(
439                "aborted with code {}",
440                crate::helpers::display_nt_status(nt_status, Style::new())
442            )
443        }
444        #[cfg(windows)]
445        AbortStatus::JobObject => "terminated via job object".to_string(),
446    }
447}
448
449#[cfg(unix)]
450pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
451    match signal {
458        1 => Some("HUP"),
459        2 => Some("INT"),
460        3 => Some("QUIT"),
461        4 => Some("ILL"),
462        5 => Some("TRAP"),
463        6 => Some("ABRT"),
464        8 => Some("FPE"),
465        9 => Some("KILL"),
466        11 => Some("SEGV"),
467        13 => Some("PIPE"),
468        14 => Some("ALRM"),
469        15 => Some("TERM"),
470        _ => None,
471    }
472}
473
474#[cfg(windows)]
475pub(crate) fn display_nt_status(
476    nt_status: windows_sys::Win32::Foundation::NTSTATUS,
477    bold_style: Style,
478) -> String {
479    let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
483    let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
485
486    if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
487        return bolded_status;
489    }
490
491    format!(
492        "{bolded_status}: {}",
493        io::Error::from_raw_os_error(win32_code as i32)
494    )
495}
496
497#[derive(Copy, Clone, Debug)]
498pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
499
500impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
501where
502    T: fmt::Display,
503{
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        write!(f, "'{}'", self.0)
506    }
507}
508
509unsafe extern "C" {
511    fn __nextest_external_symbol_that_does_not_exist();
512}
513
514#[inline]
515#[expect(dead_code)]
516pub(crate) fn statically_unreachable() -> ! {
517    unsafe {
518        __nextest_external_symbol_that_does_not_exist();
519    }
520    unreachable!("linker symbol above cannot be resolved")
521}
522
523#[cfg(test)]
524mod test {
525    use super::*;
526
527    #[test]
528    fn test_decimal_char_width() {
529        assert_eq!(1, usize_decimal_char_width(0));
530        assert_eq!(1, usize_decimal_char_width(1));
531        assert_eq!(1, usize_decimal_char_width(5));
532        assert_eq!(1, usize_decimal_char_width(9));
533        assert_eq!(2, usize_decimal_char_width(10));
534        assert_eq!(2, usize_decimal_char_width(11));
535        assert_eq!(2, usize_decimal_char_width(99));
536        assert_eq!(3, usize_decimal_char_width(100));
537        assert_eq!(3, usize_decimal_char_width(999));
538    }
539}