use crate::{
config::scripts::ScriptId,
list::{OwnedTestInstanceId, Styles, TestInstanceId},
reporter::events::{AbortStatus, StressIndex},
run_mode::NextestRunMode,
write_str::WriteStr,
};
use camino::{Utf8Path, Utf8PathBuf};
use console::AnsiCodeIterator;
use nextest_metadata::TestCaseName;
use owo_colors::{OwoColorize, Style};
use std::{fmt, io, ops::ControlFlow, path::PathBuf, process::ExitStatus, time::Duration};
use swrite::{SWrite, swrite};
use unicode_width::UnicodeWidthChar;
pub mod plural {
use crate::run_mode::NextestRunMode;
pub fn were_plural_if(plural: bool) -> &'static str {
if plural { "were" } else { "was" }
}
pub fn setup_scripts_str(count: usize) -> &'static str {
if count == 1 {
"setup script"
} else {
"setup scripts"
}
}
pub fn tests_str(mode: NextestRunMode, count: usize) -> &'static str {
tests_plural_if(mode, count != 1)
}
pub fn tests_plural_if(mode: NextestRunMode, plural: bool) -> &'static str {
match (mode, plural) {
(NextestRunMode::Test, true) => "tests",
(NextestRunMode::Test, false) => "test",
(NextestRunMode::Benchmark, true) => "benchmarks",
(NextestRunMode::Benchmark, false) => "benchmark",
}
}
pub fn tests_plural(mode: NextestRunMode) -> &'static str {
match mode {
NextestRunMode::Test => "tests",
NextestRunMode::Benchmark => "benchmarks",
}
}
pub fn binaries_str(count: usize) -> &'static str {
if count == 1 { "binary" } else { "binaries" }
}
pub fn paths_str(count: usize) -> &'static str {
if count == 1 { "path" } else { "paths" }
}
pub fn files_str(count: usize) -> &'static str {
if count == 1 { "file" } else { "files" }
}
pub fn directories_str(count: usize) -> &'static str {
if count == 1 {
"directory"
} else {
"directories"
}
}
pub fn this_crate_str(count: usize) -> &'static str {
if count == 1 {
"this crate"
} else {
"these crates"
}
}
pub fn libraries_str(count: usize) -> &'static str {
if count == 1 { "library" } else { "libraries" }
}
pub fn filters_str(count: usize) -> &'static str {
if count == 1 { "filter" } else { "filters" }
}
pub fn sections_str(count: usize) -> &'static str {
if count == 1 { "section" } else { "sections" }
}
pub fn iterations_str(count: u32) -> &'static str {
if count == 1 {
"iteration"
} else {
"iterations"
}
}
pub fn runs_str(count: usize) -> &'static str {
if count == 1 { "run" } else { "runs" }
}
pub fn orphans_str(count: usize) -> &'static str {
if count == 1 { "orphan" } else { "orphans" }
}
pub fn errors_str(count: usize) -> &'static str {
if count == 1 { "error" } else { "errors" }
}
pub fn exist_str(count: usize) -> &'static str {
if count == 1 { "exists" } else { "exist" }
}
pub fn end_str(count: usize) -> &'static str {
if count == 1 { "ends" } else { "end" }
}
pub fn remain_str(count: usize) -> &'static str {
if count == 1 { "remains" } else { "remain" }
}
}
pub struct DisplayTestInstance<'a> {
stress_index: Option<StressIndex>,
display_counter_index: Option<DisplayCounterIndex>,
instance: TestInstanceId<'a>,
styles: &'a Styles,
max_width: Option<usize>,
}
impl<'a> DisplayTestInstance<'a> {
pub fn new(
stress_index: Option<StressIndex>,
display_counter_index: Option<DisplayCounterIndex>,
instance: TestInstanceId<'a>,
styles: &'a Styles,
) -> Self {
Self {
stress_index,
display_counter_index,
instance,
styles,
max_width: None,
}
}
pub(crate) fn with_max_width(mut self, max_width: usize) -> Self {
self.max_width = Some(max_width);
self
}
}
impl fmt::Display for DisplayTestInstance<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let stress_index_str = if let Some(stress_index) = self.stress_index {
format!(
"[{}] ",
DisplayStressIndex {
stress_index,
count_style: self.styles.count,
}
)
} else {
String::new()
};
let counter_index_str = if let Some(display_counter_index) = &self.display_counter_index {
format!("{display_counter_index} ")
} else {
String::new()
};
let binary_id_str = format!("{} ", self.instance.binary_id.style(self.styles.binary_id));
let test_name_str = DisplayTestName::new(self.instance.test_name, self.styles).to_string();
if let Some(max_width) = self.max_width {
let stress_index_width = text_width(&stress_index_str);
let counter_index_width = text_width(&counter_index_str);
let binary_id_width = text_width(&binary_id_str);
let test_name_width = text_width(&test_name_str);
let mut stress_index_resolved_width = stress_index_width;
let mut counter_index_resolved_width = counter_index_width;
let mut binary_id_resolved_width = binary_id_width;
let mut test_name_resolved_width = test_name_width;
if stress_index_resolved_width > max_width {
stress_index_resolved_width = max_width;
}
let remaining_width = max_width.saturating_sub(stress_index_resolved_width);
if counter_index_resolved_width > remaining_width {
counter_index_resolved_width = remaining_width;
}
let remaining_width = max_width
.saturating_sub(stress_index_resolved_width)
.saturating_sub(counter_index_resolved_width);
if binary_id_resolved_width > remaining_width {
binary_id_resolved_width = remaining_width;
}
let remaining_width = max_width
.saturating_sub(stress_index_resolved_width)
.saturating_sub(counter_index_resolved_width)
.saturating_sub(binary_id_resolved_width);
if test_name_resolved_width > remaining_width {
test_name_resolved_width = remaining_width;
}
let test_name_truncated_str = if test_name_resolved_width == test_name_width {
test_name_str
} else {
truncate_ansi_aware(
&test_name_str,
test_name_width.saturating_sub(test_name_resolved_width),
test_name_width,
)
};
let binary_id_truncated_str = if binary_id_resolved_width == binary_id_width {
binary_id_str
} else {
truncate_ansi_aware(&binary_id_str, 0, binary_id_resolved_width)
};
let counter_index_truncated_str = if counter_index_resolved_width == counter_index_width
{
counter_index_str
} else {
truncate_ansi_aware(&counter_index_str, 0, counter_index_resolved_width)
};
let stress_index_truncated_str = if stress_index_resolved_width == stress_index_width {
stress_index_str
} else {
truncate_ansi_aware(&stress_index_str, 0, stress_index_resolved_width)
};
write!(
f,
"{}{}{}{}",
stress_index_truncated_str,
counter_index_truncated_str,
binary_id_truncated_str,
test_name_truncated_str,
)
} else {
write!(
f,
"{}{}{}{}",
stress_index_str, counter_index_str, binary_id_str, test_name_str
)
}
}
}
fn text_width(text: &str) -> usize {
strip_ansi_escapes::strip_str(text)
.chars()
.map(|c| c.width().unwrap_or(0))
.sum()
}
fn truncate_ansi_aware(text: &str, start: usize, end: usize) -> String {
let mut pos = 0;
let mut res = String::new();
for (s, is_ansi) in AnsiCodeIterator::new(text) {
if is_ansi {
res.push_str(s);
continue;
} else if pos >= end {
continue;
}
for c in s.chars() {
let c_width = c.width().unwrap_or(0);
if start <= pos && pos + c_width <= end {
res.push(c);
}
pos += c_width;
if pos > end {
break;
}
}
}
res
}
pub(crate) struct DisplayScriptInstance {
stress_index: Option<StressIndex>,
script_id: ScriptId,
full_command: String,
script_id_style: Style,
count_style: Style,
}
impl DisplayScriptInstance {
pub(crate) fn new(
stress_index: Option<StressIndex>,
script_id: ScriptId,
command: &str,
args: &[String],
script_id_style: Style,
count_style: Style,
) -> Self {
let full_command =
shell_words::join(std::iter::once(command).chain(args.iter().map(|arg| arg.as_ref())));
Self {
stress_index,
script_id,
full_command,
script_id_style,
count_style,
}
}
}
impl fmt::Display for DisplayScriptInstance {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(stress_index) = self.stress_index {
write!(
f,
"[{}] ",
DisplayStressIndex {
stress_index,
count_style: self.count_style,
}
)?;
}
write!(
f,
"{}: {}",
self.script_id.style(self.script_id_style),
self.full_command,
)
}
}
struct DisplayStressIndex {
stress_index: StressIndex,
count_style: Style,
}
impl fmt::Display for DisplayStressIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.stress_index.total {
Some(total) => {
write!(
f,
"{:>width$}/{}",
(self.stress_index.current + 1).style(self.count_style),
total.style(self.count_style),
width = decimal_char_width(total.get()),
)
}
None => {
write!(
f,
"{}",
(self.stress_index.current + 1).style(self.count_style)
)
}
}
}
}
pub enum DisplayCounterIndex {
Counter {
current: usize,
total: usize,
},
Padded {
character: char,
width: usize,
},
}
impl DisplayCounterIndex {
pub fn new_counter(current: usize, total: usize) -> Self {
Self::Counter { current, total }
}
pub fn new_padded(character: char, width: usize) -> Self {
Self::Padded { character, width }
}
}
impl fmt::Display for DisplayCounterIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Counter { current, total } => {
write!(
f,
"({:>width$}/{})",
current,
total,
width = decimal_char_width(*total)
)
}
Self::Padded { character, width } => {
let s: String = std::iter::repeat_n(*character, 2 * *width + 1).collect();
write!(f, "({s})")
}
}
}
}
pub(crate) fn decimal_char_width<T>(n: T) -> usize
where
T: TryInto<u128> + Copy,
{
let n: u128 = n.try_into().ok().expect("converted to u128");
(n.checked_ilog10().unwrap_or(0) + 1) as usize
}
pub(crate) fn write_test_name(
name: &TestCaseName,
style: &Styles,
writer: &mut dyn WriteStr,
) -> io::Result<()> {
let (module_path, trailing) = name.module_path_and_name();
if let Some(module_path) = module_path {
write!(
writer,
"{}{}",
module_path.style(style.module_path),
"::".style(style.module_path)
)?;
}
write!(writer, "{}", trailing.style(style.test_name))?;
Ok(())
}
pub(crate) struct DisplayTestName<'a> {
name: &'a TestCaseName,
styles: &'a Styles,
}
impl<'a> DisplayTestName<'a> {
pub(crate) fn new(name: &'a TestCaseName, styles: &'a Styles) -> Self {
Self { name, styles }
}
}
impl fmt::Display for DisplayTestName<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (module_path, trailing) = self.name.module_path_and_name();
if let Some(module_path) = module_path {
write!(
f,
"{}{}",
module_path.style(self.styles.module_path),
"::".style(self.styles.module_path)
)?;
}
write!(f, "{}", trailing.style(self.styles.test_name))?;
Ok(())
}
}
pub(crate) fn convert_build_platform(
platform: nextest_metadata::BuildPlatform,
) -> guppy::graph::cargo::BuildPlatform {
match platform {
nextest_metadata::BuildPlatform::Target => guppy::graph::cargo::BuildPlatform::Target,
nextest_metadata::BuildPlatform::Host => guppy::graph::cargo::BuildPlatform::Host,
}
}
pub(crate) fn dylib_path_envvar() -> &'static str {
if cfg!(windows) {
"PATH"
} else if cfg!(target_os = "macos") {
"DYLD_FALLBACK_LIBRARY_PATH"
} else {
"LD_LIBRARY_PATH"
}
}
pub(crate) fn dylib_path() -> Vec<PathBuf> {
match std::env::var_os(dylib_path_envvar()) {
Some(var) => std::env::split_paths(&var).collect(),
None => Vec::new(),
}
}
#[cfg(windows)]
pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
if !rel_path.is_relative() {
panic!("path for conversion to forward slash '{rel_path}' is not relative");
}
rel_path.as_str().replace('\\', "/").into()
}
#[cfg(not(windows))]
pub(crate) fn convert_rel_path_to_forward_slash(rel_path: &Utf8Path) -> Utf8PathBuf {
rel_path.to_path_buf()
}
#[cfg(windows)]
pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
if !rel_path.is_relative() {
panic!("path for conversion to backslash '{rel_path}' is not relative");
}
rel_path.as_str().replace('/', "\\").into()
}
#[cfg(not(windows))]
pub(crate) fn convert_rel_path_to_main_sep(rel_path: &Utf8Path) -> Utf8PathBuf {
rel_path.to_path_buf()
}
pub(crate) fn rel_path_join(rel_path: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
assert!(rel_path.is_relative(), "rel_path {rel_path} is relative");
assert!(path.is_relative(), "path {path} is relative",);
format!("{rel_path}/{path}").into()
}
#[derive(Debug)]
pub(crate) struct FormattedDuration(pub(crate) Duration);
#[derive(Copy, Clone, Debug)]
pub(crate) enum DurationRounding {
Floor,
Ceiling,
}
#[derive(Debug)]
pub(crate) struct FormattedHhMmSs {
pub(crate) duration: Duration,
pub(crate) rounding: DurationRounding,
}
impl fmt::Display for FormattedHhMmSs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let total_secs = self.duration.as_secs();
let total_secs = match self.rounding {
DurationRounding::Ceiling if self.duration.subsec_millis() > 0 => total_secs + 1,
_ => total_secs,
};
let secs = total_secs % 60;
let total_mins = total_secs / 60;
let mins = total_mins % 60;
let hours = total_mins / 60;
write!(f, "{hours:02}:{mins:02}:{secs:02}")
}
}
impl fmt::Display for FormattedDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let duration = self.0.as_secs_f64();
if duration > 60.0 {
write!(f, "{}m {:.2}s", duration as u32 / 60, duration % 60.0)
} else {
write!(f, "{duration:.2}s")
}
}
}
#[derive(Debug)]
pub(crate) struct FormattedRelativeDuration(pub(crate) Duration);
impl fmt::Display for FormattedRelativeDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
if value > 0 {
ControlFlow::Break((unit, value))
} else {
ControlFlow::Continue(())
}
}
fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
let secs = f.as_secs();
let nanos = f.subsec_nanos();
let years = secs / 31_557_600; let year_days = secs % 31_557_600;
let months = year_days / 2_630_016; let month_days = year_days % 2_630_016;
let days = month_days / 86400;
let day_secs = month_days % 86400;
let hours = day_secs / 3600;
let minutes = day_secs % 3600 / 60;
let seconds = day_secs % 60;
let millis = nanos / 1_000_000;
let micros = nanos / 1_000;
item("y", years)?;
item("mo", months)?;
item("d", days)?;
item("h", hours)?;
item("m", minutes)?;
item("s", seconds)?;
item("ms", u64::from(millis))?;
item("us", u64::from(micros))?;
item("ns", u64::from(nanos))?;
ControlFlow::Continue(())
}
match fmt(self.0) {
ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
ControlFlow::Continue(()) => write!(f, "0s"),
}
}
}
#[derive(Clone, Debug)]
pub struct ThemeCharacters {
hbar: char,
progress_chars: &'static str,
use_unicode: bool,
}
impl Default for ThemeCharacters {
fn default() -> Self {
Self {
hbar: '-',
progress_chars: "=> ",
use_unicode: false,
}
}
}
impl ThemeCharacters {
pub fn detect(stream: supports_unicode::Stream) -> Self {
let mut this = Self::default();
if supports_unicode::on(stream) {
this.use_unicode();
}
this
}
pub fn use_unicode(&mut self) {
self.hbar = '─';
self.progress_chars = "█▉▊▋▌▍▎▏ ";
self.use_unicode = true;
}
pub fn hbar_char(&self) -> char {
self.hbar
}
pub fn hbar(&self, width: usize) -> String {
std::iter::repeat_n(self.hbar, width).collect()
}
pub fn progress_chars(&self) -> &'static str {
self.progress_chars
}
pub fn tree_branch(&self) -> &'static str {
if self.use_unicode { "├─" } else { "|-" }
}
pub fn tree_last(&self) -> &'static str {
if self.use_unicode { "└─" } else { "\\-" }
}
pub fn tree_continuation(&self) -> &'static str {
if self.use_unicode { "│ " } else { "| " }
}
pub fn tree_space(&self) -> &'static str {
" "
}
}
pub(crate) fn display_exited_with(exit_status: ExitStatus) -> String {
match AbortStatus::extract(exit_status) {
Some(abort_status) => display_abort_status(abort_status),
None => match exit_status.code() {
Some(code) => format!("exited with exit code {code}"),
None => "exited with an unknown error".to_owned(),
},
}
}
pub(crate) fn display_abort_status(abort_status: AbortStatus) -> String {
match abort_status {
#[cfg(unix)]
AbortStatus::UnixSignal(sig) => match crate::helpers::signal_str(sig) {
Some(s) => {
format!("aborted with signal {sig} (SIG{s})")
}
None => {
format!("aborted with signal {sig}")
}
},
#[cfg(windows)]
AbortStatus::WindowsNtStatus(nt_status) => {
format!(
"aborted with code {}",
crate::helpers::display_nt_status(nt_status, Style::new())
)
}
#[cfg(windows)]
AbortStatus::JobObject => "terminated via job object".to_string(),
}
}
#[cfg(unix)]
pub(crate) fn signal_str(signal: i32) -> Option<&'static str> {
match signal {
1 => Some("HUP"),
2 => Some("INT"),
3 => Some("QUIT"),
4 => Some("ILL"),
5 => Some("TRAP"),
6 => Some("ABRT"),
8 => Some("FPE"),
9 => Some("KILL"),
11 => Some("SEGV"),
13 => Some("PIPE"),
14 => Some("ALRM"),
15 => Some("TERM"),
_ => None,
}
}
#[cfg(windows)]
pub(crate) fn display_nt_status(
nt_status: windows_sys::Win32::Foundation::NTSTATUS,
bold_style: Style,
) -> String {
let bolded_status = format!("{:#010x}", nt_status.style(bold_style));
match windows_nt_status_message(nt_status) {
Some(message) => format!("{bolded_status}: {message}"),
None => bolded_status,
}
}
#[cfg(windows)]
pub(crate) fn windows_nt_status_message(
nt_status: windows_sys::Win32::Foundation::NTSTATUS,
) -> Option<smol_str::SmolStr> {
let win32_code = unsafe { windows_sys::Win32::Foundation::RtlNtStatusToDosError(nt_status) };
if win32_code == windows_sys::Win32::Foundation::ERROR_MR_MID_NOT_FOUND {
return None;
}
Some(smol_str::SmolStr::new(
io::Error::from_raw_os_error(win32_code as i32).to_string(),
))
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct QuotedDisplay<'a, T: ?Sized>(pub(crate) &'a T);
impl<T: ?Sized> fmt::Display for QuotedDisplay<'_, T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "'{}'", self.0)
}
}
unsafe extern "C" {
fn __nextest_external_symbol_that_does_not_exist();
}
pub fn format_interceptor_too_many_tests(
cli_opt_name: &str,
mode: NextestRunMode,
test_count: usize,
test_instances: &[OwnedTestInstanceId],
list_styles: &Styles,
count_style: Style,
) -> String {
let mut msg = format!(
"--{} requires exactly one {}, but {} {} were selected:",
cli_opt_name,
plural::tests_plural_if(mode, false),
test_count.style(count_style),
plural::tests_str(mode, test_count)
);
for test_instance in test_instances {
let display = DisplayTestInstance::new(None, None, test_instance.as_ref(), list_styles);
swrite!(msg, "\n {}", display);
}
if test_count > test_instances.len() {
let remaining = test_count - test_instances.len();
swrite!(
msg,
"\n ... and {} more {}",
remaining.style(count_style),
plural::tests_str(mode, remaining)
);
}
msg
}
#[inline]
#[expect(dead_code)]
pub(crate) fn statically_unreachable() -> ! {
unsafe {
__nextest_external_symbol_that_does_not_exist();
}
unreachable!("linker symbol above cannot be resolved")
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_decimal_char_width() {
assert_eq!(1, decimal_char_width(0_usize));
assert_eq!(1, decimal_char_width(1_usize));
assert_eq!(1, decimal_char_width(5_usize));
assert_eq!(1, decimal_char_width(9_usize));
assert_eq!(2, decimal_char_width(10_usize));
assert_eq!(2, decimal_char_width(11_usize));
assert_eq!(2, decimal_char_width(99_usize));
assert_eq!(3, decimal_char_width(100_usize));
assert_eq!(3, decimal_char_width(999_usize));
assert_eq!(1, decimal_char_width(0_u32));
assert_eq!(3, decimal_char_width(100_u32));
assert_eq!(1, decimal_char_width(0_u64));
assert_eq!(1, decimal_char_width(1_u64));
assert_eq!(1, decimal_char_width(9_u64));
assert_eq!(2, decimal_char_width(10_u64));
assert_eq!(2, decimal_char_width(99_u64));
assert_eq!(3, decimal_char_width(100_u64));
assert_eq!(3, decimal_char_width(999_u64));
assert_eq!(6, decimal_char_width(999_999_u64));
assert_eq!(7, decimal_char_width(1_000_000_u64));
assert_eq!(8, decimal_char_width(10_000_000_u64));
assert_eq!(8, decimal_char_width(11_000_000_u64));
}
}