1use crate::{
5 cargo_config::{CargoConfigs, DiscoveredConfig},
6 helpers::{DisplayTestInstance, plural},
7 list::TestInstanceId,
8 reporter::{
9 displayer::formatters::DisplayBracketedHhMmSs,
10 events::*,
11 helpers::{Styles, print_lines_in_chunks},
12 },
13 run_mode::NextestRunMode,
14};
15use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
16use nextest_metadata::{RustBinaryId, TestCaseName};
17use owo_colors::OwoColorize;
18use std::{
19 cmp::{max, min},
20 env, fmt,
21 str::FromStr,
22 time::{Duration, Instant},
23};
24use swrite::{SWrite, swrite};
25use tracing::debug;
26
27const PROGRESS_REFRESH_RATE_HZ: u8 = 1;
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum MaxProgressRunning {
44 Count(usize),
47
48 Infinite,
50}
51
52#[cfg(feature = "config-schema")]
53impl schemars::JsonSchema for MaxProgressRunning {
54 fn schema_name() -> std::borrow::Cow<'static, str> {
55 "MaxProgressRunning".into()
56 }
57
58 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
59 schemars::json_schema!({
60 "title": "MaxProgressRunning",
61 "description":
62 "Maximum number of running tests to display: a non-negative integer, \
63 or \"infinite\" for no limit.",
64 "oneOf": [
65 { "type": "integer", "minimum": 0 },
66 { "type": "string", "enum": ["infinite"] }
67 ]
68 })
69 }
70}
71
72impl MaxProgressRunning {
73 pub const DEFAULT_VALUE: Self = Self::Count(8);
75}
76
77impl Default for MaxProgressRunning {
78 fn default() -> Self {
79 Self::DEFAULT_VALUE
80 }
81}
82
83impl FromStr for MaxProgressRunning {
84 type Err = String;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 if s.eq_ignore_ascii_case("infinite") {
88 return Ok(Self::Infinite);
89 }
90
91 match s.parse::<usize>() {
92 Err(e) => Err(format!("Error: {e} parsing {s}")),
93 Ok(n) => Ok(Self::Count(n)),
94 }
95 }
96}
97
98impl fmt::Display for MaxProgressRunning {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::Infinite => write!(f, "infinite"),
102 Self::Count(n) => write!(f, "{n}"),
103 }
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum ShowProgress {
113 Auto {
121 suppress_success: bool,
124 },
125
126 None,
128
129 Counter,
131
132 Running,
134}
135
136impl Default for ShowProgress {
137 fn default() -> Self {
138 ShowProgress::Auto {
139 suppress_success: false,
140 }
141 }
142}
143
144#[derive(Debug)]
145pub(super) enum RunningTestStatus {
146 Running,
147 Slow,
148 Delay(Duration),
149 Retry,
150}
151
152#[derive(Debug)]
153pub(super) struct RunningTest {
154 binary_id: RustBinaryId,
155 test_name: TestCaseName,
156 status: RunningTestStatus,
157 start_time: Instant,
158 paused_for: Duration,
159}
160
161impl RunningTest {
162 fn message(&self, now: Instant, width: usize, styles: &Styles) -> String {
163 let mut elapsed = (now - self.start_time).saturating_sub(self.paused_for);
164 let status = match self.status {
165 RunningTestStatus::Running => " ".to_owned(),
166 RunningTestStatus::Slow => " SLOW".style(styles.skip).to_string(),
167 RunningTestStatus::Delay(d) => {
168 elapsed = d.saturating_sub(elapsed);
172 "DELAY".style(styles.retry).to_string()
173 }
174 RunningTestStatus::Retry => "RETRY".style(styles.retry).to_string(),
175 };
176 let elapsed = format!(
177 "{:0>2}:{:0>2}:{:0>2}",
178 elapsed.as_secs() / 3600,
179 elapsed.as_secs() / 60,
180 elapsed.as_secs() % 60,
181 );
182 let max_width = width.saturating_sub(25);
183 let test = DisplayTestInstance::new(
184 None,
185 None,
186 TestInstanceId {
187 binary_id: &self.binary_id,
188
189 test_name: &self.test_name,
190 },
191 &styles.list_styles,
192 )
193 .with_max_width(max_width);
194 format!(" {} [{:>9}] {}", status, elapsed, test)
195 }
196}
197
198#[derive(Debug)]
199pub(super) struct ProgressBarState {
200 bar: ProgressBar,
201 mode: NextestRunMode,
202 stats: RunStats,
203 running: usize,
204 max_progress_running: MaxProgressRunning,
205 max_running_displayed: usize,
209 running_tests: Option<Vec<RunningTest>>,
211 buffer: String,
212 println_chunk_size: usize,
215 hidden_no_capture: bool,
225 hidden_run_paused: bool,
226 hidden_info_response: bool,
227}
228
229impl ProgressBarState {
230 pub(super) fn new(
231 mode: NextestRunMode,
232 run_count: usize,
233 progress_chars: &str,
234 max_progress_running: MaxProgressRunning,
235 ) -> Self {
236 let bar = ProgressBar::new(run_count as u64);
237 let run_count_width = format!("{run_count}").len();
238 let template = format!(
243 "{{prefix:>12}} [{{elapsed_precise:>9}}] {{wide_bar}} \
244 {{pos:>{run_count_width}}}/{{len:{run_count_width}}}: {{msg}}"
245 );
246 bar.set_style(
247 ProgressStyle::default_bar()
248 .progress_chars(progress_chars)
249 .template(&template)
250 .expect("template is known to be valid"),
251 );
252
253 let running_tests =
254 (!matches!(max_progress_running, MaxProgressRunning::Count(0))).then(Vec::new);
255
256 let println_chunk_size = env::var("__NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE")
260 .ok()
261 .and_then(|s| s.parse::<usize>().ok())
262 .unwrap_or(4096);
263
264 Self {
265 bar,
266 mode,
267 stats: RunStats::default(),
268 running: 0,
269 max_progress_running,
270 max_running_displayed: 0,
271 running_tests,
272 buffer: String::new(),
273 println_chunk_size,
274 hidden_no_capture: false,
275 hidden_run_paused: false,
276 hidden_info_response: false,
277 }
278 }
279
280 pub(super) fn tick(&mut self, styles: &Styles) {
281 self.update_message(styles);
282 self.print_and_clear_buffer();
283 }
284
285 fn print_and_clear_buffer(&mut self) {
286 self.print_and_force_redraw();
287 self.buffer.clear();
288 }
289
290 fn print_and_force_redraw(&self) {
292 if self.buffer.is_empty() {
293 self.bar.force_draw();
296 return;
297 }
298
299 print_lines_in_chunks(&self.buffer, self.println_chunk_size, |chunk| {
313 self.bar.println(chunk);
314 });
315 }
316
317 fn update_message(&mut self, styles: &Styles) {
318 let mut msg = self.progress_bar_msg(styles);
319 msg += " ";
320
321 if let Some(running_tests) = &self.running_tests {
322 let (_, width) = console::Term::stderr().size();
323 let width = max(width as usize, 40);
324 let now = Instant::now();
325 let mut count = match self.max_progress_running {
326 MaxProgressRunning::Count(count) => min(running_tests.len(), count),
327 MaxProgressRunning::Infinite => running_tests.len(),
328 };
329 for running_test in &running_tests[..count] {
330 msg.push('\n');
331 msg.push_str(&running_test.message(now, width, styles));
332 }
333 if count < running_tests.len() {
334 let overflow_count = running_tests.len() - count;
335 swrite!(
336 msg,
337 "\n ... and {} more {} running",
338 overflow_count.style(styles.count),
339 plural::tests_str(self.mode, overflow_count),
340 );
341 count += 1;
342 }
343 self.max_running_displayed = max(self.max_running_displayed, count);
344 msg.push_str(&"\n".to_string().repeat(self.max_running_displayed - count));
345 }
346 self.bar.set_message(msg);
347 }
348
349 fn progress_bar_msg(&self, styles: &Styles) -> String {
350 progress_bar_msg(&self.stats, self.running, styles)
351 }
352
353 pub(super) fn update_progress_bar(&mut self, event: &TestEvent<'_>, styles: &Styles) {
354 let before_should_hide = self.should_hide();
355
356 match &event.kind {
357 TestEventKind::StressSubRunStarted { .. } => {
358 self.bar.reset();
359 }
360 TestEventKind::StressSubRunFinished { .. } => {
361 self.bar.finish_and_clear();
364 }
365 TestEventKind::SetupScriptStarted { no_capture, .. } if *no_capture => {
367 self.hidden_no_capture = true;
368 }
369 TestEventKind::SetupScriptFinished { no_capture, .. } if *no_capture => {
371 self.hidden_no_capture = false;
372 }
373 TestEventKind::TestStarted {
374 current_stats,
375 running,
376 test_instance,
377 ..
378 } => {
379 self.running = *running;
380 self.stats = *current_stats;
381
382 self.bar.set_prefix(progress_bar_prefix(
383 current_stats,
384 current_stats.cancel_reason,
385 styles,
386 ));
387 self.bar.set_length(current_stats.initial_run_count as u64);
388 self.bar.set_position(current_stats.finished_count as u64);
389
390 if let Some(running_tests) = &mut self.running_tests {
391 running_tests.push(RunningTest {
392 binary_id: test_instance.binary_id.clone(),
393 test_name: test_instance.test_name.to_owned(),
394 status: RunningTestStatus::Running,
395 start_time: Instant::now(),
396 paused_for: Duration::ZERO,
397 });
398 }
399 }
400 TestEventKind::TestFinished {
401 current_stats,
402 running,
403 test_instance,
404 ..
405 } => {
406 self.running = *running;
407 self.stats = *current_stats;
408 self.remove_test(test_instance);
409
410 self.bar.set_prefix(progress_bar_prefix(
411 current_stats,
412 current_stats.cancel_reason,
413 styles,
414 ));
415 self.bar.set_length(current_stats.initial_run_count as u64);
416 self.bar.set_position(current_stats.finished_count as u64);
417 }
418 TestEventKind::TestAttemptFailedWillRetry {
419 test_instance,
420 delay_before_next_attempt,
421 ..
422 } => {
423 self.remove_test(test_instance);
424 if let Some(running_tests) = &mut self.running_tests {
425 running_tests.push(RunningTest {
426 binary_id: test_instance.binary_id.clone(),
427 test_name: test_instance.test_name.to_owned(),
428 status: RunningTestStatus::Delay(*delay_before_next_attempt),
429 start_time: Instant::now(),
430 paused_for: Duration::ZERO,
431 });
432 }
433 }
434 TestEventKind::TestRetryStarted { test_instance, .. } => {
435 self.remove_test(test_instance);
436 if let Some(running_tests) = &mut self.running_tests {
437 running_tests.push(RunningTest {
438 binary_id: test_instance.binary_id.clone(),
439 test_name: test_instance.test_name.to_owned(),
440 status: RunningTestStatus::Retry,
441 start_time: Instant::now(),
442 paused_for: Duration::ZERO,
443 });
444 }
445 }
446 TestEventKind::TestSlow { test_instance, .. } => {
447 if let Some(running_tests) = &mut self.running_tests {
448 running_tests
449 .iter_mut()
450 .find(|rt| {
451 &rt.binary_id == test_instance.binary_id
452 && &rt.test_name == test_instance.test_name
453 })
454 .expect("a slow test to be already running")
455 .status = RunningTestStatus::Slow;
456 }
457 }
458 TestEventKind::InfoStarted { .. } => {
459 self.hidden_info_response = true;
462 }
463 TestEventKind::InfoFinished { .. } => {
464 self.hidden_info_response = false;
466 }
467 TestEventKind::RunPaused { .. } => {
468 self.hidden_run_paused = true;
471 }
472 TestEventKind::RunContinued { .. } => {
473 self.hidden_run_paused = false;
476 let current_global_elapsed = self.bar.elapsed();
477 self.bar.set_elapsed(event.elapsed);
478
479 if let Some(running_tests) = &mut self.running_tests {
480 let delta = current_global_elapsed.saturating_sub(event.elapsed);
481 for running_test in running_tests {
482 running_test.paused_for += delta;
483 }
484 }
485 }
486 TestEventKind::RunBeginCancel {
487 current_stats,
488 running,
489 ..
490 }
491 | TestEventKind::RunBeginKill {
492 current_stats,
493 running,
494 ..
495 } => {
496 self.running = *running;
497 self.stats = *current_stats;
498 self.bar.set_prefix(progress_bar_cancel_prefix(
499 current_stats.cancel_reason,
500 styles,
501 ));
502 }
503 _ => {}
504 }
505
506 let after_should_hide = self.should_hide();
507
508 match (before_should_hide, after_should_hide) {
509 (false, true) => self.bar.set_draw_target(Self::hidden_target()),
510 (true, false) => self.bar.set_draw_target(Self::stderr_target()),
511 _ => {}
512 }
513 }
514
515 fn remove_test(&mut self, test_instance: &TestInstanceId) {
516 if let Some(running_tests) = &mut self.running_tests {
517 running_tests.remove(
518 running_tests
519 .iter()
520 .position(|e| {
521 &e.binary_id == test_instance.binary_id
522 && &e.test_name == test_instance.test_name
523 })
524 .expect("finished test to have started"),
525 );
526 }
527 }
528
529 pub(super) fn write_buf(&mut self, buf: &str) {
530 self.buffer.push_str(buf);
531 }
532
533 #[inline]
534 pub(super) fn finish_and_clear(&self) {
535 self.print_and_force_redraw();
536 self.bar.finish_and_clear();
537 }
538
539 fn stderr_target() -> ProgressDrawTarget {
540 ProgressDrawTarget::stderr_with_hz(PROGRESS_REFRESH_RATE_HZ)
541 }
542
543 fn hidden_target() -> ProgressDrawTarget {
544 ProgressDrawTarget::hidden()
545 }
546
547 fn should_hide(&self) -> bool {
548 self.hidden_no_capture || self.hidden_run_paused || self.hidden_info_response
549 }
550}
551
552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
554pub enum ShowTerminalProgress {
555 Yes,
557
558 No,
560}
561
562impl ShowTerminalProgress {
563 const ENV: &str = "CARGO_TERM_PROGRESS_TERM_INTEGRATION";
564
565 pub fn from_cargo_configs(configs: &CargoConfigs, is_terminal: bool) -> Self {
568 for config in configs.discovered_configs() {
570 match config {
571 DiscoveredConfig::CliOption { config, source } => {
572 if let Some(v) = config.term.progress.term_integration {
573 if v {
574 debug!("enabling terminal progress reporting based on {source:?}");
575 return Self::Yes;
576 } else {
577 debug!("disabling terminal progress reporting based on {source:?}");
578 return Self::No;
579 }
580 }
581 }
582 DiscoveredConfig::Env => {
583 if let Some(v) = env::var_os(Self::ENV) {
584 if v == "true" {
585 debug!(
586 "enabling terminal progress reporting based on \
587 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
588 );
589 return Self::Yes;
590 } else if v == "false" {
591 debug!(
592 "disabling terminal progress reporting based on \
593 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
594 );
595 return Self::No;
596 } else {
597 debug!(
598 "invalid value for CARGO_TERM_PROGRESS_TERM_INTEGRATION \
599 environment variable: {v:?}, ignoring"
600 );
601 }
602 }
603 }
604 DiscoveredConfig::File { config, source } => {
605 if let Some(v) = config.term.progress.term_integration {
606 if v {
607 debug!("enabling terminal progress reporting based on {source:?}");
608 return Self::Yes;
609 } else {
610 debug!("disabling terminal progress reporting based on {source:?}");
611 return Self::No;
612 }
613 }
614 }
615 }
616 }
617
618 if supports_osc_9_4(is_terminal) {
619 Self::Yes
620 } else {
621 Self::No
622 }
623 }
624}
625
626#[derive(Default)]
628pub(super) struct TerminalProgress {
629 last_value: TerminalProgressValue,
630}
631
632impl TerminalProgress {
633 pub(super) fn new(show: ShowTerminalProgress) -> Option<Self> {
634 match show {
635 ShowTerminalProgress::Yes => Some(Self::default()),
636 ShowTerminalProgress::No => None,
637 }
638 }
639
640 pub(super) fn update_progress(&mut self, event: &TestEvent<'_>) {
641 let value = match &event.kind {
642 TestEventKind::RunStarted { .. }
643 | TestEventKind::StressSubRunStarted { .. }
644 | TestEventKind::StressSubRunFinished { .. }
645 | TestEventKind::SetupScriptStarted { .. }
646 | TestEventKind::SetupScriptSlow { .. }
647 | TestEventKind::SetupScriptFinished { .. } => TerminalProgressValue::None,
648 TestEventKind::TestStarted { current_stats, .. }
649 | TestEventKind::TestFinished { current_stats, .. } => {
650 let percentage = (current_stats.finished_count as f64
651 / current_stats.initial_run_count as f64)
652 * 100.0;
653 if current_stats.has_failures() || current_stats.cancel_reason.is_some() {
654 TerminalProgressValue::Error(percentage)
655 } else {
656 TerminalProgressValue::Value(percentage)
657 }
658 }
659 TestEventKind::TestSlow { .. }
660 | TestEventKind::TestAttemptFailedWillRetry { .. }
661 | TestEventKind::TestRetryStarted { .. }
662 | TestEventKind::TestSkipped { .. }
663 | TestEventKind::InfoStarted { .. }
664 | TestEventKind::InfoResponse { .. }
665 | TestEventKind::InfoFinished { .. }
666 | TestEventKind::InputEnter { .. } => TerminalProgressValue::None,
667 TestEventKind::RunBeginCancel { current_stats, .. }
668 | TestEventKind::RunBeginKill { current_stats, .. } => {
669 let percentage = (current_stats.finished_count as f64
671 / current_stats.initial_run_count as f64)
672 * 100.0;
673 TerminalProgressValue::Error(percentage)
674 }
675 TestEventKind::RunPaused { .. }
676 | TestEventKind::RunContinued { .. }
677 | TestEventKind::RunFinished { .. } => {
678 TerminalProgressValue::Remove
681 }
682 };
683
684 self.last_value = value;
685 }
686
687 pub(super) fn last_value(&self) -> &TerminalProgressValue {
688 &self.last_value
689 }
690}
691
692fn supports_osc_9_4(is_terminal: bool) -> bool {
694 if !is_terminal {
695 debug!(
696 "autodetect terminal progress reporting: disabling since \
697 passed-in stream (usually stderr) is not a terminal"
698 );
699 return false;
700 }
701 if std::env::var_os("WT_SESSION").is_some() {
702 debug!("autodetect terminal progress reporting: enabling since WT_SESSION is set");
703 return true;
704 };
705 if std::env::var_os("ConEmuANSI").is_some_and(|term| term == "ON") {
706 debug!("autodetect terminal progress reporting: enabling since ConEmuANSI is ON");
707 return true;
708 }
709 if let Ok(term) = std::env::var("TERM_PROGRAM")
710 && (term == "WezTerm" || term == "ghostty" || term == "iTerm.app")
711 {
712 debug!("autodetect terminal progress reporting: enabling since TERM_PROGRAM is {term}");
713 return true;
714 }
715
716 false
717}
718
719#[derive(PartialEq, Debug, Default)]
723pub(super) enum TerminalProgressValue {
724 #[default]
726 None,
727 Remove,
729 Value(f64),
731 #[expect(dead_code)]
735 Indeterminate,
736 Error(f64),
738}
739
740impl fmt::Display for TerminalProgressValue {
741 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742 let (state, progress) = match self {
750 Self::None => return Ok(()), Self::Remove => (0, 0.0),
752 Self::Value(v) => (1, *v),
753 Self::Indeterminate => (3, 0.0),
754 Self::Error(v) => (2, *v),
755 };
756 write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
757 }
758}
759
760pub(super) fn progress_str(
762 elapsed: Duration,
763 current_stats: &RunStats,
764 running: usize,
765 styles: &Styles,
766) -> String {
767 let mut s = progress_bar_prefix(current_stats, current_stats.cancel_reason, styles);
769
770 swrite!(
772 s,
773 " {}{}/{}: {}",
774 DisplayBracketedHhMmSs(elapsed),
775 current_stats.finished_count,
776 current_stats.initial_run_count,
777 progress_bar_msg(current_stats, running, styles)
778 );
779
780 s
781}
782
783pub(super) fn write_summary_str(run_stats: &RunStats, styles: &Styles, out: &mut String) {
784 let &RunStats {
786 initial_run_count: _,
787 finished_count: _,
788 setup_scripts_initial_count: _,
789 setup_scripts_finished_count: _,
790 setup_scripts_passed: _,
791 setup_scripts_failed: _,
792 setup_scripts_exec_failed: _,
793 setup_scripts_timed_out: _,
794 passed,
795 passed_slow,
796 passed_timed_out: _,
797 flaky,
798 failed,
799 failed_slow: _,
800 failed_timed_out,
801 leaky,
802 leaky_failed,
803 exec_failed,
804 skipped,
805 cancel_reason: _,
806 } = run_stats;
807
808 swrite!(
809 out,
810 "{} {}",
811 passed.style(styles.count),
812 "passed".style(styles.pass)
813 );
814
815 if passed_slow > 0 || flaky > 0 || leaky > 0 {
816 let mut text = Vec::with_capacity(3);
817 if passed_slow > 0 {
818 text.push(format!(
819 "{} {}",
820 passed_slow.style(styles.count),
821 "slow".style(styles.skip),
822 ));
823 }
824 if flaky > 0 {
825 text.push(format!(
826 "{} {}",
827 flaky.style(styles.count),
828 "flaky".style(styles.skip),
829 ));
830 }
831 if leaky > 0 {
832 text.push(format!(
833 "{} {}",
834 leaky.style(styles.count),
835 "leaky".style(styles.skip),
836 ));
837 }
838 swrite!(out, " ({})", text.join(", "));
839 }
840 swrite!(out, ", ");
841
842 if failed > 0 {
843 swrite!(
844 out,
845 "{} {}",
846 failed.style(styles.count),
847 "failed".style(styles.fail),
848 );
849 if leaky_failed > 0 {
850 swrite!(
851 out,
852 " ({} due to being {})",
853 leaky_failed.style(styles.count),
854 "leaky".style(styles.fail),
855 );
856 }
857 swrite!(out, ", ");
858 }
859
860 if exec_failed > 0 {
861 swrite!(
862 out,
863 "{} {}, ",
864 exec_failed.style(styles.count),
865 "exec failed".style(styles.fail),
866 );
867 }
868
869 if failed_timed_out > 0 {
870 swrite!(
871 out,
872 "{} {}, ",
873 failed_timed_out.style(styles.count),
874 "timed out".style(styles.fail),
875 );
876 }
877
878 swrite!(
879 out,
880 "{} {}",
881 skipped.style(styles.count),
882 "skipped".style(styles.skip),
883 );
884}
885
886fn progress_bar_cancel_prefix(reason: Option<CancelReason>, styles: &Styles) -> String {
887 let status = match reason {
888 Some(CancelReason::SetupScriptFailure)
889 | Some(CancelReason::TestFailure)
890 | Some(CancelReason::ReportError)
891 | Some(CancelReason::GlobalTimeout)
892 | Some(CancelReason::TestFailureImmediate)
893 | Some(CancelReason::Signal)
894 | Some(CancelReason::Interrupt)
895 | None => "Cancelling",
896 Some(CancelReason::SecondSignal) => "Killing",
897 };
898 format!("{:>12}", status.style(styles.fail))
899}
900
901fn progress_bar_prefix(
902 run_stats: &RunStats,
903 cancel_reason: Option<CancelReason>,
904 styles: &Styles,
905) -> String {
906 if let Some(reason) = cancel_reason {
907 return progress_bar_cancel_prefix(Some(reason), styles);
908 }
909
910 let style = if run_stats.has_failures() {
911 styles.fail
912 } else {
913 styles.pass
914 };
915
916 format!("{:>12}", "Running".style(style))
917}
918
919pub(super) fn progress_bar_msg(
920 current_stats: &RunStats,
921 running: usize,
922 styles: &Styles,
923) -> String {
924 let mut s = format!("{} running, ", running.style(styles.count));
925 write_summary_str(current_stats, styles, &mut s);
926 s
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use crate::{
933 config::elements::{FlakyResult, JunitFlakyFailStatus},
934 output_spec::LiveSpec,
935 reporter::{TestOutputDisplay, test_helpers::global_slot_assignment},
936 test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
937 };
938 use bytes::Bytes;
939 use chrono::Local;
940
941 #[test]
942 fn test_progress_bar_prefix() {
943 let mut styles = Styles::default();
944 styles.colorize();
945
946 for (name, stats) in run_stats_test_failure_examples() {
947 let prefix = progress_bar_prefix(&stats, Some(CancelReason::TestFailure), &styles);
948 assert_eq!(
949 prefix,
950 " Cancelling".style(styles.fail).to_string(),
951 "{name} matches"
952 );
953 }
954 for (name, stats) in run_stats_setup_script_failure_examples() {
955 let prefix =
956 progress_bar_prefix(&stats, Some(CancelReason::SetupScriptFailure), &styles);
957 assert_eq!(
958 prefix,
959 " Cancelling".style(styles.fail).to_string(),
960 "{name} matches"
961 );
962 }
963
964 let prefix = progress_bar_prefix(&RunStats::default(), Some(CancelReason::Signal), &styles);
965 assert_eq!(prefix, " Cancelling".style(styles.fail).to_string());
966
967 let prefix = progress_bar_prefix(&RunStats::default(), None, &styles);
968 assert_eq!(prefix, " Running".style(styles.pass).to_string());
969
970 for (name, stats) in run_stats_test_failure_examples() {
971 let prefix = progress_bar_prefix(&stats, None, &styles);
972 assert_eq!(
973 prefix,
974 " Running".style(styles.fail).to_string(),
975 "{name} matches"
976 );
977 }
978 for (name, stats) in run_stats_setup_script_failure_examples() {
979 let prefix = progress_bar_prefix(&stats, None, &styles);
980 assert_eq!(
981 prefix,
982 " Running".style(styles.fail).to_string(),
983 "{name} matches"
984 );
985 }
986 }
987
988 #[test]
989 fn progress_str_snapshots() {
990 let mut styles = Styles::default();
991 styles.colorize();
992
993 let elapsed = Duration::from_secs(123456);
995 let running = 10;
996
997 for (name, stats) in run_stats_test_failure_examples() {
998 let s = progress_str(elapsed, &stats, running, &styles);
999 insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
1000
1001 let mut stats = stats;
1002 stats.cancel_reason = None;
1003 let s = progress_str(elapsed, &stats, running, &styles);
1004 insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
1005 }
1006
1007 for (name, stats) in run_stats_setup_script_failure_examples() {
1008 let s = progress_str(elapsed, &stats, running, &styles);
1009 insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
1010
1011 let mut stats = stats;
1012 stats.cancel_reason = None;
1013 let s = progress_str(elapsed, &stats, running, &styles);
1014 insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
1015 }
1016 }
1017
1018 #[test]
1019 fn running_test_snapshots() {
1020 let styles = Styles::default();
1021 let now = Instant::now();
1022
1023 for (name, running_test) in running_test_examples(now) {
1024 let msg = running_test.message(now, 80, &styles);
1025 insta::assert_snapshot!(name, msg);
1026 }
1027 }
1028
1029 fn running_test_examples(now: Instant) -> Vec<(&'static str, RunningTest)> {
1030 let binary_id = RustBinaryId::new("my-binary");
1031 let test_name = TestCaseName::new("test::my_test");
1032 let start_time = now - Duration::from_secs(125); vec![
1035 (
1036 "running_status",
1037 RunningTest {
1038 binary_id: binary_id.clone(),
1039 test_name: test_name.clone(),
1040 status: RunningTestStatus::Running,
1041 start_time,
1042 paused_for: Duration::ZERO,
1043 },
1044 ),
1045 (
1046 "slow_status",
1047 RunningTest {
1048 binary_id: binary_id.clone(),
1049 test_name: test_name.clone(),
1050 status: RunningTestStatus::Slow,
1051 start_time,
1052 paused_for: Duration::ZERO,
1053 },
1054 ),
1055 (
1056 "delay_status",
1057 RunningTest {
1058 binary_id: binary_id.clone(),
1059 test_name: test_name.clone(),
1060 status: RunningTestStatus::Delay(Duration::from_secs(130)),
1061 start_time,
1062 paused_for: Duration::ZERO,
1063 },
1064 ),
1065 (
1066 "delay_status_underflow",
1067 RunningTest {
1068 binary_id: binary_id.clone(),
1069 test_name: test_name.clone(),
1070 status: RunningTestStatus::Delay(Duration::from_secs(124)),
1071 start_time,
1072 paused_for: Duration::ZERO,
1073 },
1074 ),
1075 (
1076 "retry_status",
1077 RunningTest {
1078 binary_id: binary_id.clone(),
1079 test_name: test_name.clone(),
1080 status: RunningTestStatus::Retry,
1081 start_time,
1082 paused_for: Duration::ZERO,
1083 },
1084 ),
1085 (
1086 "with_paused_duration",
1087 RunningTest {
1088 binary_id: binary_id.clone(),
1089 test_name: test_name.clone(),
1090 status: RunningTestStatus::Running,
1091 start_time,
1092 paused_for: Duration::from_secs(30),
1093 },
1094 ),
1095 ]
1096 }
1097
1098 fn run_stats_test_failure_examples() -> Vec<(&'static str, RunStats)> {
1099 vec![
1100 (
1101 "one_failed",
1102 RunStats {
1103 initial_run_count: 20,
1104 finished_count: 1,
1105 failed: 1,
1106 cancel_reason: Some(CancelReason::TestFailure),
1107 ..RunStats::default()
1108 },
1109 ),
1110 (
1111 "one_failed_one_passed",
1112 RunStats {
1113 initial_run_count: 20,
1114 finished_count: 2,
1115 failed: 1,
1116 passed: 1,
1117 cancel_reason: Some(CancelReason::TestFailure),
1118 ..RunStats::default()
1119 },
1120 ),
1121 (
1122 "one_exec_failed",
1123 RunStats {
1124 initial_run_count: 20,
1125 finished_count: 10,
1126 exec_failed: 1,
1127 cancel_reason: Some(CancelReason::TestFailure),
1128 ..RunStats::default()
1129 },
1130 ),
1131 (
1132 "one_timed_out",
1133 RunStats {
1134 initial_run_count: 20,
1135 finished_count: 10,
1136 failed_timed_out: 1,
1137 cancel_reason: Some(CancelReason::TestFailure),
1138 ..RunStats::default()
1139 },
1140 ),
1141 ]
1142 }
1143
1144 fn run_stats_setup_script_failure_examples() -> Vec<(&'static str, RunStats)> {
1145 vec![
1146 (
1147 "one_setup_script_failed",
1148 RunStats {
1149 initial_run_count: 30,
1150 setup_scripts_failed: 1,
1151 cancel_reason: Some(CancelReason::SetupScriptFailure),
1152 ..RunStats::default()
1153 },
1154 ),
1155 (
1156 "one_setup_script_exec_failed",
1157 RunStats {
1158 initial_run_count: 35,
1159 setup_scripts_exec_failed: 1,
1160 cancel_reason: Some(CancelReason::SetupScriptFailure),
1161 ..RunStats::default()
1162 },
1163 ),
1164 (
1165 "one_setup_script_timed_out",
1166 RunStats {
1167 initial_run_count: 40,
1168 setup_scripts_timed_out: 1,
1169 cancel_reason: Some(CancelReason::SetupScriptFailure),
1170 ..RunStats::default()
1171 },
1172 ),
1173 ]
1174 }
1175
1176 #[test]
1184 fn update_progress_bar_updates_stats() {
1185 let styles = Styles::default();
1186 let binary_id = RustBinaryId::new("test-binary");
1187 let test_name = TestCaseName::new("test_name");
1188
1189 let mut state = ProgressBarState::new(
1191 NextestRunMode::Test,
1192 10,
1193 "=> ",
1194 MaxProgressRunning::default(),
1195 );
1196
1197 assert_eq!(state.stats, RunStats::default());
1199 assert_eq!(state.running, 0);
1200
1201 let started_stats = RunStats {
1203 initial_run_count: 10,
1204 passed: 5,
1205 ..RunStats::default()
1206 };
1207 let started_event = TestEvent {
1208 timestamp: Local::now().fixed_offset(),
1209 elapsed: Duration::ZERO,
1210 kind: TestEventKind::TestStarted {
1211 stress_index: None,
1212 test_instance: TestInstanceId {
1213 binary_id: &binary_id,
1214 test_name: &test_name,
1215 },
1216 slot_assignment: global_slot_assignment(0),
1217 current_stats: started_stats,
1218 running: 3,
1219 command_line: vec![],
1220 },
1221 };
1222
1223 state.update_progress_bar(&started_event, &styles);
1224
1225 assert_eq!(
1227 state.stats, started_stats,
1228 "stats should be updated on TestStarted"
1229 );
1230 assert_eq!(state.running, 3, "running should be updated on TestStarted");
1231
1232 let msg = state.progress_bar_msg(&styles);
1234 insta::assert_snapshot!(msg, @"3 running, 5 passed, 0 skipped");
1235
1236 let finished_stats = RunStats {
1238 initial_run_count: 10,
1239 finished_count: 1,
1240 passed: 8,
1241 ..RunStats::default()
1242 };
1243 let finished_event = TestEvent {
1244 timestamp: Local::now().fixed_offset(),
1245 elapsed: Duration::ZERO,
1246 kind: TestEventKind::TestFinished {
1247 stress_index: None,
1248 test_instance: TestInstanceId {
1249 binary_id: &binary_id,
1250 test_name: &test_name,
1251 },
1252 success_output: TestOutputDisplay::Never,
1253 failure_output: TestOutputDisplay::Never,
1254 junit_store_success_output: false,
1255 junit_store_failure_output: false,
1256 junit_flaky_fail_status: JunitFlakyFailStatus::default(),
1257 run_statuses: ExecutionStatuses::new(
1258 vec![ExecuteStatus {
1259 retry_data: RetryData {
1260 attempt: 1,
1261 total_attempts: 1,
1262 },
1263 output: make_test_output(),
1264 result: ExecutionResultDescription::Pass,
1265 start_time: Local::now().fixed_offset(),
1266 time_taken: Duration::from_secs(1),
1267 is_slow: false,
1268 delay_before_start: Duration::ZERO,
1269 error_summary: None,
1270 output_error_slice: None,
1271 }],
1272 FlakyResult::default(),
1273 ),
1274 current_stats: finished_stats,
1275 running: 2,
1276 },
1277 };
1278
1279 state.update_progress_bar(&finished_event, &styles);
1280
1281 assert_eq!(
1283 state.stats, finished_stats,
1284 "stats should be updated on TestFinished"
1285 );
1286 assert_eq!(
1287 state.running, 2,
1288 "running should be updated on TestFinished"
1289 );
1290
1291 let msg = state.progress_bar_msg(&styles);
1293 insta::assert_snapshot!(msg, @"2 running, 8 passed, 0 skipped");
1294
1295 let cancel_stats = RunStats {
1297 initial_run_count: 10,
1298 finished_count: 3,
1299 passed: 2,
1300 failed: 1,
1301 cancel_reason: Some(CancelReason::TestFailure),
1302 ..RunStats::default()
1303 };
1304 let cancel_event = TestEvent {
1305 timestamp: Local::now().fixed_offset(),
1306 elapsed: Duration::ZERO,
1307 kind: TestEventKind::RunBeginCancel {
1308 setup_scripts_running: 0,
1309 current_stats: cancel_stats,
1310 running: 4,
1311 },
1312 };
1313
1314 state.update_progress_bar(&cancel_event, &styles);
1315
1316 assert_eq!(
1318 state.stats, cancel_stats,
1319 "stats should be updated on RunBeginCancel"
1320 );
1321 assert_eq!(
1322 state.running, 4,
1323 "running should be updated on RunBeginCancel"
1324 );
1325
1326 let msg = state.progress_bar_msg(&styles);
1328 insta::assert_snapshot!(msg, @"4 running, 2 passed, 1 failed, 0 skipped");
1329
1330 let kill_stats = RunStats {
1332 initial_run_count: 10,
1333 finished_count: 5,
1334 passed: 3,
1335 failed: 2,
1336 cancel_reason: Some(CancelReason::Signal),
1337 ..RunStats::default()
1338 };
1339 let kill_event = TestEvent {
1340 timestamp: Local::now().fixed_offset(),
1341 elapsed: Duration::ZERO,
1342 kind: TestEventKind::RunBeginKill {
1343 setup_scripts_running: 0,
1344 current_stats: kill_stats,
1345 running: 2,
1346 },
1347 };
1348
1349 state.update_progress_bar(&kill_event, &styles);
1350
1351 assert_eq!(
1353 state.stats, kill_stats,
1354 "stats should be updated on RunBeginKill"
1355 );
1356 assert_eq!(
1357 state.running, 2,
1358 "running should be updated on RunBeginKill"
1359 );
1360
1361 let msg = state.progress_bar_msg(&styles);
1363 insta::assert_snapshot!(msg, @"2 running, 3 passed, 2 failed, 0 skipped");
1364 }
1365
1366 fn make_test_output() -> ChildExecutionOutputDescription<LiveSpec> {
1368 ChildExecutionOutput::Output {
1369 result: Some(ExecutionResult::Pass),
1370 output: ChildOutput::Split(ChildSplitOutput {
1371 stdout: Some(Bytes::new().into()),
1372 stderr: Some(Bytes::new().into()),
1373 }),
1374 errors: None,
1375 }
1376 .into()
1377 }
1378}