1use 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
21pub mod plural {
23 use crate::run_mode::NextestRunMode;
24
25 pub fn were_plural_if(plural: bool) -> &'static str {
27 if plural { "were" } else { "was" }
28 }
29
30 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 pub fn tests_str(mode: NextestRunMode, count: usize) -> &'static str {
44 tests_plural_if(mode, count != 1)
45 }
46
47 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 pub fn tests_plural(mode: NextestRunMode) -> &'static str {
62 match mode {
63 NextestRunMode::Test => "tests",
64 NextestRunMode::Benchmark => "benchmarks",
65 }
66 }
67
68 pub fn binaries_str(count: usize) -> &'static str {
70 if count == 1 { "binary" } else { "binaries" }
71 }
72
73 pub fn paths_str(count: usize) -> &'static str {
75 if count == 1 { "path" } else { "paths" }
76 }
77
78 pub fn files_str(count: usize) -> &'static str {
80 if count == 1 { "file" } else { "files" }
81 }
82
83 pub fn directories_str(count: usize) -> &'static str {
85 if count == 1 {
86 "directory"
87 } else {
88 "directories"
89 }
90 }
91
92 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 pub fn libraries_str(count: usize) -> &'static str {
103 if count == 1 { "library" } else { "libraries" }
104 }
105
106 pub fn filters_str(count: usize) -> &'static str {
108 if count == 1 { "filter" } else { "filters" }
109 }
110
111 pub fn sections_str(count: usize) -> &'static str {
113 if count == 1 { "section" } else { "sections" }
114 }
115
116 pub fn iterations_str(count: u32) -> &'static str {
118 if count == 1 {
119 "iteration"
120 } else {
121 "iterations"
122 }
123 }
124}
125
126pub 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 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 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 let Some(max_width) = self.max_width {
185 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 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 if stress_index_resolved_width > max_width {
206 stress_index_resolved_width = max_width;
207 }
208
209 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 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 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 let test_name_truncated_str = if test_name_resolved_width == test_name_width {
234 test_name_str
235 } else {
236 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 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 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 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 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 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 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
403pub enum DisplayCounterIndex {
405 Counter {
407 current: usize,
409 total: usize,
411 },
412 Padded {
414 character: char,
416 width: usize,
418 },
419}
420
421impl DisplayCounterIndex {
422 pub fn new_counter(current: usize, total: usize) -> Self {
424 Self::Counter { current, total }
425 }
426
427 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 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 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
462}
463
464pub(crate) fn u32_decimal_char_width(n: u32) -> usize {
465 (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap()
469}
470
471pub(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
491pub(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
529pub(crate) fn dylib_path_envvar() -> &'static str {
536 if cfg!(windows) {
537 "PATH"
538 } else if cfg!(target_os = "macos") {
539 "DYLD_FALLBACK_LIBRARY_PATH"
555 } else {
556 "LD_LIBRARY_PATH"
557 }
558}
559
560pub(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#[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#[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
599pub(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
620pub(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
631pub(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 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 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 let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
690 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 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
716unsafe extern "C" {
718 fn __nextest_external_symbol_that_does_not_exist();
719}
720
721pub 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}