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