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
52impl MaxProgressRunning {
53 pub const DEFAULT_VALUE: Self = Self::Count(8);
55}
56
57impl Default for MaxProgressRunning {
58 fn default() -> Self {
59 Self::DEFAULT_VALUE
60 }
61}
62
63impl FromStr for MaxProgressRunning {
64 type Err = String;
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 if s.eq_ignore_ascii_case("infinite") {
68 return Ok(Self::Infinite);
69 }
70
71 match s.parse::<usize>() {
72 Err(e) => Err(format!("Error: {e} parsing {s}")),
73 Ok(n) => Ok(Self::Count(n)),
74 }
75 }
76}
77
78impl fmt::Display for MaxProgressRunning {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::Infinite => write!(f, "infinite"),
82 Self::Count(n) => write!(f, "{n}"),
83 }
84 }
85}
86
87#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
89pub enum ShowProgress {
90 #[default]
92 Auto,
93
94 None,
96
97 Counter,
99
100 Running,
102}
103
104#[derive(Debug)]
105pub(super) enum RunningTestStatus {
106 Running,
107 Slow,
108 Delay(Duration),
109 Retry,
110}
111
112#[derive(Debug)]
113pub(super) struct RunningTest {
114 binary_id: RustBinaryId,
115 test_name: TestCaseName,
116 status: RunningTestStatus,
117 start_time: Instant,
118 paused_for: Duration,
119}
120
121impl RunningTest {
122 fn message(&self, now: Instant, width: usize, styles: &Styles) -> String {
123 let mut elapsed = (now - self.start_time).saturating_sub(self.paused_for);
124 let status = match self.status {
125 RunningTestStatus::Running => " ".to_owned(),
126 RunningTestStatus::Slow => " SLOW".style(styles.skip).to_string(),
127 RunningTestStatus::Delay(d) => {
128 elapsed = d.saturating_sub(elapsed);
132 "DELAY".style(styles.retry).to_string()
133 }
134 RunningTestStatus::Retry => "RETRY".style(styles.retry).to_string(),
135 };
136 let elapsed = format!(
137 "{:0>2}:{:0>2}:{:0>2}",
138 elapsed.as_secs() / 3600,
139 elapsed.as_secs() / 60,
140 elapsed.as_secs() % 60,
141 );
142 let max_width = width.saturating_sub(25);
143 let test = DisplayTestInstance::new(
144 None,
145 None,
146 TestInstanceId {
147 binary_id: &self.binary_id,
148
149 test_name: &self.test_name,
150 },
151 &styles.list_styles,
152 )
153 .with_max_width(max_width);
154 format!(" {} [{:>9}] {}", status, elapsed, test)
155 }
156}
157
158#[derive(Debug)]
159pub(super) struct ProgressBarState {
160 bar: ProgressBar,
161 mode: NextestRunMode,
162 stats: RunStats,
163 running: usize,
164 max_progress_running: MaxProgressRunning,
165 max_running_displayed: usize,
169 running_tests: Option<Vec<RunningTest>>,
171 buffer: String,
172 println_chunk_size: usize,
175 hidden_no_capture: bool,
185 hidden_run_paused: bool,
186 hidden_info_response: bool,
187}
188
189impl ProgressBarState {
190 pub(super) fn new(
191 mode: NextestRunMode,
192 test_count: usize,
193 progress_chars: &str,
194 max_progress_running: MaxProgressRunning,
195 ) -> Self {
196 let bar = ProgressBar::new(test_count as u64);
197 let test_count_width = format!("{test_count}").len();
198 let template = format!(
203 "{{prefix:>12}} [{{elapsed_precise:>9}}] {{wide_bar}} \
204 {{pos:>{test_count_width}}}/{{len:{test_count_width}}}: {{msg}}"
205 );
206 bar.set_style(
207 ProgressStyle::default_bar()
208 .progress_chars(progress_chars)
209 .template(&template)
210 .expect("template is known to be valid"),
211 );
212
213 let running_tests =
214 (!matches!(max_progress_running, MaxProgressRunning::Count(0))).then(Vec::new);
215
216 let println_chunk_size = env::var("__NEXTEST_PROGRESS_PRINTLN_CHUNK_SIZE")
220 .ok()
221 .and_then(|s| s.parse::<usize>().ok())
222 .unwrap_or(4096);
223
224 Self {
225 bar,
226 mode,
227 stats: RunStats::default(),
228 running: 0,
229 max_progress_running,
230 max_running_displayed: 0,
231 running_tests,
232 buffer: String::new(),
233 println_chunk_size,
234 hidden_no_capture: false,
235 hidden_run_paused: false,
236 hidden_info_response: false,
237 }
238 }
239
240 pub(super) fn tick(&mut self, styles: &Styles) {
241 self.update_message(styles);
242 self.print_and_clear_buffer();
243 }
244
245 fn print_and_clear_buffer(&mut self) {
246 self.print_and_force_redraw();
247 self.buffer.clear();
248 }
249
250 fn print_and_force_redraw(&self) {
252 if self.buffer.is_empty() {
253 self.bar.force_draw();
256 return;
257 }
258
259 print_lines_in_chunks(&self.buffer, self.println_chunk_size, |chunk| {
273 self.bar.println(chunk);
274 });
275 }
276
277 fn update_message(&mut self, styles: &Styles) {
278 let mut msg = progress_bar_msg(&self.stats, self.running, styles);
279 msg += " ";
280
281 if let Some(running_tests) = &self.running_tests {
282 let (_, width) = console::Term::stderr().size();
283 let width = max(width as usize, 40);
284 let now = Instant::now();
285 let mut count = match self.max_progress_running {
286 MaxProgressRunning::Count(count) => min(running_tests.len(), count),
287 MaxProgressRunning::Infinite => running_tests.len(),
288 };
289 for running_test in &running_tests[..count] {
290 msg.push('\n');
291 msg.push_str(&running_test.message(now, width, styles));
292 }
293 if count < running_tests.len() {
294 let overflow_count = running_tests.len() - count;
295 msg.push_str(&format!(
296 "\n ... and {} more {} running",
297 overflow_count.style(styles.count),
298 plural::tests_str(self.mode, overflow_count),
299 ));
300 count += 1;
301 }
302 self.max_running_displayed = max(self.max_running_displayed, count);
303 msg.push_str(&"\n".to_string().repeat(self.max_running_displayed - count));
304 }
305 self.bar.set_message(msg);
306 }
307
308 pub(super) fn update_progress_bar(&mut self, event: &TestEvent<'_>, styles: &Styles) {
309 let before_should_hide = self.should_hide();
310
311 match &event.kind {
312 TestEventKind::StressSubRunStarted { .. } => {
313 self.bar.reset();
314 }
315 TestEventKind::StressSubRunFinished { .. } => {
316 self.bar.finish_and_clear();
319 }
320 TestEventKind::SetupScriptStarted { no_capture, .. } => {
321 if *no_capture {
323 self.hidden_no_capture = true;
324 }
325 }
326 TestEventKind::SetupScriptFinished { no_capture, .. } => {
327 if *no_capture {
329 self.hidden_no_capture = false;
330 }
331 }
332 TestEventKind::TestStarted {
333 current_stats,
334 running,
335 test_instance,
336 ..
337 } => {
338 self.running = *running;
339
340 self.bar.set_prefix(progress_bar_prefix(
341 current_stats,
342 current_stats.cancel_reason,
343 styles,
344 ));
345 self.bar.set_length(current_stats.initial_run_count as u64);
348 self.bar.set_position(current_stats.finished_count as u64);
349
350 if let Some(running_tests) = &mut self.running_tests {
351 running_tests.push(RunningTest {
352 binary_id: test_instance.binary_id.clone(),
353 test_name: test_instance.test_name.to_owned(),
354 status: RunningTestStatus::Running,
355 start_time: Instant::now(),
356 paused_for: Duration::ZERO,
357 });
358 }
359 }
360 TestEventKind::TestFinished {
361 current_stats,
362 running,
363 test_instance,
364 ..
365 } => {
366 self.running = *running;
367 self.remove_test(test_instance);
368
369 self.bar.set_prefix(progress_bar_prefix(
370 current_stats,
371 current_stats.cancel_reason,
372 styles,
373 ));
374 self.bar.set_length(current_stats.initial_run_count as u64);
377 self.bar.set_position(current_stats.finished_count as u64);
378 }
379 TestEventKind::TestAttemptFailedWillRetry {
380 test_instance,
381 delay_before_next_attempt,
382 ..
383 } => {
384 self.remove_test(test_instance);
385 if let Some(running_tests) = &mut self.running_tests {
386 running_tests.push(RunningTest {
387 binary_id: test_instance.binary_id.clone(),
388 test_name: test_instance.test_name.to_owned(),
389 status: RunningTestStatus::Delay(*delay_before_next_attempt),
390 start_time: Instant::now(),
391 paused_for: Duration::ZERO,
392 });
393 }
394 }
395 TestEventKind::TestRetryStarted { test_instance, .. } => {
396 self.remove_test(test_instance);
397 if let Some(running_tests) = &mut self.running_tests {
398 running_tests.push(RunningTest {
399 binary_id: test_instance.binary_id.clone(),
400 test_name: test_instance.test_name.to_owned(),
401 status: RunningTestStatus::Retry,
402 start_time: Instant::now(),
403 paused_for: Duration::ZERO,
404 });
405 }
406 }
407 TestEventKind::TestSlow { test_instance, .. } => {
408 if let Some(running_tests) = &mut self.running_tests {
409 running_tests
410 .iter_mut()
411 .find(|rt| {
412 &rt.binary_id == test_instance.binary_id
413 && &rt.test_name == test_instance.test_name
414 })
415 .expect("a slow test to be already running")
416 .status = RunningTestStatus::Slow;
417 }
418 }
419 TestEventKind::InfoStarted { .. } => {
420 self.hidden_info_response = true;
423 }
424 TestEventKind::InfoFinished { .. } => {
425 self.hidden_info_response = false;
427 }
428 TestEventKind::RunPaused { .. } => {
429 self.hidden_run_paused = true;
432 }
433 TestEventKind::RunContinued { .. } => {
434 self.hidden_run_paused = false;
437 let current_global_elapsed = self.bar.elapsed();
438 self.bar.set_elapsed(event.elapsed);
439
440 if let Some(running_tests) = &mut self.running_tests {
441 let delta = current_global_elapsed.saturating_sub(event.elapsed);
442 for running_test in running_tests {
443 running_test.paused_for += delta;
444 }
445 }
446 }
447 TestEventKind::RunBeginCancel { current_stats, .. }
448 | TestEventKind::RunBeginKill { current_stats, .. } => {
449 self.bar.set_prefix(progress_bar_cancel_prefix(
450 current_stats.cancel_reason,
451 styles,
452 ));
453 }
454 _ => {}
455 }
456
457 let after_should_hide = self.should_hide();
458
459 match (before_should_hide, after_should_hide) {
460 (false, true) => self.bar.set_draw_target(Self::hidden_target()),
461 (true, false) => self.bar.set_draw_target(Self::stderr_target()),
462 _ => {}
463 }
464 }
465
466 fn remove_test(&mut self, test_instance: &TestInstanceId) {
467 if let Some(running_tests) = &mut self.running_tests {
468 running_tests.remove(
469 running_tests
470 .iter()
471 .position(|e| {
472 &e.binary_id == test_instance.binary_id
473 && &e.test_name == test_instance.test_name
474 })
475 .expect("finished test to have started"),
476 );
477 }
478 }
479
480 pub(super) fn write_buf(&mut self, buf: &str) {
481 self.buffer.push_str(buf);
482 }
483
484 #[inline]
485 pub(super) fn finish_and_clear(&self) {
486 self.print_and_force_redraw();
487 self.bar.finish_and_clear();
488 }
489
490 fn stderr_target() -> ProgressDrawTarget {
491 ProgressDrawTarget::stderr_with_hz(PROGRESS_REFRESH_RATE_HZ)
492 }
493
494 fn hidden_target() -> ProgressDrawTarget {
495 ProgressDrawTarget::hidden()
496 }
497
498 fn should_hide(&self) -> bool {
499 self.hidden_no_capture || self.hidden_run_paused || self.hidden_info_response
500 }
501
502 pub(super) fn is_hidden(&self) -> bool {
503 self.bar.is_hidden()
504 }
505}
506
507#[derive(Clone, Copy, Debug, Eq, PartialEq)]
509pub enum ShowTerminalProgress {
510 Yes,
512
513 No,
515}
516
517impl ShowTerminalProgress {
518 const ENV: &str = "CARGO_TERM_PROGRESS_TERM_INTEGRATION";
519
520 pub fn from_cargo_configs(configs: &CargoConfigs, is_terminal: bool) -> Self {
523 for config in configs.discovered_configs() {
525 match config {
526 DiscoveredConfig::CliOption { config, source } => {
527 if let Some(v) = config.term.progress.term_integration {
528 if v {
529 debug!("enabling terminal progress reporting based on {source:?}");
530 return Self::Yes;
531 } else {
532 debug!("disabling terminal progress reporting based on {source:?}");
533 return Self::No;
534 }
535 }
536 }
537 DiscoveredConfig::Env => {
538 if let Some(v) = env::var_os(Self::ENV) {
539 if v == "true" {
540 debug!(
541 "enabling terminal progress reporting based on \
542 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
543 );
544 return Self::Yes;
545 } else if v == "false" {
546 debug!(
547 "disabling terminal progress reporting based on \
548 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
549 );
550 return Self::No;
551 } else {
552 debug!(
553 "invalid value for CARGO_TERM_PROGRESS_TERM_INTEGRATION \
554 environment variable: {v:?}, ignoring"
555 );
556 }
557 }
558 }
559 DiscoveredConfig::File { config, source } => {
560 if let Some(v) = config.term.progress.term_integration {
561 if v {
562 debug!("enabling terminal progress reporting based on {source:?}");
563 return Self::Yes;
564 } else {
565 debug!("disabling terminal progress reporting based on {source:?}");
566 return Self::No;
567 }
568 }
569 }
570 }
571 }
572
573 if supports_osc_9_4(is_terminal) {
574 Self::Yes
575 } else {
576 Self::No
577 }
578 }
579}
580
581#[derive(Default)]
583pub(super) struct TerminalProgress {
584 last_value: TerminalProgressValue,
585}
586
587impl TerminalProgress {
588 pub(super) fn new(show: ShowTerminalProgress) -> Option<Self> {
589 match show {
590 ShowTerminalProgress::Yes => Some(Self::default()),
591 ShowTerminalProgress::No => None,
592 }
593 }
594
595 pub(super) fn update_progress(&mut self, event: &TestEvent<'_>) {
596 let value = match &event.kind {
597 TestEventKind::RunStarted { .. }
598 | TestEventKind::StressSubRunStarted { .. }
599 | TestEventKind::StressSubRunFinished { .. }
600 | TestEventKind::SetupScriptStarted { .. }
601 | TestEventKind::SetupScriptSlow { .. }
602 | TestEventKind::SetupScriptFinished { .. } => TerminalProgressValue::None,
603 TestEventKind::TestStarted { current_stats, .. }
604 | TestEventKind::TestFinished { current_stats, .. } => {
605 let percentage = (current_stats.finished_count as f64
606 / current_stats.initial_run_count as f64)
607 * 100.0;
608 if current_stats.has_failures() || current_stats.cancel_reason.is_some() {
609 TerminalProgressValue::Error(percentage)
610 } else {
611 TerminalProgressValue::Value(percentage)
612 }
613 }
614 TestEventKind::TestSlow { .. }
615 | TestEventKind::TestAttemptFailedWillRetry { .. }
616 | TestEventKind::TestRetryStarted { .. }
617 | TestEventKind::TestSkipped { .. }
618 | TestEventKind::InfoStarted { .. }
619 | TestEventKind::InfoResponse { .. }
620 | TestEventKind::InfoFinished { .. }
621 | TestEventKind::InputEnter { .. } => TerminalProgressValue::None,
622 TestEventKind::RunBeginCancel { current_stats, .. }
623 | TestEventKind::RunBeginKill { current_stats, .. } => {
624 let percentage = (current_stats.finished_count as f64
626 / current_stats.initial_run_count as f64)
627 * 100.0;
628 TerminalProgressValue::Error(percentage)
629 }
630 TestEventKind::RunPaused { .. }
631 | TestEventKind::RunContinued { .. }
632 | TestEventKind::RunFinished { .. } => {
633 TerminalProgressValue::Remove
636 }
637 };
638
639 self.last_value = value;
640 }
641
642 pub(super) fn last_value(&self) -> &TerminalProgressValue {
643 &self.last_value
644 }
645}
646
647fn supports_osc_9_4(is_terminal: bool) -> bool {
649 if !is_terminal {
650 debug!(
651 "autodetect terminal progress reporting: disabling since \
652 passed-in stream (usually stderr) is not a terminal"
653 );
654 return false;
655 }
656 if std::env::var_os("WT_SESSION").is_some() {
657 debug!("autodetect terminal progress reporting: enabling since WT_SESSION is set");
658 return true;
659 };
660 if std::env::var_os("ConEmuANSI").is_some_and(|term| term == "ON") {
661 debug!("autodetect terminal progress reporting: enabling since ConEmuANSI is ON");
662 return true;
663 }
664 if let Ok(term) = std::env::var("TERM_PROGRAM")
665 && (term == "WezTerm" || term == "ghostty")
666 {
667 debug!("autodetect terminal progress reporting: enabling since TERM_PROGRAM is {term}");
668 return true;
669 }
670
671 false
672}
673
674#[derive(PartialEq, Debug, Default)]
678pub(super) enum TerminalProgressValue {
679 #[default]
681 None,
682 Remove,
684 Value(f64),
686 #[expect(dead_code)]
690 Indeterminate,
691 Error(f64),
693}
694
695impl fmt::Display for TerminalProgressValue {
696 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
697 let (state, progress) = match self {
705 Self::None => return Ok(()), Self::Remove => (0, 0.0),
707 Self::Value(v) => (1, *v),
708 Self::Indeterminate => (3, 0.0),
709 Self::Error(v) => (2, *v),
710 };
711 write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
712 }
713}
714
715pub(super) fn progress_str(
717 elapsed: Duration,
718 current_stats: &RunStats,
719 running: usize,
720 styles: &Styles,
721) -> String {
722 let mut s = progress_bar_prefix(current_stats, current_stats.cancel_reason, styles);
724
725 swrite!(
727 s,
728 " {}{}/{}: {}",
729 DisplayBracketedHhMmSs(elapsed),
730 current_stats.finished_count,
731 current_stats.initial_run_count,
732 progress_bar_msg(current_stats, running, styles)
733 );
734
735 s
736}
737
738pub(super) fn write_summary_str(run_stats: &RunStats, styles: &Styles, out: &mut String) {
739 let &RunStats {
741 initial_run_count: _,
742 finished_count: _,
743 setup_scripts_initial_count: _,
744 setup_scripts_finished_count: _,
745 setup_scripts_passed: _,
746 setup_scripts_failed: _,
747 setup_scripts_exec_failed: _,
748 setup_scripts_timed_out: _,
749 passed,
750 passed_slow,
751 passed_timed_out: _,
752 flaky,
753 failed,
754 failed_slow: _,
755 failed_timed_out,
756 leaky,
757 leaky_failed,
758 exec_failed,
759 skipped,
760 cancel_reason: _,
761 } = run_stats;
762
763 swrite!(
764 out,
765 "{} {}",
766 passed.style(styles.count),
767 "passed".style(styles.pass)
768 );
769
770 if passed_slow > 0 || flaky > 0 || leaky > 0 {
771 let mut text = Vec::with_capacity(3);
772 if passed_slow > 0 {
773 text.push(format!(
774 "{} {}",
775 passed_slow.style(styles.count),
776 "slow".style(styles.skip),
777 ));
778 }
779 if flaky > 0 {
780 text.push(format!(
781 "{} {}",
782 flaky.style(styles.count),
783 "flaky".style(styles.skip),
784 ));
785 }
786 if leaky > 0 {
787 text.push(format!(
788 "{} {}",
789 leaky.style(styles.count),
790 "leaky".style(styles.skip),
791 ));
792 }
793 swrite!(out, " ({})", text.join(", "));
794 }
795 swrite!(out, ", ");
796
797 if failed > 0 {
798 swrite!(
799 out,
800 "{} {}",
801 failed.style(styles.count),
802 "failed".style(styles.fail),
803 );
804 if leaky_failed > 0 {
805 swrite!(
806 out,
807 " ({} due to being {})",
808 leaky_failed.style(styles.count),
809 "leaky".style(styles.fail),
810 );
811 }
812 swrite!(out, ", ");
813 }
814
815 if exec_failed > 0 {
816 swrite!(
817 out,
818 "{} {}, ",
819 exec_failed.style(styles.count),
820 "exec failed".style(styles.fail),
821 );
822 }
823
824 if failed_timed_out > 0 {
825 swrite!(
826 out,
827 "{} {}, ",
828 failed_timed_out.style(styles.count),
829 "timed out".style(styles.fail),
830 );
831 }
832
833 swrite!(
834 out,
835 "{} {}",
836 skipped.style(styles.count),
837 "skipped".style(styles.skip),
838 );
839}
840
841fn progress_bar_cancel_prefix(reason: Option<CancelReason>, styles: &Styles) -> String {
842 let status = match reason {
843 Some(CancelReason::SetupScriptFailure)
844 | Some(CancelReason::TestFailure)
845 | Some(CancelReason::ReportError)
846 | Some(CancelReason::GlobalTimeout)
847 | Some(CancelReason::TestFailureImmediate)
848 | Some(CancelReason::Signal)
849 | Some(CancelReason::Interrupt)
850 | None => "Cancelling",
851 Some(CancelReason::SecondSignal) => "Killing",
852 };
853 format!("{:>12}", status.style(styles.fail))
854}
855
856fn progress_bar_prefix(
857 run_stats: &RunStats,
858 cancel_reason: Option<CancelReason>,
859 styles: &Styles,
860) -> String {
861 if let Some(reason) = cancel_reason {
862 return progress_bar_cancel_prefix(Some(reason), styles);
863 }
864
865 let style = if run_stats.has_failures() {
866 styles.fail
867 } else {
868 styles.pass
869 };
870
871 format!("{:>12}", "Running".style(style))
872}
873
874pub(super) fn progress_bar_msg(
875 current_stats: &RunStats,
876 running: usize,
877 styles: &Styles,
878) -> String {
879 let mut s = format!("{} running, ", running.style(styles.count));
880 write_summary_str(current_stats, styles, &mut s);
881 s
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 #[test]
889 fn test_progress_bar_prefix() {
890 let mut styles = Styles::default();
891 styles.colorize();
892
893 for (name, stats) in run_stats_test_failure_examples() {
894 let prefix = progress_bar_prefix(&stats, Some(CancelReason::TestFailure), &styles);
895 assert_eq!(
896 prefix,
897 " Cancelling".style(styles.fail).to_string(),
898 "{name} matches"
899 );
900 }
901 for (name, stats) in run_stats_setup_script_failure_examples() {
902 let prefix =
903 progress_bar_prefix(&stats, Some(CancelReason::SetupScriptFailure), &styles);
904 assert_eq!(
905 prefix,
906 " Cancelling".style(styles.fail).to_string(),
907 "{name} matches"
908 );
909 }
910
911 let prefix = progress_bar_prefix(&RunStats::default(), Some(CancelReason::Signal), &styles);
912 assert_eq!(prefix, " Cancelling".style(styles.fail).to_string());
913
914 let prefix = progress_bar_prefix(&RunStats::default(), None, &styles);
915 assert_eq!(prefix, " Running".style(styles.pass).to_string());
916
917 for (name, stats) in run_stats_test_failure_examples() {
918 let prefix = progress_bar_prefix(&stats, None, &styles);
919 assert_eq!(
920 prefix,
921 " Running".style(styles.fail).to_string(),
922 "{name} matches"
923 );
924 }
925 for (name, stats) in run_stats_setup_script_failure_examples() {
926 let prefix = progress_bar_prefix(&stats, None, &styles);
927 assert_eq!(
928 prefix,
929 " Running".style(styles.fail).to_string(),
930 "{name} matches"
931 );
932 }
933 }
934
935 #[test]
936 fn progress_str_snapshots() {
937 let mut styles = Styles::default();
938 styles.colorize();
939
940 let elapsed = Duration::from_secs(123456);
942 let running = 10;
943
944 for (name, stats) in run_stats_test_failure_examples() {
945 let s = progress_str(elapsed, &stats, running, &styles);
946 insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
947
948 let mut stats = stats;
949 stats.cancel_reason = None;
950 let s = progress_str(elapsed, &stats, running, &styles);
951 insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
952 }
953
954 for (name, stats) in run_stats_setup_script_failure_examples() {
955 let s = progress_str(elapsed, &stats, running, &styles);
956 insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
957
958 let mut stats = stats;
959 stats.cancel_reason = None;
960 let s = progress_str(elapsed, &stats, running, &styles);
961 insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
962 }
963 }
964
965 #[test]
966 fn running_test_snapshots() {
967 let styles = Styles::default();
968 let now = Instant::now();
969
970 for (name, running_test) in running_test_examples(now) {
971 let msg = running_test.message(now, 80, &styles);
972 insta::assert_snapshot!(name, msg);
973 }
974 }
975
976 fn running_test_examples(now: Instant) -> Vec<(&'static str, RunningTest)> {
977 let binary_id = RustBinaryId::new("my-binary");
978 let test_name = TestCaseName::new("test::my_test");
979 let start_time = now - Duration::from_secs(125); vec![
982 (
983 "running_status",
984 RunningTest {
985 binary_id: binary_id.clone(),
986 test_name: test_name.clone(),
987 status: RunningTestStatus::Running,
988 start_time,
989 paused_for: Duration::ZERO,
990 },
991 ),
992 (
993 "slow_status",
994 RunningTest {
995 binary_id: binary_id.clone(),
996 test_name: test_name.clone(),
997 status: RunningTestStatus::Slow,
998 start_time,
999 paused_for: Duration::ZERO,
1000 },
1001 ),
1002 (
1003 "delay_status",
1004 RunningTest {
1005 binary_id: binary_id.clone(),
1006 test_name: test_name.clone(),
1007 status: RunningTestStatus::Delay(Duration::from_secs(130)),
1008 start_time,
1009 paused_for: Duration::ZERO,
1010 },
1011 ),
1012 (
1013 "delay_status_underflow",
1014 RunningTest {
1015 binary_id: binary_id.clone(),
1016 test_name: test_name.clone(),
1017 status: RunningTestStatus::Delay(Duration::from_secs(124)),
1018 start_time,
1019 paused_for: Duration::ZERO,
1020 },
1021 ),
1022 (
1023 "retry_status",
1024 RunningTest {
1025 binary_id: binary_id.clone(),
1026 test_name: test_name.clone(),
1027 status: RunningTestStatus::Retry,
1028 start_time,
1029 paused_for: Duration::ZERO,
1030 },
1031 ),
1032 (
1033 "with_paused_duration",
1034 RunningTest {
1035 binary_id: binary_id.clone(),
1036 test_name: test_name.clone(),
1037 status: RunningTestStatus::Running,
1038 start_time,
1039 paused_for: Duration::from_secs(30),
1040 },
1041 ),
1042 ]
1043 }
1044
1045 fn run_stats_test_failure_examples() -> Vec<(&'static str, RunStats)> {
1046 vec![
1047 (
1048 "one_failed",
1049 RunStats {
1050 initial_run_count: 20,
1051 finished_count: 1,
1052 failed: 1,
1053 cancel_reason: Some(CancelReason::TestFailure),
1054 ..RunStats::default()
1055 },
1056 ),
1057 (
1058 "one_failed_one_passed",
1059 RunStats {
1060 initial_run_count: 20,
1061 finished_count: 2,
1062 failed: 1,
1063 passed: 1,
1064 cancel_reason: Some(CancelReason::TestFailure),
1065 ..RunStats::default()
1066 },
1067 ),
1068 (
1069 "one_exec_failed",
1070 RunStats {
1071 initial_run_count: 20,
1072 finished_count: 10,
1073 exec_failed: 1,
1074 cancel_reason: Some(CancelReason::TestFailure),
1075 ..RunStats::default()
1076 },
1077 ),
1078 (
1079 "one_timed_out",
1080 RunStats {
1081 initial_run_count: 20,
1082 finished_count: 10,
1083 failed_timed_out: 1,
1084 cancel_reason: Some(CancelReason::TestFailure),
1085 ..RunStats::default()
1086 },
1087 ),
1088 ]
1089 }
1090
1091 fn run_stats_setup_script_failure_examples() -> Vec<(&'static str, RunStats)> {
1092 vec![
1093 (
1094 "one_setup_script_failed",
1095 RunStats {
1096 initial_run_count: 30,
1097 setup_scripts_failed: 1,
1098 cancel_reason: Some(CancelReason::SetupScriptFailure),
1099 ..RunStats::default()
1100 },
1101 ),
1102 (
1103 "one_setup_script_exec_failed",
1104 RunStats {
1105 initial_run_count: 35,
1106 setup_scripts_exec_failed: 1,
1107 cancel_reason: Some(CancelReason::SetupScriptFailure),
1108 ..RunStats::default()
1109 },
1110 ),
1111 (
1112 "one_setup_script_timed_out",
1113 RunStats {
1114 initial_run_count: 40,
1115 setup_scripts_timed_out: 1,
1116 cancel_reason: Some(CancelReason::SetupScriptFailure),
1117 ..RunStats::default()
1118 },
1119 ),
1120 ]
1121 }
1122}