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 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 instance: TestInstanceId<'a>,
103 styles: &'a Styles,
104}
105
106impl<'a> DisplayTestInstance<'a> {
107 pub(crate) fn new(
108 stress_index: Option<StressIndex>,
109 instance: TestInstanceId<'a>,
110 styles: &'a Styles,
111 ) -> Self {
112 Self {
113 stress_index,
114 instance,
115 styles,
116 }
117 }
118}
119
120impl fmt::Display for DisplayTestInstance<'_> {
121 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122 if let Some(stress_index) = self.stress_index {
123 write!(
124 f,
125 "[{}] ",
126 DisplayStressIndex {
127 stress_index,
128 count_style: self.styles.count,
129 }
130 )?;
131 }
132
133 write!(
134 f,
135 "{} ",
136 self.instance.binary_id.style(self.styles.binary_id),
137 )?;
138 fmt_write_test_name(self.instance.test_name, self.styles, f)
139 }
140}
141
142pub(crate) struct DisplayScriptInstance {
143 stress_index: Option<StressIndex>,
144 script_id: ScriptId,
145 full_command: String,
146 script_id_style: Style,
147 count_style: Style,
148}
149
150impl DisplayScriptInstance {
151 pub(crate) fn new(
152 stress_index: Option<StressIndex>,
153 script_id: ScriptId,
154 command: &str,
155 args: &[String],
156 script_id_style: Style,
157 count_style: Style,
158 ) -> Self {
159 let full_command =
160 shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
161
162 Self {
163 stress_index,
164 script_id,
165 full_command,
166 script_id_style,
167 count_style,
168 }
169 }
170}
171
172impl fmt::Display for DisplayScriptInstance {
173 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174 if let Some(stress_index) = self.stress_index {
175 write!(
176 f,
177 "[{}] ",
178 DisplayStressIndex {
179 stress_index,
180 count_style: self.count_style,
181 }
182 )?;
183 }
184 write!(
185 f,
186 "{}: {}",
187 self.script_id.style(self.script_id_style),
188 self.full_command,
189 )
190 }
191}
192
193struct DisplayStressIndex {
194 stress_index: StressIndex,
195 count_style: Style,
196}
197
198impl fmt::Display for DisplayStressIndex {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match self.stress_index.total {
201 Some(total) => {
202 write!(
203 f,
204 "{}/{}",
205 (self.stress_index.current + 1).style(self.count_style),
206 total.style(self.count_style)
207 )
208 }
209 None => {
210 write!(
211 f,
212 "{}",
213 (self.stress_index.current + 1).style(self.count_style)
214 )
215 }
216 }
217 }
218}
219
220pub(crate) fn write_test_name(
222 name: &str,
223 style: &Styles,
224 writer: &mut dyn WriteStr,
225) -> io::Result<()> {
226 let mut splits = name.rsplitn(2, "::");
228 let trailing = splits.next().expect("test should have at least 1 element");
229 if let Some(rest) = splits.next() {
230 write!(
231 writer,
232 "{}{}",
233 rest.style(style.module_path),
234 "::".style(style.module_path)
235 )?;
236 }
237 write!(writer, "{}", trailing.style(style.test_name))?;
238
239 Ok(())
240}
241
242pub(crate) fn fmt_write_test_name(
244 name: &str,
245 style: &Styles,
246 writer: &mut dyn fmt::Write,
247) -> fmt::Result {
248 let mut splits = name.rsplitn(2, "::");
250 let trailing = splits.next().expect("test should have at least 1 element");
251 if let Some(rest) = splits.next() {
252 write!(
253 writer,
254 "{}{}",
255 rest.style(style.module_path),
256 "::".style(style.module_path)
257 )?;
258 }
259 write!(writer, "{}", trailing.style(style.test_name))?;
260
261 Ok(())
262}
263
264pub(crate) fn convert_build_platform(
265 platform: nextest_metadata::BuildPlatform,
266) -> guppy::graph::cargo::BuildPlatform {
267 match platform {
268 nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
269 nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
270 }
271}
272
273pub(crate) fn dylib_path_envvar() -> &'static str {
280 if cfg!(windows) {
281 "PATH"
282 } else if cfg!(target_os = "macos") {
283 "DYLD_FALLBACK_LIBRARY_PATH"
299 } else {
300 "LD_LIBRARY_PATH"
301 }
302}
303
304pub(crate) fn dylib_path() -> Vec<PathBuf> {
309 match std::env::var_os(dylib_path_envvar()) {
310 Some(var) => std::env::split_paths(&var).collect(),
311 None => Vec::new(),
312 }
313}
314
315#[cfg(windows)]
317pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
318 if !rel_path.is_relative() {
319 panic!("path for conversion to forward slash '{rel_path}' is not relative");
320 }
321 rel_path.as_str().replace('\\', "/").into()
322}
323
324#[cfg(not(windows))]
325pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
326 rel_path.to_path_buf()
327}
328
329#[cfg(windows)]
331pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
332 if !rel_path.is_relative() {
333 panic!("path for conversion to backslash '{rel_path}' is not relative");
334 }
335 rel_path.as_str().replace('/', "\\").into()
336}
337
338#[cfg(not(windows))]
339pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
340 rel_path.to_path_buf()
341}
342
343pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
345 assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
346 assert!(path.is_relative(), "path {path} is relative",);
347 format!("{rel_path}/{path}").into()
348}
349
350#[derive(Debug)]
351pub(crate) struct FormattedDuration(pub(crate) Duration);
352
353impl fmt::Display for FormattedDuration {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 let duration = self.0.as_secs_f64();
356 if duration > 60.0 {
357 write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
358 } else {
359 write!(f, "{duration:.2}s")
360 }
361 }
362}
363
364pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
366 match AbortStatus::extract(exit_status) {
367 Some(abort_status) => display_abort_status(abort_status),
368 None => match exit_status.code() {
369 Some(code) => format!("exited with exit code {code}"),
370 None => "exited with an unknown error".to_owned(),
371 },
372 }
373}
374
375pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
377 match abort_status {
378 #[cfg(unix)]
379 AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
380 Some(s) => {
381 format!("aborted with signal {sig} (SIG{s})")
382 }
383 None => {
384 format!("aborted with signal {sig}")
385 }
386 },
387 #[cfg(windows)]
388 AbortStatus::WindowsNtStatus(nt_status) => {
389 format!(
390 "aborted with code {}",
391 crate::helpers::display_nt_status(nt_status, Style::new())
393 )
394 }
395 #[cfg(windows)]
396 AbortStatus::JobObject => "terminated via job object".to_string(),
397 }
398}
399
400#[cfg(unix)]
401pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
402 match signal {
409 1 => Some("HUP"),
410 2 => Some("INT"),
411 3 => Some("QUIT"),
412 4 => Some("ILL"),
413 5 => Some("TRAP"),
414 6 => Some("ABRT"),
415 8 => Some("FPE"),
416 9 => Some("KILL"),
417 11 => Some("SEGV"),
418 13 => Some("PIPE"),
419 14 => Some("ALRM"),
420 15 => Some("TERM"),
421 _ => None,
422 }
423}
424
425#[cfg(windows)]
426pub(crate) fn display_nt_status(
427 nt_status: windows_sys::Win32::Foundation::NTSTATUS,
428 bold_style: Style,
429) -> String {
430 let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
434 let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
436
437 if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
438 return bolded_status;
440 }
441
442 format!(
443 "{bolded_status}: {}",
444 io::Error::from_raw_os_error(win32_code as i32)
445 )
446}
447
448#[derive(Copy, Clone, Debug)]
449pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
450
451impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
452where
453 T: fmt::Display,
454{
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 write!(f, "'{}'", self.0)
457 }
458}
459
460unsafe extern "C" {
462 fn __nextest_external_symbol_that_does_not_exist();
463}
464
465#[inline]
466#[expect(dead_code)]
467pub(crate) fn statically_unreachable() -> ! {
468 unsafe {
469 __nextest_external_symbol_that_does_not_exist();
470 }
471 unreachable!("linker symbol above cannot be resolved")
472}