1use crate::{
7 config::scripts::ScriptId,
8 list::{Styles, TestInstanceId},
9 reporter::events::{AbortStatus, StressIndex},
10 write_str::WriteStr,
11};
12use camino::{Utf8Path, Utf8PathBuf};
13use console::AnsiCodeIterator;
14use owo_colors::{OwoColorize, Style};
15use std::{fmt, io, path::PathBuf, process::ExitStatus, time::Duration};
16use unicode_width::UnicodeWidthChar;
17
18pub mod plural {
20 pub fn were_plural_if(plural: bool) -> &'static str {
22 if plural { "were" } else { "was" }
23 }
24
25 pub fn setup_scripts_str(count: usize) -> &'static str {
27 if count == 1 {
28 "setup script"
29 } else {
30 "setup scripts"
31 }
32 }
33
34 pub fn tests_str(count: usize) -> &'static str {
36 tests_plural_if(count != 1)
37 }
38
39 pub fn tests_plural_if(plural: bool) -> &'static str {
41 if plural { "tests" } else { "test" }
42 }
43
44 pub fn binaries_str(count: usize) -> &'static str {
46 if count == 1 { "binary" } else { "binaries" }
47 }
48
49 pub fn paths_str(count: usize) -> &'static str {
51 if count == 1 { "path" } else { "paths" }
52 }
53
54 pub fn files_str(count: usize) -> &'static str {
56 if count == 1 { "file" } else { "files" }
57 }
58
59 pub fn directories_str(count: usize) -> &'static str {
61 if count == 1 {
62 "directory"
63 } else {
64 "directories"
65 }
66 }
67
68 pub fn this_crate_str(count: usize) -> &'static str {
70 if count == 1 {
71 "this crate"
72 } else {
73 "these crates"
74 }
75 }
76
77 pub fn libraries_str(count: usize) -> &'static str {
79 if count == 1 { "library" } else { "libraries" }
80 }
81
82 pub fn filters_str(count: usize) -> &'static str {
84 if count == 1 { "filter" } else { "filters" }
85 }
86
87 pub fn sections_str(count: usize) -> &'static str {
89 if count == 1 { "section" } else { "sections" }
90 }
91
92 pub fn iterations_str(count: u32) -> &'static str {
94 if count == 1 {
95 "iteration"
96 } else {
97 "iterations"
98 }
99 }
100}
101
102pub struct DisplayTestInstance<'a> {
104 stress_index: Option<StressIndex>,
105 display_counter_index: Option<DisplayCounterIndex>,
106 instance: TestInstanceId<'a>,
107 styles: &'a Styles,
108 max_width: Option<usize>,
109}
110
111impl<'a> DisplayTestInstance<'a> {
112 pub fn new(
114 stress_index: Option<StressIndex>,
115 display_counter_index: Option<DisplayCounterIndex>,
116 instance: TestInstanceId<'a>,
117 styles: &'a Styles,
118 ) -> Self {
119 Self {
120 stress_index,
121 display_counter_index,
122 instance,
123 styles,
124 max_width: None,
125 }
126 }
127
128 pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
129 self.max_width = Some(max_width);
130 self
131 }
132}
133
134impl fmt::Display for DisplayTestInstance<'_> {
135 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136 let stress_index_str = if let Some(stress_index) = self.stress_index {
138 format!(
139 "[{}] ",
140 DisplayStressIndex {
141 stress_index,
142 count_style: self.styles.count,
143 }
144 )
145 } else {
146 String::new()
147 };
148 let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
149 format!("{display_counter_index} ")
150 } else {
151 String::new()
152 };
153 let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
154 let test_name_str = format!(
155 "{}",
156 DisplayTestName::new(self.instance.test_name, self.styles)
157 );
158
159 if let Some(max_width) = self.max_width {
161 let stress_index_width = text_width(&stress_index_str);
165 let counter_index_width = text_width(&counter_index_str);
166 let binary_id_width = text_width(&binary_id_str);
167 let test_name_width = text_width(&test_name_str);
168
169 let mut stress_index_resolved_width = stress_index_width;
176 let mut counter_index_resolved_width = counter_index_width;
177 let mut binary_id_resolved_width = binary_id_width;
178 let mut test_name_resolved_width = test_name_width;
179
180 if stress_index_resolved_width > max_width {
182 stress_index_resolved_width = max_width;
183 }
184
185 let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
187 if counter_index_resolved_width > remaining_width {
188 counter_index_resolved_width = remaining_width;
189 }
190
191 let remaining_width = max_width
193 .saturating_sub(stress_index_resolved_width)
194 .saturating_sub(counter_index_resolved_width);
195 if binary_id_resolved_width > remaining_width {
196 binary_id_resolved_width = remaining_width;
197 }
198
199 let remaining_width = max_width
201 .saturating_sub(stress_index_resolved_width)
202 .saturating_sub(counter_index_resolved_width)
203 .saturating_sub(binary_id_resolved_width);
204 if test_name_resolved_width > remaining_width {
205 test_name_resolved_width = remaining_width;
206 }
207
208 let test_name_truncated_str = if test_name_resolved_width == test_name_width {
210 test_name_str
211 } else {
212 truncate_ansi_aware(
214 &test_name_str,
215 test_name_width.saturating_sub(test_name_resolved_width),
216 test_name_width,
217 )
218 };
219 let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
220 binary_id_str
221 } else {
222 truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
224 };
225 let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
226 {
227 counter_index_str
228 } else {
229 truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
231 };
232 let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
233 stress_index_str
234 } else {
235 truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
237 };
238
239 write!(
240 f,
241 "{}{}{}{}",
242 stress_index_truncated_str,
243 counter_index_truncated_str,
244 binary_id_truncated_str,
245 test_name_truncated_str,
246 )
247 } else {
248 write!(
249 f,
250 "{}{}{}{}",
251 stress_index_str, counter_index_str, binary_id_str, test_name_str
252 )
253 }
254 }
255}
256
257fn text_width(text: &str) -> usize {
258 strip_ansi_escapes::strip_str(text)
266 .chars()
267 .map(|c| c.width().unwrap_or(0))
268 .sum()
269}
270
271fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
272 let mut pos = 0;
273 let mut res = String::new();
274 for (s, is_ansi) in AnsiCodeIterator::new(text) {
275 if is_ansi {
276 res.push_str(s);
277 continue;
278 } else if pos >= end {
279 continue;
282 }
283
284 for c in s.chars() {
285 let c_width = c.width().unwrap_or(0);
286 if start <= pos && pos + c_width <= end {
287 res.push(c);
288 }
289 pos += c_width;
290 if pos > end {
291 break;
293 }
294 }
295 }
296
297 res
298}
299
300pub(crate) struct DisplayScriptInstance {
301 stress_index: Option<StressIndex>,
302 script_id: ScriptId,
303 full_command: String,
304 script_id_style: Style,
305 count_style: Style,
306}
307
308impl DisplayScriptInstance {
309 pub(crate) fn new(
310 stress_index: Option<StressIndex>,
311 script_id: ScriptId,
312 command: &str,
313 args: &[String],
314 script_id_style: Style,
315 count_style: Style,
316 ) -> Self {
317 let full_command =
318 shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
319
320 Self {
321 stress_index,
322 script_id,
323 full_command,
324 script_id_style,
325 count_style,
326 }
327 }
328}
329
330impl fmt::Display for DisplayScriptInstance {
331 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
332 if let Some(stress_index) = self.stress_index {
333 write!(
334 f,
335 "[{}] ",
336 DisplayStressIndex {
337 stress_index,
338 count_style: self.count_style,
339 }
340 )?;
341 }
342 write!(
343 f,
344 "{}: {}",
345 self.script_id.style(self.script_id_style),
346 self.full_command,
347 )
348 }
349}
350
351struct DisplayStressIndex {
352 stress_index: StressIndex,
353 count_style: Style,
354}
355
356impl fmt::Display for DisplayStressIndex {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 match self.stress_index.total {
359 Some(total) => {
360 write!(
361 f,
362 "{:>width$}/{}",
363 (self.stress_index.current + 1).style(self.count_style),
364 total.style(self.count_style),
365 width = u32_decimal_char_width(total.get()),
366 )
367 }
368 None => {
369 write!(
370 f,
371 "{}",
372 (self.stress_index.current + 1).style(self.count_style)
373 )
374 }
375 }
376 }
377}
378
379pub enum DisplayCounterIndex {
381 Counter {
383 current: usize,
385 total: usize,
387 },
388 Padded {
390 character: char,
392 width: usize,
394 },
395}
396
397impl DisplayCounterIndex {
398 pub fn new_counter(current: usize, total: usize) -> Self {
400 Self::Counter { current, total }
401 }
402
403 pub fn new_padded(character: char, width: usize) -> Self {
405 Self::Padded { character, width }
406 }
407}
408
409impl fmt::Display for DisplayCounterIndex {
410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411 match self {
412 Self::Counter { current, total } => {
413 write!(
414 f,
415 "({:>width$}/{})",
416 current,
417 total,
418 width = usize_decimal_char_width(*total)
419 )
420 }
421 Self::Padded { character, width } => {
422 let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
427 write!(f, "({s})")
428 }
429 }
430 }
431}
432
433pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
434 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
438}
439
440pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
441 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
445}
446
447pub(crate) fn write_test_name(
449 name: &str,
450 style: &Styles,
451 writer: &mut dyn WriteStr,
452) -> io::Result<()> {
453 let mut splits = name.rsplitn(2, "::");
455 let trailing = splits.next().expect("test should have at least 1 element");
456 if let Some(rest) = splits.next() {
457 write!(
458 writer,
459 "{}{}",
460 rest.style(style.module_path),
461 "::".style(style.module_path)
462 )?;
463 }
464 write!(writer, "{}", trailing.style(style.test_name))?;
465
466 Ok(())
467}
468
469pub(crate) struct DisplayTestName<'a> {
471 name: &'a str,
472 styles: &'a Styles,
473}
474
475impl<'a> DisplayTestName<'a> {
476 pub(crate) fn new(name: &'a str, styles: &'a Styles) -> Self {
477 Self { name, styles }
478 }
479}
480
481impl fmt::Display for DisplayTestName<'_> {
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 let mut splits = self.name.rsplitn(2, "::");
485 let trailing = splits.next().expect("test should have at least 1 element");
486 if let Some(rest) = splits.next() {
487 write!(
488 f,
489 "{}{}",
490 rest.style(self.styles.module_path),
491 "::".style(self.styles.module_path)
492 )?;
493 }
494 write!(f, "{}", trailing.style(self.styles.test_name))?;
495
496 Ok(())
497 }
498}
499
500pub(crate) fn convert_build_platform(
501 platform: nextest_metadata::BuildPlatform,
502) -> guppy::graph::cargo::BuildPlatform {
503 match platform {
504 nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
505 nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
506 }
507}
508
509pub(crate) fn dylib_path_envvar() -> &'static str {
516 if cfg!(windows) {
517 "PATH"
518 } else if cfg!(target_os = "macos") {
519 "DYLD_FALLBACK_LIBRARY_PATH"
535 } else {
536 "LD_LIBRARY_PATH"
537 }
538}
539
540pub(crate) fn dylib_path() -> Vec<PathBuf> {
545 match std::env::var_os(dylib_path_envvar()) {
546 Some(var) => std::env::split_paths(&var).collect(),
547 None => Vec::new(),
548 }
549}
550
551#[cfg(windows)]
553pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
554 if !rel_path.is_relative() {
555 panic!("path for conversion to forward slash '{rel_path}' is not relative");
556 }
557 rel_path.as_str().replace('\\', "/").into()
558}
559
560#[cfg(not(windows))]
561pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
562 rel_path.to_path_buf()
563}
564
565#[cfg(windows)]
567pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
568 if !rel_path.is_relative() {
569 panic!("path for conversion to backslash '{rel_path}' is not relative");
570 }
571 rel_path.as_str().replace('/', "\\").into()
572}
573
574#[cfg(not(windows))]
575pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
576 rel_path.to_path_buf()
577}
578
579pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
581 assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
582 assert!(path.is_relative(), "path {path} is relative",);
583 format!("{rel_path}/{path}").into()
584}
585
586#[derive(Debug)]
587pub(crate) struct FormattedDuration(pub(crate) Duration);
588
589impl fmt::Display for FormattedDuration {
590 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591 let duration = self.0.as_secs_f64();
592 if duration > 60.0 {
593 write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
594 } else {
595 write!(f, "{duration:.2}s")
596 }
597 }
598}
599
600pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
602 match AbortStatus::extract(exit_status) {
603 Some(abort_status) => display_abort_status(abort_status),
604 None => match exit_status.code() {
605 Some(code) => format!("exited with exit code {code}"),
606 None => "exited with an unknown error".to_owned(),
607 },
608 }
609}
610
611pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
613 match abort_status {
614 #[cfg(unix)]
615 AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
616 Some(s) => {
617 format!("aborted with signal {sig} (SIG{s})")
618 }
619 None => {
620 format!("aborted with signal {sig}")
621 }
622 },
623 #[cfg(windows)]
624 AbortStatus::WindowsNtStatus(nt_status) => {
625 format!(
626 "aborted with code {}",
627 crate::helpers::display_nt_status(nt_status, Style::new())
629 )
630 }
631 #[cfg(windows)]
632 AbortStatus::JobObject => "terminated via job object".to_string(),
633 }
634}
635
636#[cfg(unix)]
637pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
638 match signal {
645 1 => Some("HUP"),
646 2 => Some("INT"),
647 3 => Some("QUIT"),
648 4 => Some("ILL"),
649 5 => Some("TRAP"),
650 6 => Some("ABRT"),
651 8 => Some("FPE"),
652 9 => Some("KILL"),
653 11 => Some("SEGV"),
654 13 => Some("PIPE"),
655 14 => Some("ALRM"),
656 15 => Some("TERM"),
657 _ => None,
658 }
659}
660
661#[cfg(windows)]
662pub(crate) fn display_nt_status(
663 nt_status: windows_sys::Win32::Foundation::NTSTATUS,
664 bold_style: Style,
665) -> String {
666 let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
670 let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
672
673 if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
674 return bolded_status;
676 }
677
678 format!(
679 "{bolded_status}: {}",
680 io::Error::from_raw_os_error(win32_code as i32)
681 )
682}
683
684#[derive(Copy, Clone, Debug)]
685pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
686
687impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
688where
689 T: fmt::Display,
690{
691 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692 write!(f, "'{}'", self.0)
693 }
694}
695
696unsafe extern "C" {
698 fn __nextest_external_symbol_that_does_not_exist();
699}
700
701#[inline]
702#[expect(dead_code)]
703pub(crate) fn statically_unreachable() -> ! {
704 unsafe {
705 __nextest_external_symbol_that_does_not_exist();
706 }
707 unreachable!("linker symbol above cannot be resolved")
708}
709
710#[cfg(test)]
711mod test {
712 use super::*;
713
714 #[test]
715 fn test_decimal_char_width() {
716 assert_eq!(1, usize_decimal_char_width(0));
717 assert_eq!(1, usize_decimal_char_width(1));
718 assert_eq!(1, usize_decimal_char_width(5));
719 assert_eq!(1, usize_decimal_char_width(9));
720 assert_eq!(2, usize_decimal_char_width(10));
721 assert_eq!(2, usize_decimal_char_width(11));
722 assert_eq!(2, usize_decimal_char_width(99));
723 assert_eq!(3, usize_decimal_char_width(100));
724 assert_eq!(3, usize_decimal_char_width(999));
725 }
726}