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