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(crate) struct DisplayTestInstance<'a> {
103 stress_index: Option<StressIndex>,
104 display_counter_index: Option<DisplayCounterIndex>,
105 instance: TestInstanceId<'a>,
106 styles: &'a Styles,
107 max_width: Option<usize>,
108}
109
110impl<'a> DisplayTestInstance<'a> {
111 pub(crate) fn new(
112 stress_index: Option<StressIndex>,
113 display_counter_index: Option<DisplayCounterIndex>,
114 instance: TestInstanceId<'a>,
115 styles: &'a Styles,
116 ) -> Self {
117 Self {
118 stress_index,
119 display_counter_index,
120 instance,
121 styles,
122 max_width: None,
123 }
124 }
125
126 pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
127 self.max_width = Some(max_width);
128 self
129 }
130}
131
132impl fmt::Display for DisplayTestInstance<'_> {
133 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
134 let stress_index_str = if let Some(stress_index) = self.stress_index {
136 format!(
137 "[{}] ",
138 DisplayStressIndex {
139 stress_index,
140 count_style: self.styles.count,
141 }
142 )
143 } else {
144 String::new()
145 };
146 let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
147 format!("{display_counter_index} ")
148 } else {
149 String::new()
150 };
151 let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
152 let test_name_str = format!(
153 "{}",
154 DisplayTestName::new(self.instance.test_name, self.styles)
155 );
156
157 if let Some(max_width) = self.max_width {
159 let stress_index_width = text_width(&stress_index_str);
163 let counter_index_width = text_width(&counter_index_str);
164 let binary_id_width = text_width(&binary_id_str);
165 let test_name_width = text_width(&test_name_str);
166
167 let mut stress_index_resolved_width = stress_index_width;
174 let mut counter_index_resolved_width = counter_index_width;
175 let mut binary_id_resolved_width = binary_id_width;
176 let mut test_name_resolved_width = test_name_width;
177
178 if stress_index_resolved_width > max_width {
180 stress_index_resolved_width = max_width;
181 }
182
183 let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
185 if counter_index_resolved_width > remaining_width {
186 counter_index_resolved_width = remaining_width;
187 }
188
189 let remaining_width = max_width
191 .saturating_sub(stress_index_resolved_width)
192 .saturating_sub(counter_index_resolved_width);
193 if binary_id_resolved_width > remaining_width {
194 binary_id_resolved_width = remaining_width;
195 }
196
197 let remaining_width = max_width
199 .saturating_sub(stress_index_resolved_width)
200 .saturating_sub(counter_index_resolved_width)
201 .saturating_sub(binary_id_resolved_width);
202 if test_name_resolved_width > remaining_width {
203 test_name_resolved_width = remaining_width;
204 }
205
206 let test_name_truncated_str = if test_name_resolved_width == test_name_width {
208 test_name_str
209 } else {
210 truncate_ansi_aware(
212 &test_name_str,
213 test_name_width.saturating_sub(test_name_resolved_width),
214 test_name_width,
215 )
216 };
217 let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
218 binary_id_str
219 } else {
220 truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
222 };
223 let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
224 {
225 counter_index_str
226 } else {
227 truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
229 };
230 let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
231 stress_index_str
232 } else {
233 truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
235 };
236
237 write!(
238 f,
239 "{}{}{}{}",
240 stress_index_truncated_str,
241 counter_index_truncated_str,
242 binary_id_truncated_str,
243 test_name_truncated_str,
244 )
245 } else {
246 write!(
247 f,
248 "{}{}{}{}",
249 stress_index_str, counter_index_str, binary_id_str, test_name_str
250 )
251 }
252 }
253}
254
255fn text_width(text: &str) -> usize {
256 strip_ansi_escapes::strip_str(text)
264 .chars()
265 .map(|c| c.width().unwrap_or(0))
266 .sum()
267}
268
269fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
270 let mut pos = 0;
271 let mut res = String::new();
272 for (s, is_ansi) in AnsiCodeIterator::new(text) {
273 if is_ansi {
274 res.push_str(s);
275 continue;
276 } else if pos >= end {
277 continue;
280 }
281
282 for c in s.chars() {
283 let c_width = c.width().unwrap_or(0);
284 if start <= pos && pos + c_width <= end {
285 res.push(c);
286 }
287 pos += c_width;
288 if pos > end {
289 break;
291 }
292 }
293 }
294
295 res
296}
297
298pub(crate) struct DisplayScriptInstance {
299 stress_index: Option<StressIndex>,
300 script_id: ScriptId,
301 full_command: String,
302 script_id_style: Style,
303 count_style: Style,
304}
305
306impl DisplayScriptInstance {
307 pub(crate) fn new(
308 stress_index: Option<StressIndex>,
309 script_id: ScriptId,
310 command: &str,
311 args: &[String],
312 script_id_style: Style,
313 count_style: Style,
314 ) -> Self {
315 let full_command =
316 shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
317
318 Self {
319 stress_index,
320 script_id,
321 full_command,
322 script_id_style,
323 count_style,
324 }
325 }
326}
327
328impl fmt::Display for DisplayScriptInstance {
329 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
330 if let Some(stress_index) = self.stress_index {
331 write!(
332 f,
333 "[{}] ",
334 DisplayStressIndex {
335 stress_index,
336 count_style: self.count_style,
337 }
338 )?;
339 }
340 write!(
341 f,
342 "{}: {}",
343 self.script_id.style(self.script_id_style),
344 self.full_command,
345 )
346 }
347}
348
349struct DisplayStressIndex {
350 stress_index: StressIndex,
351 count_style: Style,
352}
353
354impl fmt::Display for DisplayStressIndex {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 match self.stress_index.total {
357 Some(total) => {
358 write!(
359 f,
360 "{:>width$}/{}",
361 (self.stress_index.current + 1).style(self.count_style),
362 total.style(self.count_style),
363 width = u32_decimal_char_width(total.get()),
364 )
365 }
366 None => {
367 write!(
368 f,
369 "{}",
370 (self.stress_index.current + 1).style(self.count_style)
371 )
372 }
373 }
374 }
375}
376
377pub(super) enum DisplayCounterIndex {
378 Counter { current: usize, total: usize },
379 Padded { character: char, width: usize },
380}
381
382impl DisplayCounterIndex {
383 pub fn new_counter(current: usize, total: usize) -> Self {
384 Self::Counter { current, total }
385 }
386
387 pub fn new_padded(character: char, width: usize) -> Self {
388 Self::Padded { character, width }
389 }
390}
391
392impl fmt::Display for DisplayCounterIndex {
393 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 match self {
395 Self::Counter { current, total } => {
396 write!(
397 f,
398 "({:>width$}/{})",
399 current,
400 total,
401 width = usize_decimal_char_width(*total)
402 )
403 }
404 Self::Padded { character, width } => {
405 let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
410 write!(f, "({s})")
411 }
412 }
413 }
414}
415
416pub(crate) fn usize_decimal_char_width(n: usize) -> usize {
417 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
421}
422
423pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
424 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
428}
429
430pub(crate) fn write_test_name(
432 name: &str,
433 style: &Styles,
434 writer: &mut dyn WriteStr,
435) -> io::Result<()> {
436 let mut splits = name.rsplitn(2, "::");
438 let trailing = splits.next().expect("test should have at least 1 element");
439 if let Some(rest) = splits.next() {
440 write!(
441 writer,
442 "{}{}",
443 rest.style(style.module_path),
444 "::".style(style.module_path)
445 )?;
446 }
447 write!(writer, "{}", trailing.style(style.test_name))?;
448
449 Ok(())
450}
451
452pub(crate) struct DisplayTestName<'a> {
454 name: &'a str,
455 styles: &'a Styles,
456}
457
458impl<'a> DisplayTestName<'a> {
459 pub(crate) fn new(name: &'a str, styles: &'a Styles) -> Self {
460 Self { name, styles }
461 }
462}
463
464impl fmt::Display for DisplayTestName<'_> {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 let mut splits = self.name.rsplitn(2, "::");
468 let trailing = splits.next().expect("test should have at least 1 element");
469 if let Some(rest) = splits.next() {
470 write!(
471 f,
472 "{}{}",
473 rest.style(self.styles.module_path),
474 "::".style(self.styles.module_path)
475 )?;
476 }
477 write!(f, "{}", trailing.style(self.styles.test_name))?;
478
479 Ok(())
480 }
481}
482
483pub(crate) fn convert_build_platform(
484 platform: nextest_metadata::BuildPlatform,
485) -> guppy::graph::cargo::BuildPlatform {
486 match platform {
487 nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
488 nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
489 }
490}
491
492pub(crate) fn dylib_path_envvar() -> &'static str {
499 if cfg!(windows) {
500 "PATH"
501 } else if cfg!(target_os = "macos") {
502 "DYLD_FALLBACK_LIBRARY_PATH"
518 } else {
519 "LD_LIBRARY_PATH"
520 }
521}
522
523pub(crate) fn dylib_path() -> Vec<PathBuf> {
528 match std::env::var_os(dylib_path_envvar()) {
529 Some(var) => std::env::split_paths(&var).collect(),
530 None => Vec::new(),
531 }
532}
533
534#[cfg(windows)]
536pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
537 if !rel_path.is_relative() {
538 panic!("path for conversion to forward slash '{rel_path}' is not relative");
539 }
540 rel_path.as_str().replace('\\', "/").into()
541}
542
543#[cfg(not(windows))]
544pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
545 rel_path.to_path_buf()
546}
547
548#[cfg(windows)]
550pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
551 if !rel_path.is_relative() {
552 panic!("path for conversion to backslash '{rel_path}' is not relative");
553 }
554 rel_path.as_str().replace('/', "\\").into()
555}
556
557#[cfg(not(windows))]
558pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
559 rel_path.to_path_buf()
560}
561
562pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
564 assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
565 assert!(path.is_relative(), "path {path} is relative",);
566 format!("{rel_path}/{path}").into()
567}
568
569#[derive(Debug)]
570pub(crate) struct FormattedDuration(pub(crate) Duration);
571
572impl fmt::Display for FormattedDuration {
573 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574 let duration = self.0.as_secs_f64();
575 if duration > 60.0 {
576 write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
577 } else {
578 write!(f, "{duration:.2}s")
579 }
580 }
581}
582
583pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
585 match AbortStatus::extract(exit_status) {
586 Some(abort_status) => display_abort_status(abort_status),
587 None => match exit_status.code() {
588 Some(code) => format!("exited with exit code {code}"),
589 None => "exited with an unknown error".to_owned(),
590 },
591 }
592}
593
594pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
596 match abort_status {
597 #[cfg(unix)]
598 AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
599 Some(s) => {
600 format!("aborted with signal {sig} (SIG{s})")
601 }
602 None => {
603 format!("aborted with signal {sig}")
604 }
605 },
606 #[cfg(windows)]
607 AbortStatus::WindowsNtStatus(nt_status) => {
608 format!(
609 "aborted with code {}",
610 crate::helpers::display_nt_status(nt_status, Style::new())
612 )
613 }
614 #[cfg(windows)]
615 AbortStatus::JobObject => "terminated via job object".to_string(),
616 }
617}
618
619#[cfg(unix)]
620pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
621 match signal {
628 1 => Some("HUP"),
629 2 => Some("INT"),
630 3 => Some("QUIT"),
631 4 => Some("ILL"),
632 5 => Some("TRAP"),
633 6 => Some("ABRT"),
634 8 => Some("FPE"),
635 9 => Some("KILL"),
636 11 => Some("SEGV"),
637 13 => Some("PIPE"),
638 14 => Some("ALRM"),
639 15 => Some("TERM"),
640 _ => None,
641 }
642}
643
644#[cfg(windows)]
645pub(crate) fn display_nt_status(
646 nt_status: windows_sys::Win32::Foundation::NTSTATUS,
647 bold_style: Style,
648) -> String {
649 let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
653 let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
655
656 if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
657 return bolded_status;
659 }
660
661 format!(
662 "{bolded_status}: {}",
663 io::Error::from_raw_os_error(win32_code as i32)
664 )
665}
666
667#[derive(Copy, Clone, Debug)]
668pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
669
670impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
671where
672 T: fmt::Display,
673{
674 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675 write!(f, "'{}'", self.0)
676 }
677}
678
679unsafe extern "C" {
681 fn __nextest_external_symbol_that_does_not_exist();
682}
683
684#[inline]
685#[expect(dead_code)]
686pub(crate) fn statically_unreachable() -> ! {
687 unsafe {
688 __nextest_external_symbol_that_does_not_exist();
689 }
690 unreachable!("linker symbol above cannot be resolved")
691}
692
693#[cfg(test)]
694mod test {
695 use super::*;
696
697 #[test]
698 fn test_decimal_char_width() {
699 assert_eq!(1, usize_decimal_char_width(0));
700 assert_eq!(1, usize_decimal_char_width(1));
701 assert_eq!(1, usize_decimal_char_width(5));
702 assert_eq!(1, usize_decimal_char_width(9));
703 assert_eq!(2, usize_decimal_char_width(10));
704 assert_eq!(2, usize_decimal_char_width(11));
705 assert_eq!(2, usize_decimal_char_width(99));
706 assert_eq!(3, usize_decimal_char_width(100));
707 assert_eq!(3, usize_decimal_char_width(999));
708 }
709}