From b02c4d0ce43a8933b036cfea8465af9bd0770c75 Mon Sep 17 00:00:00 2001 From: Ivan Babrou Date: Sun, 28 Sep 2025 22:13:26 -0700 Subject: [PATCH] Add a counter to the status line with each test --- cargo-nextest/src/dispatch.rs | 41 ++++- .../tests/integration/fixtures.rs | 22 ++- nextest-runner/src/helpers.rs | 61 ++++++- nextest-runner/src/reporter/displayer/imp.rs | 155 ++++++++++++------ nextest-runner/src/reporter/displayer/mod.rs | 1 + .../src/reporter/displayer/progress.rs | 21 +++ nextest-runner/src/reporter/imp.rs | 16 +- nextest-runner/src/reporter/mod.rs | 2 +- 8 files changed, 249 insertions(+), 70 deletions(-) diff --git a/cargo-nextest/src/dispatch.rs b/cargo-nextest/src/dispatch.rs index 3588aa4f1ea..d94f45508ab 100644 --- a/cargo-nextest/src/dispatch.rs +++ b/cargo-nextest/src/dispatch.rs @@ -36,7 +36,8 @@ use nextest_runner::{ platform::{BuildPlatforms, HostPlatform, PlatformLibdir, TargetPlatform}, redact::Redactor, reporter::{ - FinalStatusLevel, ReporterBuilder, StatusLevel, TestOutputDisplay, TestOutputErrorSlice, + FinalStatusLevel, ReporterBuilder, ShowProgress, StatusLevel, TestOutputDisplay, + TestOutputErrorSlice, events::{FinalRunStats, RunStatsFailureKind}, highlight_end, structured, }, @@ -1049,7 +1050,11 @@ struct ReporterOpts { )] final_status_level: Option, - /// Do not display the progress bar + /// Show progress in a specified way. + #[arg(long, env = "NEXTEST_SHOW_PROGRESS")] + show_progress: Option, + + /// Do not display the progress bar. Deprecated, use **--show-progress** instead. #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())] hide_progress_bar: bool, @@ -1128,6 +1133,16 @@ impl ReporterOpts { warn!("ignoring --message-format-version because --no-run is specified"); } + let show_progress = match (self.show_progress, self.hide_progress_bar) { + (Some(show_progress), true) => { + warn!("ignoring --hide-progress-bar because --show-progress is specified"); + show_progress + } + (Some(show_progress), false) => show_progress, + (None, true) => ShowProgressOpt::None, + (None, false) => ShowProgressOpt::default(), + }; + // --- let mut builder = ReporterBuilder::default(); @@ -1146,7 +1161,7 @@ impl ReporterOpts { if let Some(final_status_level) = self.final_status_level { builder.set_final_status_level(final_status_level.into()); } - builder.set_hide_progress_bar(self.hide_progress_bar); + builder.set_show_progress(show_progress.into()); builder.set_no_output_indent(self.no_output_indent); builder } @@ -1225,6 +1240,26 @@ impl From for FinalStatusLevel { } } +#[derive(Default, Clone, Copy, Debug, ValueEnum)] +enum ShowProgressOpt { + #[default] + Auto, + None, + Bar, + Counter, +} + +impl From for ShowProgress { + fn from(opt: ShowProgressOpt) -> Self { + match opt { + ShowProgressOpt::Auto => Self::Auto, + ShowProgressOpt::None => Self::None, + ShowProgressOpt::Bar => Self::Bar, + ShowProgressOpt::Counter => Self::Counter, + } + } +} + #[derive(Debug)] struct BaseApp { output: OutputContext, diff --git a/integration-tests/tests/integration/fixtures.rs b/integration-tests/tests/integration/fixtures.rs index cad0799b646..f07dad835a1 100644 --- a/integration-tests/tests/integration/fixtures.rs +++ b/integration-tests/tests/integration/fixtures.rs @@ -153,13 +153,23 @@ impl CheckResult { fn make_status_line_regex(self, name: &str) -> Regex { let name = regex::escape(name); match self { - CheckResult::Pass => Regex::new(&format!(r"PASS \[.*\] *{name}")).unwrap(), - CheckResult::Leak => Regex::new(&format!(r"LEAK \[.*\] *{name}")).unwrap(), - CheckResult::LeakFail => Regex::new(&format!(r"LEAK-FAIL \[.*\] *{name}")).unwrap(), - CheckResult::Fail => Regex::new(&format!(r"FAIL \[.*\] *{name}")).unwrap(), - CheckResult::FailLeak => Regex::new(&format!(r"FAIL \+ LEAK \[.*\] *{name}")).unwrap(), + CheckResult::Pass => { + Regex::new(&format!(r"PASS \[[^\]]+\] \([^\)]+\) *{name}")).unwrap() + } + CheckResult::Leak => { + Regex::new(&format!(r"LEAK \[[^\]]+\] \([^\)]+\) *{name}")).unwrap() + } + CheckResult::LeakFail => { + Regex::new(&format!(r"LEAK-FAIL \[[^\]]+\] \([^\)]+\) *{name}")).unwrap() + } + CheckResult::Fail => { + Regex::new(&format!(r"FAIL \[[^\]]+\] \([^\)]+\) *{name}")).unwrap() + } + CheckResult::FailLeak => { + Regex::new(&format!(r"FAIL \+ LEAK \[[^\]]+\] \([^\)]+\) *{name}")).unwrap() + } CheckResult::Abort => { - Regex::new(&format!(r"(ABORT|SIGSEGV|SIGABRT) \[.*\] *{name}")).unwrap() + Regex::new(&format!(r"(ABORT|SIGSEGV|SIGABRT) \[[^\]]+\] *{name}")).unwrap() } } } diff --git a/nextest-runner/src/helpers.rs b/nextest-runner/src/helpers.rs index bfa70e93a1b..459f8bd90e8 100644 --- a/nextest-runner/src/helpers.rs +++ b/nextest-runner/src/helpers.rs @@ -6,7 +6,7 @@ use crate::{ config::scripts::ScriptId, list::{Styles, TestInstanceId}, - reporter::events::{AbortStatus, StressIndex}, + reporter::events::{AbortStatus, RunStats, StressIndex}, write_str::WriteStr, }; use camino::{Utf8Path, Utf8PathBuf}; @@ -99,6 +99,7 @@ pub mod plural { pub(crate) struct DisplayTestInstance<'a> { stress_index: Option, + display_counter_index: Option, instance: TestInstanceId<'a>, styles: &'a Styles, } @@ -106,11 +107,13 @@ pub(crate) struct DisplayTestInstance<'a> { impl<'a> DisplayTestInstance<'a> { pub(crate) fn new( stress_index: Option, + display_counter_index: Option, instance: TestInstanceId<'a>, styles: &'a Styles, ) -> Self { Self { stress_index, + display_counter_index, instance, styles, } @@ -130,6 +133,10 @@ impl fmt::Display for DisplayTestInstance<'_> { )?; } + if let Some(display_counter_index) = &self.display_counter_index { + write!(f, "{display_counter_index} ")? + } + write!( f, "{} ", @@ -217,6 +224,40 @@ impl fmt::Display for DisplayStressIndex { } } +pub(super) struct DisplayCounterIndex(usize, usize); + +impl DisplayCounterIndex { + pub fn new(current_stats: &RunStats) -> Self { + Self( + current_stats.finished_count, + current_stats.initial_run_count, + ) + } + + pub fn width(&self) -> usize { + decimal_char_width(self.1) * 2 + 3 + } +} + +impl fmt::Display for DisplayCounterIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "({:>width$}/{})", + self.0, + self.1, + width = decimal_char_width(self.1) + ) + } +} + +pub(crate) fn decimal_char_width(n: usize) -> usize { + // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And + // None for 0 which we unwrap to the same as 1). Add 1 to it to get the + // actual number of digits. + (n.checked_ilog10().unwrap_or(0) + 1).try_into().unwrap() +} + /// Write out a test name. pub(crate) fn write_test_name( name: &str, @@ -470,3 +511,21 @@ pub(crate) fn statically_unreachable() -> ! { } 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)); + assert_eq!(1, decimal_char_width(1)); + assert_eq!(1, decimal_char_width(5)); + assert_eq!(1, decimal_char_width(9)); + assert_eq!(2, decimal_char_width(10)); + assert_eq!(2, decimal_char_width(11)); + assert_eq!(2, decimal_char_width(99)); + assert_eq!(3, decimal_char_width(100)); + assert_eq!(3, decimal_char_width(999)); + } +} diff --git a/nextest-runner/src/reporter/displayer/imp.rs b/nextest-runner/src/reporter/displayer/imp.rs index 3fa00e68d52..2462cb4f652 100644 --- a/nextest-runner/src/reporter/displayer/imp.rs +++ b/nextest-runner/src/reporter/displayer/imp.rs @@ -19,10 +19,12 @@ use crate::{ cargo_config::CargoConfigs, config::{elements::LeakTimeoutResult, overrides::CompiledDefaultFilter, scripts::ScriptId}, errors::WriteEventError, - helpers::{DisplayScriptInstance, DisplayTestInstance, plural}, + helpers::{ + DisplayCounterIndex, DisplayScriptInstance, DisplayTestInstance, decimal_char_width, plural, + }, list::{TestInstance, TestInstanceId}, reporter::{ - displayer::{formatters::DisplayHhMmSs, progress::TerminalProgress}, + displayer::{ShowProgress, formatters::DisplayHhMmSs, progress::TerminalProgress}, events::*, helpers::Styles, imp::ReporterStderr, @@ -48,7 +50,7 @@ pub(crate) struct DisplayReporterBuilder { pub(crate) failure_output: Option, pub(crate) should_colorize: bool, pub(crate) no_capture: bool, - pub(crate) hide_progress_bar: bool, + pub(crate) show_progress: ShowProgress, pub(crate) no_output_indent: bool, } @@ -82,34 +84,18 @@ impl DisplayReporterBuilder { } } + let mut show_progress_bar = false; + let stderr = match output { ReporterStderr::Terminal => { - let progress_bar = if self.no_capture { - // Do not use a progress bar if --no-capture is passed in. - // This is required since we pass down stderr to the child - // process. - // - // In the future, we could potentially switch to using a - // pty, in which case we could still potentially use the - // progress bar as a status bar. However, that brings about - // its own complications: what if a test's output doesn't - // include a newline? We might have to use a curses-like UI - // which would be a lot of work for not much gain. - None - } else if is_ci::uncached() { - // Some CI environments appear to pretend to be a terminal. - // Disable the progress bar in these environments. - None - } else if self.hide_progress_bar { - None - } else { - let state = - ProgressBarState::new(self.test_count, theme_characters.progress_chars); - // Note: even if we create a progress bar here, if stderr is - // piped, indicatif will not show it. - Some(state) - }; + let progress_bar = self.progress_bar(theme_characters.progress_chars); let term_progress = TerminalProgress::new(configs, &io::stderr()); + + show_progress_bar = progress_bar + .as_ref() + .map(|progress_bar| !progress_bar.is_hidden()) + .unwrap_or_default(); + ReporterStderrImpl::Terminal { progress_bar, term_progress, @@ -129,6 +115,12 @@ impl DisplayReporterBuilder { false => self.failure_output, }; + let show_counter = match self.show_progress { + ShowProgress::Auto => is_ci::uncached() || !show_progress_bar, + ShowProgress::Bar | ShowProgress::None => false, + ShowProgress::Counter => true, + }; + DisplayReporter { inner: DisplayReporterImpl { default_filter: self.default_filter, @@ -138,6 +130,7 @@ impl DisplayReporterBuilder { }, no_capture: self.no_capture, no_output_indent: self.no_output_indent, + no_counter: !show_counter, styles, theme_characters, cancel_status: None, @@ -147,6 +140,39 @@ impl DisplayReporterBuilder { stderr, } } + + fn progress_bar(&self, progress_chars: &'static str) -> Option { + if self.no_capture { + // Do not use a progress bar if --no-capture is passed in. + // This is required since we pass down stderr to the child + // process. + // + // In the future, we could potentially switch to using a + // pty, in which case we could still potentially use the + // progress bar as a status bar. However, that brings about + // its own complications: what if a test's output doesn't + // include a newline? We might have to use a curses-like UI + // which would be a lot of work for not much gain. + return None; + } + + if is_ci::uncached() { + // Some CI environments appear to pretend to be a terminal. + // Disable the progress bar in these environments. + return None; + } + + match self.show_progress { + ShowProgress::None | ShowProgress::Counter => return None, + // For auto we enable progress bar if not in ci, and it's checked above. + ShowProgress::Auto | ShowProgress::Bar => (), + }; + + let state = ProgressBarState::new(self.test_count, progress_chars); + // Note: even if we create a progress bar here, if stderr is + // piped, indicatif will not show it. + Some(state) + } } /// Functionality to report test results to stderr, JUnit, and/or structured, @@ -259,6 +285,7 @@ struct DisplayReporterImpl<'a> { status_levels: StatusLevels, no_capture: bool, no_output_indent: bool, + no_counter: bool, styles: Box, theme_characters: ThemeCharacters, cancel_status: Option, @@ -478,16 +505,25 @@ impl<'a> DisplayReporterImpl<'a> { TestEventKind::TestStarted { stress_index, test_instance, + current_stats, .. } => { // In no-capture mode, print out a test start event. if self.no_capture { // The spacing is to align test instances. + let width = 11 + + if self.no_counter { + 0 + } else { + DisplayCounterIndex::new(current_stats).width() + 1 + }; writeln!( writer, - "{:>12} {}", + "{:>12} {:>width$} {}", "START".style(self.styles.pass), - self.display_test_instance(*stress_index, test_instance.id()), + " ", + self.display_test_instance(*stress_index, None, test_instance.id()), + width = width, )?; } } @@ -531,7 +567,7 @@ impl<'a> DisplayReporterImpl<'a> { writer, "{}{}", DisplaySlowDuration(*elapsed), - self.display_test_instance(*stress_index, test_instance.id()) + self.display_test_instance(*stress_index, None, test_instance.id()) )?; } @@ -561,7 +597,7 @@ impl<'a> DisplayReporterImpl<'a> { writeln!( writer, "{}", - self.display_test_instance(*stress_index, test_instance.id()) + self.display_test_instance(*stress_index, None, test_instance.id()) )?; // This test is guaranteed to have failed. @@ -598,7 +634,7 @@ impl<'a> DisplayReporterImpl<'a> { writeln!( writer, "{}", - self.display_test_instance(*stress_index, test_instance.id()) + self.display_test_instance(*stress_index, None, test_instance.id()) )?; } } @@ -620,7 +656,7 @@ impl<'a> DisplayReporterImpl<'a> { writer, "[{:<9}] {}", "", - self.display_test_instance(*stress_index, test_instance.id()) + self.display_test_instance(*stress_index, None, test_instance.id()) )?; } TestEventKind::TestFinished { @@ -629,6 +665,7 @@ impl<'a> DisplayReporterImpl<'a> { success_output, failure_output, run_statuses, + current_stats, .. } => { let describe = run_statuses.describe(); @@ -646,7 +683,13 @@ impl<'a> DisplayReporterImpl<'a> { ); if output_on_test_finished.write_status_line { - self.write_status_line(*stress_index, *test_instance, describe, writer)?; + self.write_status_line( + *stress_index, + *test_instance, + describe, + current_stats, + writer, + )?; } if output_on_test_finished.show_immediate { self.write_test_execute_status(last_status, false, writer)?; @@ -1130,7 +1173,7 @@ impl<'a> DisplayReporterImpl<'a> { writeln!( writer, "[ ] {}", - self.display_test_instance(stress_index, test_instance) + self.display_test_instance(stress_index, None, test_instance) )?; Ok(()) @@ -1182,6 +1225,7 @@ impl<'a> DisplayReporterImpl<'a> { stress_index: Option, test_instance: TestInstance<'a>, describe: ExecutionDescription<'_>, + current_stats: &RunStats, writer: &mut dyn Write, ) -> io::Result<()> { let last_status = describe.last_status(); @@ -1224,12 +1268,16 @@ impl<'a> DisplayReporterImpl<'a> { } }; - // Print the time taken and the name of the test. + write!( + writer, + "{}", + DisplayBracketedDuration(last_status.time_taken) + )?; + writeln!( writer, - "{}{}", - DisplayBracketedDuration(last_status.time_taken), - self.display_test_instance(stress_index, test_instance.id()) + "{}", + self.display_test_instance(stress_index, Some(current_stats), test_instance.id()) )?; // On Windows, also print out the exception if available. @@ -1319,7 +1367,7 @@ impl<'a> DisplayReporterImpl<'a> { writer, "{}{}", DisplayBracketedDuration(last_status.time_taken), - self.display_test_instance(stress_index, test_instance), + self.display_test_instance(stress_index, None, test_instance), )?; // On Windows, also print out the exception if available. @@ -1338,9 +1386,19 @@ impl<'a> DisplayReporterImpl<'a> { fn display_test_instance( &self, stress_index: Option, + current_stats: Option<&RunStats>, instance: TestInstanceId<'a>, ) -> DisplayTestInstance<'_> { - DisplayTestInstance::new(stress_index, instance, &self.styles.list_styles) + let counter_index = current_stats.and_then(|current_stats| { + (!self.no_counter).then(|| DisplayCounterIndex::new(current_stats)) + }); + + DisplayTestInstance::new( + stress_index, + counter_index, + instance, + &self.styles.list_styles, + ) } fn display_script_instance( @@ -1379,7 +1437,7 @@ impl<'a> DisplayReporterImpl<'a> { // The width to be printed out is index width + total width + 1 for '/' // + 1 for ':' + 1 for the space after that. let count_width = decimal_char_width(index + 1) + decimal_char_width(total) + 3; - let padding = usize::try_from(8_u32.saturating_sub(count_width)).unwrap(); + let padding = 8usize.saturating_sub(count_width); write!( writer, @@ -1440,7 +1498,7 @@ impl<'a> DisplayReporterImpl<'a> { writeln!( writer, "{}", - self.display_test_instance(*stress_index, *test_instance) + self.display_test_instance(*stress_index, None, *test_instance) )?; // We want to show an attached attempt string either if this is @@ -2128,13 +2186,6 @@ impl ThemeCharacters { } } -fn decimal_char_width(n: usize) -> u32 { - // checked_ilog10 returns 0 for 1-9, 1 for 10-99, 2 for 100-999, etc. (And - // None for 0 which we unwrap to the same as 1). Add 1 to it to get the - // actual number of digits. - n.checked_ilog10().unwrap_or(0) + 1 -} - #[cfg(test)] mod tests { use super::*; @@ -2181,7 +2232,7 @@ mod tests { failure_output: Some(TestOutputDisplay::Immediate), should_colorize: false, no_capture: true, - hide_progress_bar: false, + show_progress: ShowProgress::default(), no_output_indent: false, }; let output = ReporterStderr::Buffer(out); diff --git a/nextest-runner/src/reporter/displayer/mod.rs b/nextest-runner/src/reporter/displayer/mod.rs index 71c0de49338..95d2cf358f3 100644 --- a/nextest-runner/src/reporter/displayer/mod.rs +++ b/nextest-runner/src/reporter/displayer/mod.rs @@ -10,5 +10,6 @@ mod status_level; mod unit_output; pub(crate) use imp::*; +pub use progress::ShowProgress; pub use status_level::*; pub use unit_output::*; diff --git a/nextest-runner/src/reporter/displayer/progress.rs b/nextest-runner/src/reporter/displayer/progress.rs index 3491e5f8c19..3e49f7aeb08 100644 --- a/nextest-runner/src/reporter/displayer/progress.rs +++ b/nextest-runner/src/reporter/displayer/progress.rs @@ -15,6 +15,23 @@ use std::{ use swrite::{SWrite, swrite}; use tracing::debug; +/// How to show progress. +#[derive(Default, Clone, Copy, Debug)] +pub enum ShowProgress { + /// Automatically decide based on environment. + #[default] + Auto, + + /// No progress display. + None, + + /// Show a progress bar. + Bar, + + /// Show a counter on each line. + Counter, +} + #[derive(Debug)] pub(super) struct ProgressBarState { bar: ProgressBar, @@ -183,6 +200,10 @@ impl ProgressBarState { || self.hidden_info_response || self.hidden_between_sub_runs } + + pub(super) fn is_hidden(&self) -> bool { + self.bar.is_hidden() + } } /// OSC 9 terminal progress reporting. diff --git a/nextest-runner/src/reporter/imp.rs b/nextest-runner/src/reporter/imp.rs index 8cf596f590d..befc9c734d9 100644 --- a/nextest-runner/src/reporter/imp.rs +++ b/nextest-runner/src/reporter/imp.rs @@ -14,7 +14,10 @@ use crate::{ config::core::EvaluatableProfile, errors::WriteEventError, list::TestList, - reporter::{aggregator::EventAggregator, events::*, structured::StructuredReporter}, + reporter::{ + aggregator::EventAggregator, displayer::ShowProgress, events::*, + structured::StructuredReporter, + }, }; /// Standard error destination for the reporter. @@ -41,7 +44,7 @@ pub struct ReporterBuilder { final_status_level: Option, verbose: bool, - hide_progress_bar: bool, + show_progress: ShowProgress, no_output_indent: bool, } @@ -91,10 +94,9 @@ impl ReporterBuilder { self } - /// Sets visibility of the progress bar. - /// The progress bar is also hidden if `no_capture` is set. - pub fn set_hide_progress_bar(&mut self, hide_progress_bar: bool) -> &mut Self { - self.hide_progress_bar = hide_progress_bar; + /// Sets the way of displaying progress. + pub fn set_show_progress(&mut self, show_progress: ShowProgress) -> &mut Self { + self.show_progress = show_progress; self } @@ -133,7 +135,7 @@ impl ReporterBuilder { failure_output: self.failure_output, should_colorize: self.should_colorize, no_capture: self.no_capture, - hide_progress_bar: self.hide_progress_bar, + show_progress: self.show_progress, no_output_indent: self.no_output_indent, } .build(cargo_configs, output); diff --git a/nextest-runner/src/reporter/mod.rs b/nextest-runner/src/reporter/mod.rs index 106e5f7d551..230383b9273 100644 --- a/nextest-runner/src/reporter/mod.rs +++ b/nextest-runner/src/reporter/mod.rs @@ -13,7 +13,7 @@ mod helpers; mod imp; pub mod structured; -pub use displayer::{FinalStatusLevel, StatusLevel, TestOutputDisplay}; +pub use displayer::{FinalStatusLevel, ShowProgress, StatusLevel, TestOutputDisplay}; pub use error_description::*; pub use helpers::highlight_end; pub use imp::*;