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