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