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 = self.progress_bar_msg(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 swrite!(
296 msg,
297 "\n ... and {} more {} running",
298 overflow_count.style(styles.count),
299 plural::tests_str(self.mode, overflow_count),
300 );
301 count += 1;
302 }
303 self.max_running_displayed = max(self.max_running_displayed, count);
304 msg.push_str(&"\n".to_string().repeat(self.max_running_displayed - count));
305 }
306 self.bar.set_message(msg);
307 }
308
309 fn progress_bar_msg(&self, styles: &Styles) -> String {
310 progress_bar_msg(&self.stats, self.running, styles)
311 }
312
313 pub(super) fn update_progress_bar(&mut self, event: &TestEvent<'_>, styles: &Styles) {
314 let before_should_hide = self.should_hide();
315
316 match &event.kind {
317 TestEventKind::StressSubRunStarted { .. } => {
318 self.bar.reset();
319 }
320 TestEventKind::StressSubRunFinished { .. } => {
321 self.bar.finish_and_clear();
324 }
325 TestEventKind::SetupScriptStarted { no_capture, .. } => {
326 if *no_capture {
328 self.hidden_no_capture = true;
329 }
330 }
331 TestEventKind::SetupScriptFinished { no_capture, .. } => {
332 if *no_capture {
334 self.hidden_no_capture = false;
335 }
336 }
337 TestEventKind::TestStarted {
338 current_stats,
339 running,
340 test_instance,
341 ..
342 } => {
343 self.running = *running;
344 self.stats = *current_stats;
345
346 self.bar.set_prefix(progress_bar_prefix(
347 current_stats,
348 current_stats.cancel_reason,
349 styles,
350 ));
351 self.bar.set_length(current_stats.initial_run_count as u64);
354 self.bar.set_position(current_stats.finished_count as u64);
355
356 if let Some(running_tests) = &mut self.running_tests {
357 running_tests.push(RunningTest {
358 binary_id: test_instance.binary_id.clone(),
359 test_name: test_instance.test_name.to_owned(),
360 status: RunningTestStatus::Running,
361 start_time: Instant::now(),
362 paused_for: Duration::ZERO,
363 });
364 }
365 }
366 TestEventKind::TestFinished {
367 current_stats,
368 running,
369 test_instance,
370 ..
371 } => {
372 self.running = *running;
373 self.stats = *current_stats;
374 self.remove_test(test_instance);
375
376 self.bar.set_prefix(progress_bar_prefix(
377 current_stats,
378 current_stats.cancel_reason,
379 styles,
380 ));
381 self.bar.set_length(current_stats.initial_run_count as u64);
384 self.bar.set_position(current_stats.finished_count as u64);
385 }
386 TestEventKind::TestAttemptFailedWillRetry {
387 test_instance,
388 delay_before_next_attempt,
389 ..
390 } => {
391 self.remove_test(test_instance);
392 if let Some(running_tests) = &mut self.running_tests {
393 running_tests.push(RunningTest {
394 binary_id: test_instance.binary_id.clone(),
395 test_name: test_instance.test_name.to_owned(),
396 status: RunningTestStatus::Delay(*delay_before_next_attempt),
397 start_time: Instant::now(),
398 paused_for: Duration::ZERO,
399 });
400 }
401 }
402 TestEventKind::TestRetryStarted { test_instance, .. } => {
403 self.remove_test(test_instance);
404 if let Some(running_tests) = &mut self.running_tests {
405 running_tests.push(RunningTest {
406 binary_id: test_instance.binary_id.clone(),
407 test_name: test_instance.test_name.to_owned(),
408 status: RunningTestStatus::Retry,
409 start_time: Instant::now(),
410 paused_for: Duration::ZERO,
411 });
412 }
413 }
414 TestEventKind::TestSlow { test_instance, .. } => {
415 if let Some(running_tests) = &mut self.running_tests {
416 running_tests
417 .iter_mut()
418 .find(|rt| {
419 &rt.binary_id == test_instance.binary_id
420 && &rt.test_name == test_instance.test_name
421 })
422 .expect("a slow test to be already running")
423 .status = RunningTestStatus::Slow;
424 }
425 }
426 TestEventKind::InfoStarted { .. } => {
427 self.hidden_info_response = true;
430 }
431 TestEventKind::InfoFinished { .. } => {
432 self.hidden_info_response = false;
434 }
435 TestEventKind::RunPaused { .. } => {
436 self.hidden_run_paused = true;
439 }
440 TestEventKind::RunContinued { .. } => {
441 self.hidden_run_paused = false;
444 let current_global_elapsed = self.bar.elapsed();
445 self.bar.set_elapsed(event.elapsed);
446
447 if let Some(running_tests) = &mut self.running_tests {
448 let delta = current_global_elapsed.saturating_sub(event.elapsed);
449 for running_test in running_tests {
450 running_test.paused_for += delta;
451 }
452 }
453 }
454 TestEventKind::RunBeginCancel {
455 current_stats,
456 running,
457 ..
458 }
459 | TestEventKind::RunBeginKill {
460 current_stats,
461 running,
462 ..
463 } => {
464 self.running = *running;
465 self.stats = *current_stats;
466 self.bar.set_prefix(progress_bar_cancel_prefix(
467 current_stats.cancel_reason,
468 styles,
469 ));
470 }
471 _ => {}
472 }
473
474 let after_should_hide = self.should_hide();
475
476 match (before_should_hide, after_should_hide) {
477 (false, true) => self.bar.set_draw_target(Self::hidden_target()),
478 (true, false) => self.bar.set_draw_target(Self::stderr_target()),
479 _ => {}
480 }
481 }
482
483 fn remove_test(&mut self, test_instance: &TestInstanceId) {
484 if let Some(running_tests) = &mut self.running_tests {
485 running_tests.remove(
486 running_tests
487 .iter()
488 .position(|e| {
489 &e.binary_id == test_instance.binary_id
490 && &e.test_name == test_instance.test_name
491 })
492 .expect("finished test to have started"),
493 );
494 }
495 }
496
497 pub(super) fn write_buf(&mut self, buf: &str) {
498 self.buffer.push_str(buf);
499 }
500
501 #[inline]
502 pub(super) fn finish_and_clear(&self) {
503 self.print_and_force_redraw();
504 self.bar.finish_and_clear();
505 }
506
507 fn stderr_target() -> ProgressDrawTarget {
508 ProgressDrawTarget::stderr_with_hz(PROGRESS_REFRESH_RATE_HZ)
509 }
510
511 fn hidden_target() -> ProgressDrawTarget {
512 ProgressDrawTarget::hidden()
513 }
514
515 fn should_hide(&self) -> bool {
516 self.hidden_no_capture || self.hidden_run_paused || self.hidden_info_response
517 }
518
519 pub(super) fn is_hidden(&self) -> bool {
520 self.bar.is_hidden()
521 }
522}
523
524#[derive(Clone, Copy, Debug, Eq, PartialEq)]
526pub enum ShowTerminalProgress {
527 Yes,
529
530 No,
532}
533
534impl ShowTerminalProgress {
535 const ENV: &str = "CARGO_TERM_PROGRESS_TERM_INTEGRATION";
536
537 pub fn from_cargo_configs(configs: &CargoConfigs, is_terminal: bool) -> Self {
540 for config in configs.discovered_configs() {
542 match config {
543 DiscoveredConfig::CliOption { config, source } => {
544 if let Some(v) = config.term.progress.term_integration {
545 if v {
546 debug!("enabling terminal progress reporting based on {source:?}");
547 return Self::Yes;
548 } else {
549 debug!("disabling terminal progress reporting based on {source:?}");
550 return Self::No;
551 }
552 }
553 }
554 DiscoveredConfig::Env => {
555 if let Some(v) = env::var_os(Self::ENV) {
556 if v == "true" {
557 debug!(
558 "enabling terminal progress reporting based on \
559 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
560 );
561 return Self::Yes;
562 } else if v == "false" {
563 debug!(
564 "disabling terminal progress reporting based on \
565 CARGO_TERM_PROGRESS_TERM_INTEGRATION environment variable"
566 );
567 return Self::No;
568 } else {
569 debug!(
570 "invalid value for CARGO_TERM_PROGRESS_TERM_INTEGRATION \
571 environment variable: {v:?}, ignoring"
572 );
573 }
574 }
575 }
576 DiscoveredConfig::File { config, source } => {
577 if let Some(v) = config.term.progress.term_integration {
578 if v {
579 debug!("enabling terminal progress reporting based on {source:?}");
580 return Self::Yes;
581 } else {
582 debug!("disabling terminal progress reporting based on {source:?}");
583 return Self::No;
584 }
585 }
586 }
587 }
588 }
589
590 if supports_osc_9_4(is_terminal) {
591 Self::Yes
592 } else {
593 Self::No
594 }
595 }
596}
597
598#[derive(Default)]
600pub(super) struct TerminalProgress {
601 last_value: TerminalProgressValue,
602}
603
604impl TerminalProgress {
605 pub(super) fn new(show: ShowTerminalProgress) -> Option<Self> {
606 match show {
607 ShowTerminalProgress::Yes => Some(Self::default()),
608 ShowTerminalProgress::No => None,
609 }
610 }
611
612 pub(super) fn update_progress(&mut self, event: &TestEvent<'_>) {
613 let value = match &event.kind {
614 TestEventKind::RunStarted { .. }
615 | TestEventKind::StressSubRunStarted { .. }
616 | TestEventKind::StressSubRunFinished { .. }
617 | TestEventKind::SetupScriptStarted { .. }
618 | TestEventKind::SetupScriptSlow { .. }
619 | TestEventKind::SetupScriptFinished { .. } => TerminalProgressValue::None,
620 TestEventKind::TestStarted { current_stats, .. }
621 | TestEventKind::TestFinished { current_stats, .. } => {
622 let percentage = (current_stats.finished_count as f64
623 / current_stats.initial_run_count as f64)
624 * 100.0;
625 if current_stats.has_failures() || current_stats.cancel_reason.is_some() {
626 TerminalProgressValue::Error(percentage)
627 } else {
628 TerminalProgressValue::Value(percentage)
629 }
630 }
631 TestEventKind::TestSlow { .. }
632 | TestEventKind::TestAttemptFailedWillRetry { .. }
633 | TestEventKind::TestRetryStarted { .. }
634 | TestEventKind::TestSkipped { .. }
635 | TestEventKind::InfoStarted { .. }
636 | TestEventKind::InfoResponse { .. }
637 | TestEventKind::InfoFinished { .. }
638 | TestEventKind::InputEnter { .. } => TerminalProgressValue::None,
639 TestEventKind::RunBeginCancel { current_stats, .. }
640 | TestEventKind::RunBeginKill { current_stats, .. } => {
641 let percentage = (current_stats.finished_count as f64
643 / current_stats.initial_run_count as f64)
644 * 100.0;
645 TerminalProgressValue::Error(percentage)
646 }
647 TestEventKind::RunPaused { .. }
648 | TestEventKind::RunContinued { .. }
649 | TestEventKind::RunFinished { .. } => {
650 TerminalProgressValue::Remove
653 }
654 };
655
656 self.last_value = value;
657 }
658
659 pub(super) fn last_value(&self) -> &TerminalProgressValue {
660 &self.last_value
661 }
662}
663
664fn supports_osc_9_4(is_terminal: bool) -> bool {
666 if !is_terminal {
667 debug!(
668 "autodetect terminal progress reporting: disabling since \
669 passed-in stream (usually stderr) is not a terminal"
670 );
671 return false;
672 }
673 if std::env::var_os("WT_SESSION").is_some() {
674 debug!("autodetect terminal progress reporting: enabling since WT_SESSION is set");
675 return true;
676 };
677 if std::env::var_os("ConEmuANSI").is_some_and(|term| term == "ON") {
678 debug!("autodetect terminal progress reporting: enabling since ConEmuANSI is ON");
679 return true;
680 }
681 if let Ok(term) = std::env::var("TERM_PROGRAM")
682 && (term == "WezTerm" || term == "ghostty" || term == "iTerm.app")
683 {
684 debug!("autodetect terminal progress reporting: enabling since TERM_PROGRAM is {term}");
685 return true;
686 }
687
688 false
689}
690
691#[derive(PartialEq, Debug, Default)]
695pub(super) enum TerminalProgressValue {
696 #[default]
698 None,
699 Remove,
701 Value(f64),
703 #[expect(dead_code)]
707 Indeterminate,
708 Error(f64),
710}
711
712impl fmt::Display for TerminalProgressValue {
713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714 let (state, progress) = match self {
722 Self::None => return Ok(()), Self::Remove => (0, 0.0),
724 Self::Value(v) => (1, *v),
725 Self::Indeterminate => (3, 0.0),
726 Self::Error(v) => (2, *v),
727 };
728 write!(f, "\x1b]9;4;{state};{progress:.0}\x1b\\")
729 }
730}
731
732pub(super) fn progress_str(
734 elapsed: Duration,
735 current_stats: &RunStats,
736 running: usize,
737 styles: &Styles,
738) -> String {
739 let mut s = progress_bar_prefix(current_stats, current_stats.cancel_reason, styles);
741
742 swrite!(
744 s,
745 " {}{}/{}: {}",
746 DisplayBracketedHhMmSs(elapsed),
747 current_stats.finished_count,
748 current_stats.initial_run_count,
749 progress_bar_msg(current_stats, running, styles)
750 );
751
752 s
753}
754
755pub(super) fn write_summary_str(run_stats: &RunStats, styles: &Styles, out: &mut String) {
756 let &RunStats {
758 initial_run_count: _,
759 finished_count: _,
760 setup_scripts_initial_count: _,
761 setup_scripts_finished_count: _,
762 setup_scripts_passed: _,
763 setup_scripts_failed: _,
764 setup_scripts_exec_failed: _,
765 setup_scripts_timed_out: _,
766 passed,
767 passed_slow,
768 passed_timed_out: _,
769 flaky,
770 failed,
771 failed_slow: _,
772 failed_timed_out,
773 leaky,
774 leaky_failed,
775 exec_failed,
776 skipped,
777 cancel_reason: _,
778 } = run_stats;
779
780 swrite!(
781 out,
782 "{} {}",
783 passed.style(styles.count),
784 "passed".style(styles.pass)
785 );
786
787 if passed_slow > 0 || flaky > 0 || leaky > 0 {
788 let mut text = Vec::with_capacity(3);
789 if passed_slow > 0 {
790 text.push(format!(
791 "{} {}",
792 passed_slow.style(styles.count),
793 "slow".style(styles.skip),
794 ));
795 }
796 if flaky > 0 {
797 text.push(format!(
798 "{} {}",
799 flaky.style(styles.count),
800 "flaky".style(styles.skip),
801 ));
802 }
803 if leaky > 0 {
804 text.push(format!(
805 "{} {}",
806 leaky.style(styles.count),
807 "leaky".style(styles.skip),
808 ));
809 }
810 swrite!(out, " ({})", text.join(", "));
811 }
812 swrite!(out, ", ");
813
814 if failed > 0 {
815 swrite!(
816 out,
817 "{} {}",
818 failed.style(styles.count),
819 "failed".style(styles.fail),
820 );
821 if leaky_failed > 0 {
822 swrite!(
823 out,
824 " ({} due to being {})",
825 leaky_failed.style(styles.count),
826 "leaky".style(styles.fail),
827 );
828 }
829 swrite!(out, ", ");
830 }
831
832 if exec_failed > 0 {
833 swrite!(
834 out,
835 "{} {}, ",
836 exec_failed.style(styles.count),
837 "exec failed".style(styles.fail),
838 );
839 }
840
841 if failed_timed_out > 0 {
842 swrite!(
843 out,
844 "{} {}, ",
845 failed_timed_out.style(styles.count),
846 "timed out".style(styles.fail),
847 );
848 }
849
850 swrite!(
851 out,
852 "{} {}",
853 skipped.style(styles.count),
854 "skipped".style(styles.skip),
855 );
856}
857
858fn progress_bar_cancel_prefix(reason: Option<CancelReason>, styles: &Styles) -> String {
859 let status = match reason {
860 Some(CancelReason::SetupScriptFailure)
861 | Some(CancelReason::TestFailure)
862 | Some(CancelReason::ReportError)
863 | Some(CancelReason::GlobalTimeout)
864 | Some(CancelReason::TestFailureImmediate)
865 | Some(CancelReason::Signal)
866 | Some(CancelReason::Interrupt)
867 | None => "Cancelling",
868 Some(CancelReason::SecondSignal) => "Killing",
869 };
870 format!("{:>12}", status.style(styles.fail))
871}
872
873fn progress_bar_prefix(
874 run_stats: &RunStats,
875 cancel_reason: Option<CancelReason>,
876 styles: &Styles,
877) -> String {
878 if let Some(reason) = cancel_reason {
879 return progress_bar_cancel_prefix(Some(reason), styles);
880 }
881
882 let style = if run_stats.has_failures() {
883 styles.fail
884 } else {
885 styles.pass
886 };
887
888 format!("{:>12}", "Running".style(style))
889}
890
891pub(super) fn progress_bar_msg(
892 current_stats: &RunStats,
893 running: usize,
894 styles: &Styles,
895) -> String {
896 let mut s = format!("{} running, ", running.style(styles.count));
897 write_summary_str(current_stats, styles, &mut s);
898 s
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904 use crate::{
905 reporter::TestOutputDisplay,
906 test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput, ChildSplitOutput},
907 };
908 use bytes::Bytes;
909 use chrono::Local;
910
911 #[test]
912 fn test_progress_bar_prefix() {
913 let mut styles = Styles::default();
914 styles.colorize();
915
916 for (name, stats) in run_stats_test_failure_examples() {
917 let prefix = progress_bar_prefix(&stats, Some(CancelReason::TestFailure), &styles);
918 assert_eq!(
919 prefix,
920 " Cancelling".style(styles.fail).to_string(),
921 "{name} matches"
922 );
923 }
924 for (name, stats) in run_stats_setup_script_failure_examples() {
925 let prefix =
926 progress_bar_prefix(&stats, Some(CancelReason::SetupScriptFailure), &styles);
927 assert_eq!(
928 prefix,
929 " Cancelling".style(styles.fail).to_string(),
930 "{name} matches"
931 );
932 }
933
934 let prefix = progress_bar_prefix(&RunStats::default(), Some(CancelReason::Signal), &styles);
935 assert_eq!(prefix, " Cancelling".style(styles.fail).to_string());
936
937 let prefix = progress_bar_prefix(&RunStats::default(), None, &styles);
938 assert_eq!(prefix, " Running".style(styles.pass).to_string());
939
940 for (name, stats) in run_stats_test_failure_examples() {
941 let prefix = progress_bar_prefix(&stats, None, &styles);
942 assert_eq!(
943 prefix,
944 " Running".style(styles.fail).to_string(),
945 "{name} matches"
946 );
947 }
948 for (name, stats) in run_stats_setup_script_failure_examples() {
949 let prefix = progress_bar_prefix(&stats, None, &styles);
950 assert_eq!(
951 prefix,
952 " Running".style(styles.fail).to_string(),
953 "{name} matches"
954 );
955 }
956 }
957
958 #[test]
959 fn progress_str_snapshots() {
960 let mut styles = Styles::default();
961 styles.colorize();
962
963 let elapsed = Duration::from_secs(123456);
965 let running = 10;
966
967 for (name, stats) in run_stats_test_failure_examples() {
968 let s = progress_str(elapsed, &stats, running, &styles);
969 insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
970
971 let mut stats = stats;
972 stats.cancel_reason = None;
973 let s = progress_str(elapsed, &stats, running, &styles);
974 insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
975 }
976
977 for (name, stats) in run_stats_setup_script_failure_examples() {
978 let s = progress_str(elapsed, &stats, running, &styles);
979 insta::assert_snapshot!(format!("{name}_with_cancel_reason"), s);
980
981 let mut stats = stats;
982 stats.cancel_reason = None;
983 let s = progress_str(elapsed, &stats, running, &styles);
984 insta::assert_snapshot!(format!("{name}_without_cancel_reason"), s);
985 }
986 }
987
988 #[test]
989 fn running_test_snapshots() {
990 let styles = Styles::default();
991 let now = Instant::now();
992
993 for (name, running_test) in running_test_examples(now) {
994 let msg = running_test.message(now, 80, &styles);
995 insta::assert_snapshot!(name, msg);
996 }
997 }
998
999 fn running_test_examples(now: Instant) -> Vec<(&'static str, RunningTest)> {
1000 let binary_id = RustBinaryId::new("my-binary");
1001 let test_name = TestCaseName::new("test::my_test");
1002 let start_time = now - Duration::from_secs(125); vec![
1005 (
1006 "running_status",
1007 RunningTest {
1008 binary_id: binary_id.clone(),
1009 test_name: test_name.clone(),
1010 status: RunningTestStatus::Running,
1011 start_time,
1012 paused_for: Duration::ZERO,
1013 },
1014 ),
1015 (
1016 "slow_status",
1017 RunningTest {
1018 binary_id: binary_id.clone(),
1019 test_name: test_name.clone(),
1020 status: RunningTestStatus::Slow,
1021 start_time,
1022 paused_for: Duration::ZERO,
1023 },
1024 ),
1025 (
1026 "delay_status",
1027 RunningTest {
1028 binary_id: binary_id.clone(),
1029 test_name: test_name.clone(),
1030 status: RunningTestStatus::Delay(Duration::from_secs(130)),
1031 start_time,
1032 paused_for: Duration::ZERO,
1033 },
1034 ),
1035 (
1036 "delay_status_underflow",
1037 RunningTest {
1038 binary_id: binary_id.clone(),
1039 test_name: test_name.clone(),
1040 status: RunningTestStatus::Delay(Duration::from_secs(124)),
1041 start_time,
1042 paused_for: Duration::ZERO,
1043 },
1044 ),
1045 (
1046 "retry_status",
1047 RunningTest {
1048 binary_id: binary_id.clone(),
1049 test_name: test_name.clone(),
1050 status: RunningTestStatus::Retry,
1051 start_time,
1052 paused_for: Duration::ZERO,
1053 },
1054 ),
1055 (
1056 "with_paused_duration",
1057 RunningTest {
1058 binary_id: binary_id.clone(),
1059 test_name: test_name.clone(),
1060 status: RunningTestStatus::Running,
1061 start_time,
1062 paused_for: Duration::from_secs(30),
1063 },
1064 ),
1065 ]
1066 }
1067
1068 fn run_stats_test_failure_examples() -> Vec<(&'static str, RunStats)> {
1069 vec![
1070 (
1071 "one_failed",
1072 RunStats {
1073 initial_run_count: 20,
1074 finished_count: 1,
1075 failed: 1,
1076 cancel_reason: Some(CancelReason::TestFailure),
1077 ..RunStats::default()
1078 },
1079 ),
1080 (
1081 "one_failed_one_passed",
1082 RunStats {
1083 initial_run_count: 20,
1084 finished_count: 2,
1085 failed: 1,
1086 passed: 1,
1087 cancel_reason: Some(CancelReason::TestFailure),
1088 ..RunStats::default()
1089 },
1090 ),
1091 (
1092 "one_exec_failed",
1093 RunStats {
1094 initial_run_count: 20,
1095 finished_count: 10,
1096 exec_failed: 1,
1097 cancel_reason: Some(CancelReason::TestFailure),
1098 ..RunStats::default()
1099 },
1100 ),
1101 (
1102 "one_timed_out",
1103 RunStats {
1104 initial_run_count: 20,
1105 finished_count: 10,
1106 failed_timed_out: 1,
1107 cancel_reason: Some(CancelReason::TestFailure),
1108 ..RunStats::default()
1109 },
1110 ),
1111 ]
1112 }
1113
1114 fn run_stats_setup_script_failure_examples() -> Vec<(&'static str, RunStats)> {
1115 vec![
1116 (
1117 "one_setup_script_failed",
1118 RunStats {
1119 initial_run_count: 30,
1120 setup_scripts_failed: 1,
1121 cancel_reason: Some(CancelReason::SetupScriptFailure),
1122 ..RunStats::default()
1123 },
1124 ),
1125 (
1126 "one_setup_script_exec_failed",
1127 RunStats {
1128 initial_run_count: 35,
1129 setup_scripts_exec_failed: 1,
1130 cancel_reason: Some(CancelReason::SetupScriptFailure),
1131 ..RunStats::default()
1132 },
1133 ),
1134 (
1135 "one_setup_script_timed_out",
1136 RunStats {
1137 initial_run_count: 40,
1138 setup_scripts_timed_out: 1,
1139 cancel_reason: Some(CancelReason::SetupScriptFailure),
1140 ..RunStats::default()
1141 },
1142 ),
1143 ]
1144 }
1145
1146 #[test]
1154 fn update_progress_bar_updates_stats() {
1155 let styles = Styles::default();
1156 let binary_id = RustBinaryId::new("test-binary");
1157 let test_name = TestCaseName::new("test_name");
1158
1159 let mut state = ProgressBarState::new(
1161 NextestRunMode::Test,
1162 10,
1163 "=> ",
1164 MaxProgressRunning::default(),
1165 );
1166
1167 assert_eq!(state.stats, RunStats::default());
1169 assert_eq!(state.running, 0);
1170
1171 let started_stats = RunStats {
1173 initial_run_count: 10,
1174 passed: 5,
1175 ..RunStats::default()
1176 };
1177 let started_event = TestEvent {
1178 timestamp: Local::now().fixed_offset(),
1179 elapsed: Duration::ZERO,
1180 kind: TestEventKind::TestStarted {
1181 stress_index: None,
1182 test_instance: TestInstanceId {
1183 binary_id: &binary_id,
1184 test_name: &test_name,
1185 },
1186 current_stats: started_stats,
1187 running: 3,
1188 command_line: vec![],
1189 },
1190 };
1191
1192 state.update_progress_bar(&started_event, &styles);
1193
1194 assert_eq!(
1196 state.stats, started_stats,
1197 "stats should be updated on TestStarted"
1198 );
1199 assert_eq!(state.running, 3, "running should be updated on TestStarted");
1200
1201 let msg = state.progress_bar_msg(&styles);
1203 insta::assert_snapshot!(msg, @"3 running, 5 passed, 0 skipped");
1204
1205 let finished_stats = RunStats {
1207 initial_run_count: 10,
1208 finished_count: 1,
1209 passed: 8,
1210 ..RunStats::default()
1211 };
1212 let finished_event = TestEvent {
1213 timestamp: Local::now().fixed_offset(),
1214 elapsed: Duration::ZERO,
1215 kind: TestEventKind::TestFinished {
1216 stress_index: None,
1217 test_instance: TestInstanceId {
1218 binary_id: &binary_id,
1219 test_name: &test_name,
1220 },
1221 success_output: TestOutputDisplay::Never,
1222 failure_output: TestOutputDisplay::Never,
1223 junit_store_success_output: false,
1224 junit_store_failure_output: false,
1225 run_statuses: ExecutionStatuses::new(vec![ExecuteStatus {
1226 retry_data: RetryData {
1227 attempt: 1,
1228 total_attempts: 1,
1229 },
1230 output: make_test_output(),
1231 result: ExecutionResultDescription::Pass,
1232 start_time: Local::now().fixed_offset(),
1233 time_taken: Duration::from_secs(1),
1234 is_slow: false,
1235 delay_before_start: Duration::ZERO,
1236 error_summary: None,
1237 output_error_slice: None,
1238 }]),
1239 current_stats: finished_stats,
1240 running: 2,
1241 },
1242 };
1243
1244 state.update_progress_bar(&finished_event, &styles);
1245
1246 assert_eq!(
1248 state.stats, finished_stats,
1249 "stats should be updated on TestFinished"
1250 );
1251 assert_eq!(
1252 state.running, 2,
1253 "running should be updated on TestFinished"
1254 );
1255
1256 let msg = state.progress_bar_msg(&styles);
1258 insta::assert_snapshot!(msg, @"2 running, 8 passed, 0 skipped");
1259
1260 let cancel_stats = RunStats {
1262 initial_run_count: 10,
1263 finished_count: 3,
1264 passed: 2,
1265 failed: 1,
1266 cancel_reason: Some(CancelReason::TestFailure),
1267 ..RunStats::default()
1268 };
1269 let cancel_event = TestEvent {
1270 timestamp: Local::now().fixed_offset(),
1271 elapsed: Duration::ZERO,
1272 kind: TestEventKind::RunBeginCancel {
1273 setup_scripts_running: 0,
1274 current_stats: cancel_stats,
1275 running: 4,
1276 },
1277 };
1278
1279 state.update_progress_bar(&cancel_event, &styles);
1280
1281 assert_eq!(
1283 state.stats, cancel_stats,
1284 "stats should be updated on RunBeginCancel"
1285 );
1286 assert_eq!(
1287 state.running, 4,
1288 "running should be updated on RunBeginCancel"
1289 );
1290
1291 let msg = state.progress_bar_msg(&styles);
1293 insta::assert_snapshot!(msg, @"4 running, 2 passed, 1 failed, 0 skipped");
1294
1295 let kill_stats = RunStats {
1297 initial_run_count: 10,
1298 finished_count: 5,
1299 passed: 3,
1300 failed: 2,
1301 cancel_reason: Some(CancelReason::Signal),
1302 ..RunStats::default()
1303 };
1304 let kill_event = TestEvent {
1305 timestamp: Local::now().fixed_offset(),
1306 elapsed: Duration::ZERO,
1307 kind: TestEventKind::RunBeginKill {
1308 setup_scripts_running: 0,
1309 current_stats: kill_stats,
1310 running: 2,
1311 },
1312 };
1313
1314 state.update_progress_bar(&kill_event, &styles);
1315
1316 assert_eq!(
1318 state.stats, kill_stats,
1319 "stats should be updated on RunBeginKill"
1320 );
1321 assert_eq!(
1322 state.running, 2,
1323 "running should be updated on RunBeginKill"
1324 );
1325
1326 let msg = state.progress_bar_msg(&styles);
1328 insta::assert_snapshot!(msg, @"2 running, 3 passed, 2 failed, 0 skipped");
1329 }
1330
1331 fn make_test_output() -> ChildExecutionOutputDescription<ChildSingleOutput> {
1333 ChildExecutionOutput::Output {
1334 result: Some(ExecutionResult::Pass),
1335 output: ChildOutput::Split(ChildSplitOutput {
1336 stdout: Some(Bytes::new().into()),
1337 stderr: Some(Bytes::new().into()),
1338 }),
1339 errors: None,
1340 }
1341 .into()
1342 }
1343}