use crate::{
cargo_config::{CargoConfigs, DiscoveredConfig},
helpers::{DisplayTestInstance, plural},
list::TestInstanceId,
reporter::{
displayer::formatters::DisplayBracketedHhMmSs,
events::*,
helpers::{Styles, print_lines_in_chunks},
},
run_mode::NextestRunMode,
};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use nextest_metadata::{RustBinaryId, TestCaseName};
use owo_colors::OwoColorize;
use std::{
cmp::{max, min},
env, fmt,
str::FromStr,
time::{Duration, Instant},
};
use swrite::{SWrite, swrite};
use tracing::debug;
const PROGRESS_REFRESH_RATE_HZ: u8 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MaxProgressRunning {
Count(usize),
Infinite,
}
impl MaxProgressRunning {
pub const DEFAULT_VALUE: Self = Self::Count(8);
}
impl Default for MaxProgressRunning {
fn default() -> Self {
Self::DEFAULT_VALUE
}
}
impl FromStr for MaxProgressRunning {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("infinite") {
return Ok(Self::Infinite);
}
match s.parse::<usize>() {
Err(e) => Err(format!("Error: {e} parsing {s}")),
Ok(n) => Ok(Self::Count(n)),
}
}
}
impl fmt::Display for MaxProgressRunning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Infinite => write!(f, "infinite"),
Self::Count(n) => write!(f, "{n}"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShowProgress {
Auto {
suppress_success: bool,
},
None,
Counter,
Running,
}
impl Default for ShowProgress {
fn default() -> Self {
ShowProgress::Auto {
suppress_success: false,
}
}
}
#[derive(Debug)]
pub(super) enum RunningTestStatus {
Running,
Slow,
Delay(Duration),
Retry,
}
#[derive(Debug)]
pub(super) struct RunningTest {
binary_id: RustBinaryId,
test_name: TestCaseName,
status: RunningTestStatus,
start_time: Instant,
paused_for: Duration,
}
impl RunningTest {
fn message(&self, now: Instant, width: usize, styles: &Styles) -> String {
let mut elapsed = (now - self.start_time).saturating_sub(self.paused_for);
let status = match self.status {
RunningTestStatus::Running => " ".to_owned(),
RunningTestStatus::Slow => " SLOW".style(styles.skip).to_string(),
RunningTestStatus::Delay(d) => {
elapsed = d.saturating_sub(elapsed);
"DELAY".style(styles.retry).to_string()
}
RunningTestStatus::Retry => "RETRY".style(styles.retry).to_string(),
};
let elapsed = format!(
"{:0>2}:{:0>2}:{:0>2}",
elapsed.as_secs() / 3600,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60,
);
let max_width = width.saturating_sub(25);
let test = DisplayTestInstance::new(
None,
None,
TestInstanceId {
binary_id: &self.binary_id,
test_name: &self.test_name,
},
&styles.list_styles,
)
.with_max_width(max_width);
format!(" {} [{:>9}] {}", status, elapsed, test)
}
}
#[derive(Debug)]
pub(super) struct ProgressBarState {
bar: ProgressBar,
mode: NextestRunMode,
stats: RunStats,
running: usize,
max_progress_running: MaxProgressRunning,
max_running_displayed: usize,
running_tests: Option<Vec<RunningTest>>,
buffer: String,
println_chunk_size: usize,
hidden_no_capture: bool,
hidden_run_paused: bool,
hidden_info_response: bool,
}
impl ProgressBarState {
pub(super) fn new(
mode: NextestRunMode,
run_count: usize,
progress_chars: &str,
max_progress_running: MaxProgressRunning,
) -> Self {
let bar = ProgressBar::new(run_count as u64);
let run_count_width = format!("{run_count}").len();
let template = format!(
"{{prefix:>12}} [{{elapsed_precise:>9}}] {{wide_bar}} \
{{pos:>{run_count_width}}}/{{len:{run_count_width}}}: {{msg}}"
);
bar.set_style(
ProgressStyle::default_bar()
.progress_chars(progress_chars)
.template(&template)
.expect("template is known to be valid"),
);
let running_tests =
(!matches!(max_progress_running, MaxProgressRunning::Count(0))).then(Vec::new);
let println_chunk_size = env::var("__NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(4096);
Self {
bar,
mode,
stats: RunStats::default(),
running: 0,
max_progress_running,
max_running_displayed: 0,
running_tests,
buffer: String::new(),
println_chunk_size,
hidden_no_capture: false,
hidden_run_paused: false,
hidden_info_response: false,
}
}
pub(super) fn tick(&mut self, styles: &Styles) {
self.update_message(styles);
self.print_and_clear_buffer();
}
fn print_and_clear_buffer(&mut self) {
self.print_and_force_redraw();
self.buffer.clear();
}
fn print_and_force_redraw(&self) {
if self.buffer.is_empty() {
self.bar.force_draw();
return;
}
print_lines_in_chunks(&self.buffer, self.println_chunk_size, |chunk| {
self.bar.println(chunk);
});
}
fn update_message(&mut self, styles: &Styles) {
let mut msg = self.progress_bar_msg(styles);
msg += " ";
if let Some(running_tests) = &self.running_tests {
let (_, width) = console::Term::stderr().size();
let width = max(width as usize, 40);
let now = Instant::now();
let mut count = match self.max_progress_running {
MaxProgressRunning::Count(count) => min(running_tests.len(), count),
MaxProgressRunning::Infinite => running_tests.len(),
};
for running_test in &running_tests[..count] {
msg.push('\n');
msg.push_str(&running_test.message(now, width, styles));
}
if count < running_tests.len() {
let overflow_count = running_tests.len() - count;
swrite!(
msg,
"\n ... and {} more {} running",
overflow_count.style(styles.count),
plural::tests_str(self.mode, overflow_count),
);
count += 1;
}
self.max_running_displayed = max(self.max_running_displayed, count);
msg.push_str(&"\n".to_string().repeat(self.max_running_displayed - count));
}
self.bar.set_message(msg);
}
fn progress_bar_msg(&self, styles: &Styles) -> String {
progress_bar_msg(&self.stats, self.running, styles)
}
pub(super) fn update_progress_bar(&mut self, event: &TestEvent<'_>, styles: &Styles) {
let before_should_hide = self.should_hide();
match &event.kind {
TestEventKind::StressSubRunStarted { .. } => {
self.bar.reset();
}
TestEventKind::StressSubRunFinished { .. } => {
self.bar.finish_and_clear();
}
TestEventKind::SetupScriptStarted { no_capture, .. } => {
if *no_capture {
self.hidden_no_capture = true;
}
}
TestEventKind::SetupScriptFinished { no_capture, .. } => {
if *no_capture {
self.hidden_no_capture = false;
}
}
TestEventKind::TestStarted {
current_stats,
running,
test_instance,
..
} => {
self.running = *running;
self.stats = *current_stats;
self.bar.set_prefix(progress_bar_prefix(
current_stats,
current_stats.cancel_reason,
styles,
));
self.bar.set_length(current_stats.initial_run_count as u64);
self.bar.set_position(current_stats.finished_count as u64);
if let Some(running_tests) = &mut self.running_tests {
running_tests.push(RunningTest {
binary_id: test_instance.binary_id.clone(),
test_name: test_instance.test_name.to_owned(),
status: RunningTestStatus::Running,
start_time: Instant::now(),
paused_for: Duration::ZERO,
});
}
}
TestEventKind::TestFinished {
current_stats,
running,
test_instance,
..
} => {
self.running = *running;
self.stats = *current_stats;
self.remove_test(test_instance);
self.bar.set_prefix(progress_bar_prefix(
current_stats,
current_stats.cancel_reason,
styles,
));
self.bar.set_length(current_stats.initial_run_count as u64);
self.bar.set_position(current_stats.finished_count as u64);
}
TestEventKind::TestAttemptFailedWillRetry {
test_instance,
delay_before_next_attempt,
..
} => {
self.remove_test(test_instance);
if let Some(running_tests) = &mut self.running_tests {
running_tests.push(RunningTest {
binary_id: test_instance.binary_id.clone(),
test_name: test_instance.test_name.to_owned(),
status: RunningTestStatus::Delay(*delay_before_next_attempt),
start_time: Instant::now(),
paused_for: Duration::ZERO,
});
}
}
TestEventKind::TestRetryStarted { test_instance, .. } => {
self.remove_test(test_instance);
if let Some(running_tests) = &mut self.running_tests {
running_tests.push(RunningTest {
binary_id: test_instance.binary_id.clone(),
test_name: test_instance.test_name.to_owned(),
status: RunningTestStatus::Retry,
start_time: Instant::now(),
paused_for: Duration::ZERO,
});
}
}
TestEventKind::TestSlow { test_instance, .. } => {
if let Some(running_tests) = &mut self.running_tests {
running_tests
.iter_mut()
.find(|rt| {
&rt.binary_id == test_instance.binary_id
&& &rt.test_name == test_instance.test_name
})
.expect("a slow test to be already running")
.status = RunningTestStatus::Slow;
}
}
TestEventKind::InfoStarted { .. } => {
self.hidden_info_response = true;
}
TestEventKind::InfoFinished { .. } => {
self.hidden_info_response = false;
}
TestEventKind::RunPaused { .. } => {
self.hidden_run_paused = true;
}
TestEventKind::RunContinued { .. } => {
self.hidden_run_paused = false;
let current_global_elapsed = self.bar.elapsed();
self.bar.set_elapsed(event.elapsed);
if let Some(running_tests) = &mut self.running_tests {
let delta = current_global_elapsed.saturating_sub(event.elapsed);
for running_test in running_tests {
running_test.paused_for += delta;
}
}
}
TestEventKind::RunBeginCancel {
current_stats,
running,
..
}
| TestEventKind::RunBeginKill {
current_stats,
running,
..
} => {
self.running = *running;
self.stats = *current_stats;
self.bar.set_prefix(progress_bar_cancel_prefix(
current_stats.cancel_reason,
styles,
));
}
_ => {}
}
let after_should_hide = self.should_hide();
match (before_should_hide, after_should_hide) {
(false, true) => self.bar.set_draw_target(Self::hidden_target()),
(true, false) => self.bar.set_draw_target(Self::stderr_target()),
_ => {}
}
}
fn remove_test(&mut self, test_instance: &TestInstanceId) {
if let Some(running_tests) = &mut self.running_tests {
running_tests.remove(
running_tests
.iter()
.position(|e| {
&e.binary_id == test_instance.binary_id
&& &e.test_name == test_instance.test_name
})
.expect("finished test to have started"),
);
}
}
pub(super) fn write_buf(&mut self, buf: &str) {
self.buffer.push_str(buf);
}
#[inline]
pub(super) fn finish_and_clear(&self) {
self.print_and_force_redraw();
self.bar.finish_and_clear();
}
fn stderr_target() -> ProgressDrawTarget {
ProgressDrawTarget::stderr_with_hz(PROGRESS_REFRESH_RATE_HZ)
}
fn hidden_target() -> ProgressDrawTarget {
ProgressDrawTarget::hidden()
}
fn should_hide(&self) -> bool {
self.hidden_no_capture || self.hidden_run_paused || self.hidden_info_response
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ShowTerminalProgress {
Yes,
No,
}
impl ShowTerminalProgress {
const ENV: &str = "CARGO_TERM_PROGRESS_TERM_INTEGRATION";
pub fn from_cargo_configs(configs: &CargoConfigs, is_terminal: bool) -> Self {
for config in configs.discovered_configs() {
match config {
DiscoveredConfig::CliOption { config, source } => {
if let Some(v) = config.term.progress.term_integration {
if v {
debug!("enabling terminal progress reporting based on {source:?}");
return Self::Yes;
} else {
debug!("disabling terminal progress reporting based on {source:?}");
return Self::No;
}
}
}
DiscoveredConfig::Env => {
if let Some(v) = env::var_os(Self::ENV) {
if v == "true" {
debug!(
"enabling terminal progress reporting based on \
CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
);
return Self::Yes;
} else if v == "false" {
debug!(
"disabling terminal progress reporting based on \
CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
);
return Self::No;
} else {
debug!(
"invalid value for CARGO_TERM_PROGRESS_TERM_INTEGRATION \
environment variable: {v:?}, ignoring"
);
}
}
}
DiscoveredConfig::File { config, source } => {
if let Some(v) = config.term.progress.term_integration {
if v {
debug!("enabling terminal progress reporting based on {source:?}");
return Self::Yes;
} else {
debug!("disabling terminal progress reporting based on {source:?}");
return Self::No;
}
}
}
}
}
if supports_osc_9_4(is_terminal) {
Self::Yes
} else {
Self::No
}
}
}
#[derive(Default)]
pub(super) struct TerminalProgress {
last_value: TerminalProgressValue,
}
impl TerminalProgress {
pub(super) fn new(show: ShowTerminalProgress) -> Option<Self> {
match show {
ShowTerminalProgress::Yes => Some(Self::default()),
ShowTerminalProgress::No => None,
}
}
pub(super) fn update_progress(&mut self, event: &TestEvent<'_>) {
let value = match &event.kind {
TestEventKind::RunStarted { .. }
| TestEventKind::StressSubRunStarted { .. }
| TestEventKind::StressSubRunFinished { .. }
| TestEventKind::SetupScriptStarted { .. }
| TestEventKind::SetupScriptSlow { .. }
| TestEventKind::SetupScriptFinished { .. } => TerminalProgressValue::None,
TestEventKind::TestStarted { current_stats, .. }
| TestEventKind::TestFinished { current_stats, .. } => {
let percentage = (current_stats.finished_count as f64
/ current_stats.initial_run_count as f64)
* 100.0;
if current_stats.has_failures() || current_stats.cancel_reason.is_some() {
TerminalProgressValue::Error(percentage)
} else {
TerminalProgressValue::Value(percentage)
}
}
TestEventKind::TestSlow { .. }
| TestEventKind::TestAttemptFailedWillRetry { .. }
| TestEventKind::TestRetryStarted { .. }
| TestEventKind::TestSkipped { .. }
| TestEventKind::InfoStarted { .. }
| TestEventKind::InfoResponse { .. }
| TestEventKind::InfoFinished { .. }
| TestEventKind::InputEnter { .. } => TerminalProgressValue::None,
TestEventKind::RunBeginCancel { current_stats, .. }
| TestEventKind::RunBeginKill { current_stats, .. } => {
let percentage = (current_stats.finished_count as f64
/ current_stats.initial_run_count as f64)
* 100.0;
TerminalProgressValue::Error(percentage)
}
TestEventKind::RunPaused { .. }
| TestEventKind::RunContinued { .. }
| TestEventKind::RunFinished { .. } => {
TerminalProgressValue::Remove
}
};
self.last_value = value;
}
pub(super) fn last_value(&self) -> &TerminalProgressValue {
&self.last_value
}
}
fn supports_osc_9_4(is_terminal: bool) -> bool {
if !is_terminal {
debug!(
"autodetect terminal progress reporting: disabling since \
passed-in stream (usually stderr) is not a terminal"
);
return false;
}
if std::env::var_os("WT_SESSION").is_some() {
debug!("autodetect terminal progress reporting: enabling since WT_SESSION is set");
return true;
};
if std::env::var_os("ConEmuANSI").is_some_and(|term| term == "ON") {
debug!("autodetect terminal progress reporting: enabling since ConEmuANSI is ON");
return true;
}
if let Ok(term) = std::env::var("TERM_PROGRAM")
&& (term == "WezTerm" || term == "ghostty" || term == "iTerm.app")
{
debug!("autodetect terminal progress reporting: enabling since TERM_PROGRAM is {term}");
return true;
}
false
}
#[derive(PartialEq, Debug, Default)]
pub(super) enum TerminalProgressValue {
#[default]
None,
Remove,
Value(f64),
#[expect(dead_code)]
Indeterminate,
Error(f64),
}
impl fmt::Display for TerminalProgressValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (state, progress) = match self {
Self::None => return Ok(()), Self::Remove => (0, 0.0),
Self::Value(v) => (1, *v),
Self::Indeterminate => (3, 0.0),
Self::Error(v) => (2, *v),
};
write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
}
}
pub(super) fn progress_str(
elapsed: Duration,
current_stats: &RunStats,
running: usize,
styles: &Styles,
) -> String {
let mut s = progress_bar_prefix(current_stats, current_stats.cancel_reason, styles);
swrite!(
s,
" {}{}/{}: {}",
DisplayBracketedHhMmSs(elapsed),
current_stats.finished_count,
current_stats.initial_run_count,
progress_bar_msg(current_stats, running, styles)
);
s
}
pub(super) fn write_summary_str(run_stats: &RunStats, styles: &Styles, out: &mut String) {
let &RunStats {
initial_run_count: _,
finished_count: _,
setup_scripts_initial_count: _,
setup_scripts_finished_count: _,
setup_scripts_passed: _,
setup_scripts_failed: _,
setup_scripts_exec_failed: _,
setup_scripts_timed_out: _,
passed,
passed_slow,
passed_timed_out: _,
flaky,
failed,
failed_slow: _,
failed_timed_out,
leaky,
leaky_failed,
exec_failed,
skipped,
cancel_reason: _,
} = run_stats;
swrite!(
out,
"{} {}",
passed.style(styles.count),
"passed".style(styles.pass)
);
if passed_slow > 0 || flaky > 0 || leaky > 0 {
let mut text = Vec::with_capacity(3);
if passed_slow > 0 {
text.push(format!(
"{} {}",
passed_slow.style(styles.count),
"slow".style(styles.skip),
));
}
if flaky > 0 {
text.push(format!(
"{} {}",
flaky.style(styles.count),
"flaky".style(styles.skip),
));
}
if leaky > 0 {
text.push(format!(
"{} {}",
leaky.style(styles.count),
"leaky".style(styles.skip),
));
}
swrite!(out, " ({})", text.join(", "));
}
swrite!(out, ", ");
if failed > 0 {
swrite!(
out,
"{} {}",
failed.style(styles.count),
"failed".style(styles.fail),
);
if leaky_failed > 0 {
swrite!(
out,
" ({} due to being {})",
leaky_failed.style(styles.count),
"leaky".style(styles.fail),
);
}
swrite!(out, ", ");
}
if exec_failed > 0 {
swrite!(
out,
"{} {}, ",
exec_failed.style(styles.count),
"exec failed".style(styles.fail),
);
}
if failed_timed_out > 0 {
swrite!(
out,
"{} {}, ",
failed_timed_out.style(styles.count),
"timed out".style(styles.fail),
);
}
swrite!(
out,
"{} {}",
skipped.style(styles.count),
"skipped".style(styles.skip),
);
}
fn progress_bar_cancel_prefix(reason: Option<CancelReason>, styles: &Styles) -> String {
let status = match reason {
Some(CancelReason::SetupScriptFailure)
| Some(CancelReason::TestFailure)
| Some(CancelReason::ReportError)
| Some(CancelReason::GlobalTimeout)
| Some(CancelReason::TestFailureImmediate)
| Some(CancelReason::Signal)
| Some(CancelReason::Interrupt)
| None => "Cancelling",
Some(CancelReason::SecondSignal) => "Killing",
};
format!("{:>12}", status.style(styles.fail))
}
fn progress_bar_prefix(
run_stats: &RunStats,
cancel_reason: Option<CancelReason>,
styles: &Styles,
) -> String {
if let Some(reason) = cancel_reason {
return progress_bar_cancel_prefix(Some(reason), styles);
}
let style = if run_stats.has_failures() {
styles.fail
} else {
styles.pass
};
format!("{:>12}", "Running".style(style))
}
pub(super) fn progress_bar_msg(
current_stats: &RunStats,
running: usize,
styles: &Styles,
) -> String {
let mut s = format!("{} running, ", running.style(styles.count));
write_summary_str(current_stats, styles, &mut s);
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
config::elements::{FlakyResult, JunitFlakyFailStatus},
output_spec::LiveSpec,
reporter::{TestOutputDisplay, test_helpers::global_slot_assignment},
test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
};
use bytes::Bytes;
use chrono::Local;
#[test]
fn test_progress_bar_prefix() {
let mut styles = Styles::default();
styles.colorize();
for (name, stats) in run_stats_test_failure_examples() {
let prefix = progress_bar_prefix(&stats, Some(CancelReason::TestFailure), &styles);
assert_eq!(
prefix,
" Cancelling".style(styles.fail).to_string(),
"{name} matches"
);
}
for (name, stats) in run_stats_setup_script_failure_examples() {
let prefix =
progress_bar_prefix(&stats, Some(CancelReason::SetupScriptFailure), &styles);
assert_eq!(
prefix,
" Cancelling".style(styles.fail).to_string(),
"{name} matches"
);
}
let prefix = progress_bar_prefix(&RunStats::default(), Some(CancelReason::Signal), &styles);
assert_eq!(prefix, " Cancelling".style(styles.fail).to_string());
let prefix = progress_bar_prefix(&RunStats::default(), None, &styles);
assert_eq!(prefix, " Running".style(styles.pass).to_string());
for (name, stats) in run_stats_test_failure_examples() {
let prefix = progress_bar_prefix(&stats, None, &styles);
assert_eq!(
prefix,
" Running".style(styles.fail).to_string(),
"{name} matches"
);
}
for (name, stats) in run_stats_setup_script_failure_examples() {
let prefix = progress_bar_prefix(&stats, None, &styles);
assert_eq!(
prefix,
" Running".style(styles.fail).to_string(),
"{name} matches"
);
}
}
#[test]
fn progress_str_snapshots() {
let mut styles = Styles::default();
styles.colorize();
let elapsed = Duration::from_secs(123456);
let running = 10;
for (name, stats) in run_stats_test_failure_examples() {
let s = progress_str(elapsed, &stats, running, &styles);
insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
let mut stats = stats;
stats.cancel_reason = None;
let s = progress_str(elapsed, &stats, running, &styles);
insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
}
for (name, stats) in run_stats_setup_script_failure_examples() {
let s = progress_str(elapsed, &stats, running, &styles);
insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
let mut stats = stats;
stats.cancel_reason = None;
let s = progress_str(elapsed, &stats, running, &styles);
insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
}
}
#[test]
fn running_test_snapshots() {
let styles = Styles::default();
let now = Instant::now();
for (name, running_test) in running_test_examples(now) {
let msg = running_test.message(now, 80, &styles);
insta::assert_snapshot!(name, msg);
}
}
fn running_test_examples(now: Instant) -> Vec<(&'static str, RunningTest)> {
let binary_id = RustBinaryId::new("my-binary");
let test_name = TestCaseName::new("test::my_test");
let start_time = now - Duration::from_secs(125);
vec![
(
"running_status",
RunningTest {
binary_id: binary_id.clone(),
test_name: test_name.clone(),
status: RunningTestStatus::Running,
start_time,
paused_for: Duration::ZERO,
},
),
(
"slow_status",
RunningTest {
binary_id: binary_id.clone(),
test_name: test_name.clone(),
status: RunningTestStatus::Slow,
start_time,
paused_for: Duration::ZERO,
},
),
(
"delay_status",
RunningTest {
binary_id: binary_id.clone(),
test_name: test_name.clone(),
status: RunningTestStatus::Delay(Duration::from_secs(130)),
start_time,
paused_for: Duration::ZERO,
},
),
(
"delay_status_underflow",
RunningTest {
binary_id: binary_id.clone(),
test_name: test_name.clone(),
status: RunningTestStatus::Delay(Duration::from_secs(124)),
start_time,
paused_for: Duration::ZERO,
},
),
(
"retry_status",
RunningTest {
binary_id: binary_id.clone(),
test_name: test_name.clone(),
status: RunningTestStatus::Retry,
start_time,
paused_for: Duration::ZERO,
},
),
(
"with_paused_duration",
RunningTest {
binary_id: binary_id.clone(),
test_name: test_name.clone(),
status: RunningTestStatus::Running,
start_time,
paused_for: Duration::from_secs(30),
},
),
]
}
fn run_stats_test_failure_examples() -> Vec<(&'static str, RunStats)> {
vec![
(
"one_failed",
RunStats {
initial_run_count: 20,
finished_count: 1,
failed: 1,
cancel_reason: Some(CancelReason::TestFailure),
..RunStats::default()
},
),
(
"one_failed_one_passed",
RunStats {
initial_run_count: 20,
finished_count: 2,
failed: 1,
passed: 1,
cancel_reason: Some(CancelReason::TestFailure),
..RunStats::default()
},
),
(
"one_exec_failed",
RunStats {
initial_run_count: 20,
finished_count: 10,
exec_failed: 1,
cancel_reason: Some(CancelReason::TestFailure),
..RunStats::default()
},
),
(
"one_timed_out",
RunStats {
initial_run_count: 20,
finished_count: 10,
failed_timed_out: 1,
cancel_reason: Some(CancelReason::TestFailure),
..RunStats::default()
},
),
]
}
fn run_stats_setup_script_failure_examples() -> Vec<(&'static str, RunStats)> {
vec![
(
"one_setup_script_failed",
RunStats {
initial_run_count: 30,
setup_scripts_failed: 1,
cancel_reason: Some(CancelReason::SetupScriptFailure),
..RunStats::default()
},
),
(
"one_setup_script_exec_failed",
RunStats {
initial_run_count: 35,
setup_scripts_exec_failed: 1,
cancel_reason: Some(CancelReason::SetupScriptFailure),
..RunStats::default()
},
),
(
"one_setup_script_timed_out",
RunStats {
initial_run_count: 40,
setup_scripts_timed_out: 1,
cancel_reason: Some(CancelReason::SetupScriptFailure),
..RunStats::default()
},
),
]
}
#[test]
fn update_progress_bar_updates_stats() {
let styles = Styles::default();
let binary_id = RustBinaryId::new("test-binary");
let test_name = TestCaseName::new("test_name");
let mut state = ProgressBarState::new(
NextestRunMode::Test,
10,
"=> ",
MaxProgressRunning::default(),
);
assert_eq!(state.stats, RunStats::default());
assert_eq!(state.running, 0);
let started_stats = RunStats {
initial_run_count: 10,
passed: 5,
..RunStats::default()
};
let started_event = TestEvent {
timestamp: Local::now().fixed_offset(),
elapsed: Duration::ZERO,
kind: TestEventKind::TestStarted {
stress_index: None,
test_instance: TestInstanceId {
binary_id: &binary_id,
test_name: &test_name,
},
slot_assignment: global_slot_assignment(0),
current_stats: started_stats,
running: 3,
command_line: vec![],
},
};
state.update_progress_bar(&started_event, &styles);
assert_eq!(
state.stats, started_stats,
"stats should be updated on TestStarted"
);
assert_eq!(state.running, 3, "running should be updated on TestStarted");
let msg = state.progress_bar_msg(&styles);
insta::assert_snapshot!(msg, @"3 running, 5 passed, 0 skipped");
let finished_stats = RunStats {
initial_run_count: 10,
finished_count: 1,
passed: 8,
..RunStats::default()
};
let finished_event = TestEvent {
timestamp: Local::now().fixed_offset(),
elapsed: Duration::ZERO,
kind: TestEventKind::TestFinished {
stress_index: None,
test_instance: TestInstanceId {
binary_id: &binary_id,
test_name: &test_name,
},
success_output: TestOutputDisplay::Never,
failure_output: TestOutputDisplay::Never,
junit_store_success_output: false,
junit_store_failure_output: false,
junit_flaky_fail_status: JunitFlakyFailStatus::default(),
run_statuses: ExecutionStatuses::new(
vec![ExecuteStatus {
retry_data: RetryData {
attempt: 1,
total_attempts: 1,
},
output: make_test_output(),
result: ExecutionResultDescription::Pass,
start_time: Local::now().fixed_offset(),
time_taken: Duration::from_secs(1),
is_slow: false,
delay_before_start: Duration::ZERO,
error_summary: None,
output_error_slice: None,
}],
FlakyResult::default(),
),
current_stats: finished_stats,
running: 2,
},
};
state.update_progress_bar(&finished_event, &styles);
assert_eq!(
state.stats, finished_stats,
"stats should be updated on TestFinished"
);
assert_eq!(
state.running, 2,
"running should be updated on TestFinished"
);
let msg = state.progress_bar_msg(&styles);
insta::assert_snapshot!(msg, @"2 running, 8 passed, 0 skipped");
let cancel_stats = RunStats {
initial_run_count: 10,
finished_count: 3,
passed: 2,
failed: 1,
cancel_reason: Some(CancelReason::TestFailure),
..RunStats::default()
};
let cancel_event = TestEvent {
timestamp: Local::now().fixed_offset(),
elapsed: Duration::ZERO,
kind: TestEventKind::RunBeginCancel {
setup_scripts_running: 0,
current_stats: cancel_stats,
running: 4,
},
};
state.update_progress_bar(&cancel_event, &styles);
assert_eq!(
state.stats, cancel_stats,
"stats should be updated on RunBeginCancel"
);
assert_eq!(
state.running, 4,
"running should be updated on RunBeginCancel"
);
let msg = state.progress_bar_msg(&styles);
insta::assert_snapshot!(msg, @"4 running, 2 passed, 1 failed, 0 skipped");
let kill_stats = RunStats {
initial_run_count: 10,
finished_count: 5,
passed: 3,
failed: 2,
cancel_reason: Some(CancelReason::Signal),
..RunStats::default()
};
let kill_event = TestEvent {
timestamp: Local::now().fixed_offset(),
elapsed: Duration::ZERO,
kind: TestEventKind::RunBeginKill {
setup_scripts_running: 0,
current_stats: kill_stats,
running: 2,
},
};
state.update_progress_bar(&kill_event, &styles);
assert_eq!(
state.stats, kill_stats,
"stats should be updated on RunBeginKill"
);
assert_eq!(
state.running, 2,
"running should be updated on RunBeginKill"
);
let msg = state.progress_bar_msg(&styles);
insta::assert_snapshot!(msg, @"2 running, 3 passed, 2 failed, 0 skipped");
}
fn make_test_output() -> ChildExecutionOutputDescription<LiveSpec> {
ChildExecutionOutput::Output {
result: Some(ExecutionResult::Pass),
output: ChildOutput::Split(ChildSplitOutput {
stdout: Some(Bytes::new().into()),
stderr: Some(Bytes::new().into()),
}),
errors: None,
}
.into()
}
}