1use super::{
10 PruneKind, PrunePlan, PruneResult, RecordedRunInfo, RecordedRunStatus,
11 SnapshotWithReplayability,
12 run_id_index::RunIdIndex,
13 store::{CompletedRunStats, ReplayabilityStatus, StressCompletedRunStats},
14 tree::{RunInfo, RunTree, TreeIterItem},
15};
16use crate::{
17 helpers::{ThemeCharacters, plural},
18 indenter::indented,
19 redact::{Redactor, SizeDisplay},
20};
21use camino::Utf8Path;
22use chrono::{DateTime, Utc};
23use owo_colors::{OwoColorize, Style};
24use quick_junit::ReportUuid;
25use std::{
26 collections::HashMap,
27 error::Error,
28 fmt::{self, Write as _},
29};
30use swrite::{SWrite, swrite};
31
32const FIELD_VALUE_INDENT: &str = " ";
35
36#[derive(Clone, Debug, Default)]
38pub struct Styles {
39 pub run_id_prefix: Style,
41 pub run_id_rest: Style,
43 pub timestamp: Style,
45 pub duration: Style,
47 pub size: Style,
49 pub count: Style,
51 pub passed: Style,
53 pub failed: Style,
55 pub cancelled: Style,
57 pub label: Style,
59 pub section: Style,
61}
62
63impl Styles {
64 pub fn colorize(&mut self) {
66 self.run_id_prefix = Style::new().bold().purple();
67 self.run_id_rest = Style::new().bright_black();
68 self.timestamp = Style::new();
69 self.duration = Style::new();
70 self.size = Style::new();
71 self.count = Style::new().bold();
72 self.passed = Style::new().bold().green();
73 self.failed = Style::new().bold().red();
74 self.cancelled = Style::new().bold().yellow();
75 self.label = Style::new().bold();
76 self.section = Style::new().bold();
77 }
78
79 pub fn format_run_id(&self, run_id: ReportUuid, run_id_index: Option<&RunIdIndex>) -> String {
85 let run_id_str = run_id.to_string();
86 if let Some(index) = run_id_index
87 && let Some(prefix_info) = index.shortest_unique_prefix(run_id)
88 {
89 let prefix_len = prefix_info.prefix.len().min(run_id_str.len());
90 let (prefix_part, rest_part) = run_id_str.split_at(prefix_len);
91 format!(
92 "{}{}",
93 prefix_part.style(self.run_id_prefix),
94 rest_part.style(self.run_id_rest),
95 )
96 } else {
97 run_id_str.style(self.label).to_string()
98 }
99 }
100
101 pub fn format_run_id_short(
106 &self,
107 run_id: ReportUuid,
108 run_id_index: Option<&RunIdIndex>,
109 ) -> String {
110 let full_short: String = run_id.to_string().chars().take(8).collect();
111 if let Some(index) = run_id_index
112 && let Some(prefix_info) = index.shortest_unique_prefix(run_id)
113 {
114 let prefix_len = prefix_info.prefix.len().min(8);
115 let (prefix_part, rest_part) = full_short.split_at(prefix_len);
116 format!(
117 "{}{}",
118 prefix_part.style(self.run_id_prefix),
119 rest_part.style(self.run_id_rest),
120 )
121 } else {
122 full_short.style(self.label).to_string()
123 }
124 }
125}
126
127#[derive(Clone, Copy, Debug, Default)]
133pub struct RunListAlignment {
134 pub passed_width: usize,
136 pub size_width: usize,
142 pub tree_prefix_width: usize,
148}
149
150impl RunListAlignment {
151 const MIN_SIZE_WIDTH: usize = 9;
153
154 pub fn from_runs(runs: &[RecordedRunInfo]) -> Self {
158 let passed_width = runs
159 .iter()
160 .map(|run| run.status.passed_count_width())
161 .max()
162 .unwrap_or(1);
163
164 let size_width = runs
165 .iter()
166 .map(|run| SizeDisplay(run.sizes.total_compressed()).display_width())
167 .max()
168 .unwrap_or(Self::MIN_SIZE_WIDTH)
169 .max(Self::MIN_SIZE_WIDTH);
170
171 Self {
172 passed_width,
173 size_width,
174 tree_prefix_width: 0,
175 }
176 }
177
178 pub fn from_runs_with_total(runs: &[RecordedRunInfo], total_size_bytes: u64) -> Self {
184 let passed_width = runs
185 .iter()
186 .map(|run| run.status.passed_count_width())
187 .max()
188 .unwrap_or(1);
189
190 let max_run_size_width = runs
191 .iter()
192 .map(|run| SizeDisplay(run.sizes.total_compressed()).display_width())
193 .max()
194 .unwrap_or(0);
195
196 let total_size_width = SizeDisplay(total_size_bytes).display_width();
197
198 let size_width = max_run_size_width
199 .max(total_size_width)
200 .max(Self::MIN_SIZE_WIDTH);
201
202 Self {
203 passed_width,
204 size_width,
205 tree_prefix_width: 0,
206 }
207 }
208
209 pub(super) fn with_tree(mut self, tree: &RunTree) -> Self {
214 self.tree_prefix_width = tree
215 .iter()
216 .map(|item| item.tree_prefix_width())
217 .max()
218 .unwrap_or(0);
219 self
220 }
221}
222
223#[derive(Clone, Debug)]
228pub struct DisplayPruneResult<'a> {
229 pub(super) result: &'a PruneResult,
230 pub(super) styles: &'a Styles,
231}
232
233impl fmt::Display for DisplayPruneResult<'_> {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 let result = self.result;
236 if result.deleted_count == 0 && result.orphans_deleted == 0 {
237 if result.errors.is_empty() {
238 writeln!(f, "no runs to prune")?;
239 } else {
240 writeln!(
241 f,
242 "no runs pruned ({} {} occurred)",
243 result.errors.len().style(self.styles.count),
244 plural::errors_str(result.errors.len()),
245 )?;
246 }
247 } else {
248 let orphan_suffix = if result.orphans_deleted > 0 {
249 format!(
250 ", {} {}",
251 result.orphans_deleted.style(self.styles.count),
252 plural::orphans_str(result.orphans_deleted)
253 )
254 } else {
255 String::new()
256 };
257 let error_suffix = if result.errors.is_empty() {
258 String::new()
259 } else {
260 format!(
261 " ({} {} occurred)",
262 result.errors.len().style(self.styles.count),
263 plural::errors_str(result.errors.len()),
264 )
265 };
266 writeln!(
267 f,
268 "pruned {} {}{}, freed {}{}",
269 result.deleted_count.style(self.styles.count),
270 plural::runs_str(result.deleted_count),
271 orphan_suffix,
272 SizeDisplay(result.freed_bytes),
273 error_suffix,
274 )?;
275 }
276
277 if result.kind == PruneKind::Explicit && !result.errors.is_empty() {
279 writeln!(f)?;
280 writeln!(f, "errors:")?;
281 for error in &result.errors {
282 write!(f, " - {error}")?;
283 let mut curr = error.source();
284 while let Some(source) = curr {
285 write!(f, ": {source}")?;
286 curr = source.source();
287 }
288 writeln!(f)?;
289 }
290 }
291
292 Ok(())
293 }
294}
295
296#[derive(Clone, Debug)]
301pub struct DisplayPrunePlan<'a> {
302 pub(super) plan: &'a PrunePlan,
303 pub(super) run_id_index: &'a RunIdIndex,
304 pub(super) styles: &'a Styles,
305 pub(super) redactor: &'a Redactor,
306}
307
308impl fmt::Display for DisplayPrunePlan<'_> {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 let plan = self.plan;
311 if plan.runs().is_empty() {
312 writeln!(f, "no runs would be pruned")
313 } else {
314 writeln!(
315 f,
316 "would prune {} {}, freeing {}:\n",
317 plan.runs().len().style(self.styles.count),
318 plural::runs_str(plan.runs().len()),
319 self.redactor.redact_size(plan.total_bytes())
320 )?;
321
322 let alignment = RunListAlignment::from_runs(plan.runs());
323 let replayable = ReplayabilityStatus::Replayable;
325 for run in plan.runs() {
326 writeln!(
327 f,
328 "{}",
329 run.display(
330 self.run_id_index,
331 &replayable,
332 alignment,
333 self.styles,
334 self.redactor
335 )
336 )?;
337 }
338 Ok(())
339 }
340 }
341}
342
343#[derive(Clone, Debug)]
345pub struct DisplayRecordedRunInfo<'a> {
346 run: &'a RecordedRunInfo,
347 run_id_index: &'a RunIdIndex,
348 replayability: &'a ReplayabilityStatus,
349 alignment: RunListAlignment,
350 styles: &'a Styles,
351 redactor: &'a Redactor,
352 prefix: &'a str,
354 run_id_padding: usize,
356}
357
358impl fmt::Display for DisplayRecordedRunInfo<'_> {
359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360 let run = self.run;
361
362 let run_id_display = self
363 .styles
364 .format_run_id_short(run.run_id, Some(self.run_id_index));
365
366 let status_display = self.format_status();
367
368 let timestamp_display = self.redactor.redact_timestamp(&run.started_at);
369 let duration_display = self.redactor.redact_store_duration(run.duration_secs);
370 let size_display = self.redactor.redact_size(run.sizes.total_compressed());
371
372 write!(
373 f,
374 "{}{}{:padding$} {} {} {:>width$} {}",
375 self.prefix,
376 run_id_display,
377 "",
378 timestamp_display.style(self.styles.timestamp),
379 duration_display.style(self.styles.duration),
380 size_display.style(self.styles.size),
381 status_display,
382 padding = self.run_id_padding,
383 width = self.alignment.size_width,
384 )?;
385
386 match self.replayability {
388 ReplayabilityStatus::Replayable => {}
389 ReplayabilityStatus::NotReplayable(_) => {
390 write!(f, " ({})", "not replayable".style(self.styles.failed))?;
391 }
392 ReplayabilityStatus::Incomplete => {
393 }
396 }
397
398 Ok(())
399 }
400}
401
402impl<'a> DisplayRecordedRunInfo<'a> {
403 pub(super) fn new(
404 run: &'a RecordedRunInfo,
405 run_id_index: &'a RunIdIndex,
406 replayability: &'a ReplayabilityStatus,
407 alignment: RunListAlignment,
408 styles: &'a Styles,
409 redactor: &'a Redactor,
410 ) -> Self {
411 Self {
412 run,
413 run_id_index,
414 replayability,
415 alignment,
416 styles,
417 redactor,
418 prefix: " ",
419 run_id_padding: 0,
420 }
421 }
422
423 pub(super) fn with_tree_formatting(mut self, prefix: &'a str, run_id_padding: usize) -> Self {
427 self.prefix = prefix;
428 self.run_id_padding = run_id_padding;
429 self
430 }
431
432 fn format_status(&self) -> String {
434 match &self.run.status {
435 RecordedRunStatus::Incomplete => {
436 format!(
437 "{:>width$} {}",
438 "",
439 "incomplete".style(self.styles.cancelled),
440 width = self.alignment.passed_width,
441 )
442 }
443 RecordedRunStatus::Unknown => {
444 format!(
445 "{:>width$} {}",
446 "",
447 "unknown".style(self.styles.cancelled),
448 width = self.alignment.passed_width,
449 )
450 }
451 RecordedRunStatus::Completed(stats) => self.format_normal_stats(stats, false),
452 RecordedRunStatus::Cancelled(stats) => self.format_normal_stats(stats, true),
453 RecordedRunStatus::StressCompleted(stats) => self.format_stress_stats(stats, false),
454 RecordedRunStatus::StressCancelled(stats) => self.format_stress_stats(stats, true),
455 }
456 }
457
458 fn format_normal_stats(&self, stats: &CompletedRunStats, cancelled: bool) -> String {
460 if stats.initial_run_count == 0 {
463 return format!(
464 "{:>width$} {}",
465 0.style(self.styles.count),
466 "passed".style(self.styles.cancelled),
467 width = self.alignment.passed_width,
468 );
469 }
470
471 let mut result = String::new();
472
473 swrite!(
475 result,
476 "{:>width$} {}",
477 stats.passed.style(self.styles.count),
478 "passed".style(self.styles.passed),
479 width = self.alignment.passed_width,
480 );
481
482 if stats.failed > 0 {
483 swrite!(
484 result,
485 " / {} {}",
486 stats.failed.style(self.styles.count),
487 "failed".style(self.styles.failed),
488 );
489 }
490
491 let not_run = stats
493 .initial_run_count
494 .saturating_sub(stats.passed)
495 .saturating_sub(stats.failed);
496 if not_run > 0 {
497 swrite!(
498 result,
499 " / {} {}",
500 not_run.style(self.styles.count),
501 "not run".style(self.styles.cancelled),
502 );
503 }
504
505 if cancelled {
506 swrite!(result, " {}", "(cancelled)".style(self.styles.cancelled));
507 }
508
509 result
510 }
511
512 fn format_stress_stats(&self, stats: &StressCompletedRunStats, cancelled: bool) -> String {
514 let mut result = String::new();
515
516 swrite!(
518 result,
519 "{:>width$} {} {}",
520 stats.success_count.style(self.styles.count),
521 "passed".style(self.styles.passed),
522 plural::iterations_str(stats.success_count),
523 width = self.alignment.passed_width,
524 );
525
526 if stats.failed_count > 0 {
527 swrite!(
528 result,
529 " / {} {}",
530 stats.failed_count.style(self.styles.count),
531 "failed".style(self.styles.failed),
532 );
533 }
534
535 if let Some(initial) = stats.initial_iteration_count {
538 let not_run = initial
539 .get()
540 .saturating_sub(stats.success_count)
541 .saturating_sub(stats.failed_count);
542 if not_run > 0 {
543 swrite!(
544 result,
545 " / {} {}",
546 not_run.style(self.styles.count),
547 "not run".style(self.styles.cancelled),
548 );
549 }
550 }
551
552 if cancelled {
553 swrite!(result, " {}", "(cancelled)".style(self.styles.cancelled));
554 }
555
556 result
557 }
558}
559
560pub struct DisplayRecordedRunInfoDetailed<'a> {
565 run: &'a RecordedRunInfo,
566 run_id_index: &'a RunIdIndex,
567 replayability: &'a ReplayabilityStatus,
568 now: DateTime<Utc>,
569 styles: &'a Styles,
570 theme_characters: &'a ThemeCharacters,
571 redactor: &'a Redactor,
572}
573
574impl<'a> DisplayRecordedRunInfoDetailed<'a> {
575 pub(super) fn new(
576 run: &'a RecordedRunInfo,
577 run_id_index: &'a RunIdIndex,
578 replayability: &'a ReplayabilityStatus,
579 now: DateTime<Utc>,
580 styles: &'a Styles,
581 theme_characters: &'a ThemeCharacters,
582 redactor: &'a Redactor,
583 ) -> Self {
584 Self {
585 run,
586 run_id_index,
587 replayability,
588 now,
589 styles,
590 theme_characters,
591 redactor,
592 }
593 }
594
595 fn format_run_id(&self) -> String {
597 self.format_run_id_with_prefix(self.run.run_id)
598 }
599
600 fn format_run_id_with_prefix(&self, run_id: ReportUuid) -> String {
602 self.styles.format_run_id(run_id, Some(self.run_id_index))
603 }
604
605 fn write_field(
607 &self,
608 f: &mut fmt::Formatter<'_>,
609 label: &str,
610 value: impl fmt::Display,
611 ) -> fmt::Result {
612 write!(f, " {:18}", format!("{}:", label).style(self.styles.label))?;
613 let mut value_writer = indented(f).with_str(FIELD_VALUE_INDENT).skip_initial();
616 writeln!(value_writer, "{value}")
617 }
618
619 fn write_status_field(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
621 let status_str = self.run.status.short_status_str();
622 let exit_code = self.run.status.exit_code();
623
624 match exit_code {
625 Some(code) => {
626 let exit_code_style = if code == 0 {
627 self.styles.passed
628 } else {
629 self.styles.failed
630 };
631 self.write_field(
632 f,
633 "status",
634 format!("{} (exit code {})", status_str, code.style(exit_code_style)),
635 )
636 }
637 None => self.write_field(f, "status", status_str),
638 }
639 }
640
641 fn write_replayable(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 match self.replayability {
644 ReplayabilityStatus::Replayable => {
645 self.write_field(f, "replayable", "yes".style(self.styles.passed))
646 }
647 ReplayabilityStatus::NotReplayable(reasons) => {
648 let mut reasons_str = String::new();
649 for reason in reasons {
650 if !reasons_str.is_empty() {
651 swrite!(reasons_str, ", {reason}");
652 } else {
653 swrite!(reasons_str, "{reason}");
654 }
655 }
656 self.write_field(
657 f,
658 "replayable",
659 format!("{}: {}", "no".style(self.styles.failed), reasons_str),
660 )
661 }
662 ReplayabilityStatus::Incomplete => self.write_field(
663 f,
664 "replayable",
665 format!(
666 "{}: run is incomplete (archive may be partial)",
667 "maybe".style(self.styles.cancelled)
668 ),
669 ),
670 }
671 }
672
673 fn write_stats_section(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
675 match &self.run.status {
676 RecordedRunStatus::Incomplete | RecordedRunStatus::Unknown => {
677 Ok(())
679 }
680 RecordedRunStatus::Completed(stats) | RecordedRunStatus::Cancelled(stats) => {
681 writeln!(f, " {}:", "tests".style(self.styles.section))?;
682 writeln!(
683 f,
684 " {:16}{}",
685 "passed:".style(self.styles.label),
686 stats.passed.style(self.styles.passed),
687 )?;
688 if stats.failed > 0 {
689 writeln!(
690 f,
691 " {:16}{}",
692 "failed:".style(self.styles.label),
693 stats.failed.style(self.styles.failed),
694 )?;
695 }
696 let not_run = stats
697 .initial_run_count
698 .saturating_sub(stats.passed)
699 .saturating_sub(stats.failed);
700 if not_run > 0 {
701 writeln!(
702 f,
703 " {:16}{}",
704 "not run:".style(self.styles.label),
705 not_run.style(self.styles.cancelled),
706 )?;
707 }
708 writeln!(f)
709 }
710 RecordedRunStatus::StressCompleted(stats)
711 | RecordedRunStatus::StressCancelled(stats) => {
712 writeln!(f, " {}:", "iterations".style(self.styles.section))?;
713 writeln!(
714 f,
715 " {:16}{}",
716 "passed:".style(self.styles.label),
717 stats.success_count.style(self.styles.passed),
718 )?;
719 if stats.failed_count > 0 {
720 writeln!(
721 f,
722 " {:16}{}",
723 "failed:".style(self.styles.label),
724 stats.failed_count.style(self.styles.failed),
725 )?;
726 }
727 if let Some(initial) = stats.initial_iteration_count {
728 let not_run = initial
729 .get()
730 .saturating_sub(stats.success_count)
731 .saturating_sub(stats.failed_count);
732 if not_run > 0 {
733 writeln!(
734 f,
735 " {:16}{}",
736 "not run:".style(self.styles.label),
737 not_run.style(self.styles.cancelled),
738 )?;
739 }
740 }
741 writeln!(f)
742 }
743 }
744 }
745
746 fn write_sizes_section(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
748 writeln!(
751 f,
752 " {:18}{:>10} {:>12} {:>7}",
753 "sizes:".style(self.styles.section),
754 "compressed".style(self.styles.label),
755 "uncompressed".style(self.styles.label),
756 "entries".style(self.styles.label),
757 )?;
758
759 let sizes = &self.run.sizes;
760
761 writeln!(
762 f,
763 " {:16}{:>10} {:>12} {:>7}",
764 "log".style(self.styles.label),
765 self.redactor
766 .redact_size(sizes.log.compressed)
767 .style(self.styles.size),
768 self.redactor
769 .redact_size(sizes.log.uncompressed)
770 .style(self.styles.size),
771 sizes.log.entries.style(self.styles.size),
772 )?;
773
774 writeln!(
775 f,
776 " {:16}{:>10} {:>12} {:>7}",
777 "store".style(self.styles.label),
778 self.redactor
779 .redact_size(sizes.store.compressed)
780 .style(self.styles.size),
781 self.redactor
782 .redact_size(sizes.store.uncompressed)
783 .style(self.styles.size),
784 sizes.store.entries.style(self.styles.size),
785 )?;
786
787 writeln!(
789 f,
790 " {:16}{} {} {}",
791 "",
792 self.theme_characters.hbar(10),
793 self.theme_characters.hbar(12),
794 self.theme_characters.hbar(7),
795 )?;
796
797 writeln!(
798 f,
799 " {:16}{:>10} {:>12} {:>7}",
800 "total".style(self.styles.section),
801 self.redactor
802 .redact_size(sizes.total_compressed())
803 .style(self.styles.size),
804 self.redactor
805 .redact_size(sizes.total_uncompressed())
806 .style(self.styles.size),
807 sizes.total_entries().style(self.styles.size),
809 )
810 }
811
812 fn format_env_vars(&self) -> String {
814 self.redactor.redact_env_vars(&self.run.env_vars)
815 }
816
817 fn format_cli_args(&self) -> String {
819 self.redactor.redact_cli_args(&self.run.cli_args)
820 }
821}
822
823impl fmt::Display for DisplayRecordedRunInfoDetailed<'_> {
824 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
825 let run = self.run;
826
827 writeln!(
829 f,
830 "{} {}",
831 "Run".style(self.styles.section),
832 self.format_run_id()
833 )?;
834 writeln!(f)?;
835
836 self.write_field(
838 f,
839 "nextest version",
840 self.redactor.redact_version(&run.nextest_version),
841 )?;
842
843 if let Some(parent_run_id) = run.parent_run_id {
845 self.write_field(
846 f,
847 "parent run",
848 self.format_run_id_with_prefix(parent_run_id),
849 )?;
850 }
851
852 if !run.cli_args.is_empty() {
854 self.write_field(f, "command", self.format_cli_args())?;
855 }
856
857 if !run.env_vars.is_empty() {
859 self.write_field(f, "env", self.format_env_vars())?;
860 }
861
862 self.write_status_field(f)?;
863 let timestamp = self.redactor.redact_detailed_timestamp(&run.started_at);
865 let relative_duration = self
866 .now
867 .signed_duration_since(run.started_at.with_timezone(&Utc))
868 .to_std()
869 .unwrap_or(std::time::Duration::ZERO);
870 let relative_display = self.redactor.redact_relative_duration(relative_duration);
871 self.write_field(
872 f,
873 "started at",
874 format!(
875 "{} ({} ago)",
876 timestamp,
877 relative_display.style(self.styles.count)
878 ),
879 )?;
880 let last_written_timestamp = self
882 .redactor
883 .redact_detailed_timestamp(&run.last_written_at);
884 let last_written_relative_duration = self
885 .now
886 .signed_duration_since(run.last_written_at.with_timezone(&Utc))
887 .to_std()
888 .unwrap_or(std::time::Duration::ZERO);
889 let last_written_relative_display = self
890 .redactor
891 .redact_relative_duration(last_written_relative_duration);
892 self.write_field(
893 f,
894 "last written at",
895 format!(
896 "{} ({} ago)",
897 last_written_timestamp,
898 last_written_relative_display.style(self.styles.count)
899 ),
900 )?;
901 self.write_field(
902 f,
903 "duration",
904 self.redactor.redact_detailed_duration(run.duration_secs),
905 )?;
906
907 self.write_replayable(f)?;
908 writeln!(f)?;
909
910 self.write_stats_section(f)?;
912
913 self.write_sizes_section(f)?;
915
916 Ok(())
917 }
918}
919
920pub struct DisplayRunList<'a> {
928 snapshot_with_replayability: &'a SnapshotWithReplayability<'a>,
929 store_path: Option<&'a Utf8Path>,
930 styles: &'a Styles,
931 theme_characters: &'a ThemeCharacters,
932 redactor: &'a Redactor,
933}
934
935impl<'a> DisplayRunList<'a> {
936 pub fn new(
947 snapshot_with_replayability: &'a SnapshotWithReplayability<'a>,
948 store_path: Option<&'a Utf8Path>,
949 styles: &'a Styles,
950 theme_characters: &'a ThemeCharacters,
951 redactor: &'a Redactor,
952 ) -> Self {
953 Self {
954 snapshot_with_replayability,
955 store_path,
956 styles,
957 theme_characters,
958 redactor,
959 }
960 }
961}
962
963impl fmt::Display for DisplayRunList<'_> {
964 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
965 let snapshot = self.snapshot_with_replayability.snapshot();
966
967 if let Some(path) = self.store_path {
969 writeln!(f, "{}: {}\n", "store".style(self.styles.count), path)?;
970 }
971
972 if snapshot.run_count() == 0 {
973 return Ok(());
976 }
977
978 writeln!(
979 f,
980 "{} recorded {}:\n",
981 snapshot.run_count().style(self.styles.count),
982 plural::runs_str(snapshot.run_count()),
983 )?;
984
985 let tree = RunTree::build(
986 &snapshot
987 .runs()
988 .iter()
989 .map(|run| RunInfo {
990 run_id: run.run_id,
991 parent_run_id: run.parent_run_id,
992 started_at: run.started_at,
993 })
994 .collect::<Vec<_>>(),
995 );
996
997 let alignment =
998 RunListAlignment::from_runs_with_total(snapshot.runs(), snapshot.total_size())
999 .with_tree(&tree);
1000 let latest_run_id = self.snapshot_with_replayability.latest_run_id();
1001
1002 let run_map: HashMap<_, _> = snapshot.runs().iter().map(|r| (r.run_id, r)).collect();
1003
1004 for item in tree.iter() {
1005 let prefix = format_tree_prefix(self.theme_characters, item);
1006
1007 let run_id_padding = (alignment.tree_prefix_width - item.tree_prefix_width()) * 2;
1009
1010 match item.run_id {
1011 Some(run_id) => {
1012 let run = run_map
1013 .get(&run_id)
1014 .expect("run ID from tree should exist in snapshot");
1015 let replayability = self
1016 .snapshot_with_replayability
1017 .get_replayability(run.run_id);
1018
1019 let display = DisplayRecordedRunInfo::new(
1020 run,
1021 snapshot.run_id_index(),
1022 replayability,
1023 alignment,
1024 self.styles,
1025 self.redactor,
1026 )
1027 .with_tree_formatting(&prefix, run_id_padding);
1028
1029 if Some(run.run_id) == latest_run_id {
1030 writeln!(f, "{} *{}", display, "latest".style(self.styles.count))?;
1031 } else {
1032 writeln!(f, "{}", display)?;
1033 }
1034 }
1035 None => {
1036 let virtual_padding = run_id_padding + 5;
1038 writeln!(
1039 f,
1040 "{}{}{:padding$} {}",
1041 prefix,
1042 "???".style(self.styles.run_id_rest),
1043 "",
1044 "(pruned parent)".style(self.styles.cancelled),
1045 padding = virtual_padding,
1046 )?;
1047 }
1048 }
1049 }
1050
1051 let first_col_width = 2 + alignment.tree_prefix_width * 2 + 8;
1054 let total_line_spacing = first_col_width + 2 + 19 + 2 + 10 + 2;
1055
1056 writeln!(
1057 f,
1058 "{:spacing$}{}",
1059 "",
1060 self.theme_characters.hbar(alignment.size_width),
1061 spacing = total_line_spacing,
1062 )?;
1063
1064 let size_display = self.redactor.redact_size(snapshot.total_size());
1065 let size_formatted = format!("{:>width$}", size_display, width = alignment.size_width);
1066 writeln!(
1067 f,
1068 "{:spacing$}{}",
1069 "",
1070 size_formatted.style(self.styles.size),
1071 spacing = total_line_spacing,
1072 )?;
1073
1074 Ok(())
1075 }
1076}
1077
1078fn format_tree_prefix(theme_characters: &ThemeCharacters, item: &TreeIterItem) -> String {
1085 let mut prefix = String::new();
1086
1087 prefix.push_str(" ");
1089
1090 if item.depth == 0 {
1091 return prefix;
1092 }
1093
1094 for &has_continuation in &item.continuation_flags {
1095 if has_continuation {
1096 prefix.push_str(theme_characters.tree_continuation());
1097 } else {
1098 prefix.push_str(theme_characters.tree_space());
1099 }
1100 }
1101
1102 if !item.is_only_child {
1103 if item.is_last {
1104 prefix.push_str(theme_characters.tree_last());
1105 } else {
1106 prefix.push_str(theme_characters.tree_branch());
1107 }
1108 }
1109
1110 prefix
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116 use crate::{
1117 errors::RecordPruneError,
1118 helpers::ThemeCharacters,
1119 record::{
1120 CompletedRunStats, ComponentSizes, NonReplayableReason, PruneKind, PrunePlan,
1121 PruneResult, RecordedRunStatus, RecordedSizes, RunStoreSnapshot,
1122 SnapshotWithReplayability, StressCompletedRunStats,
1123 format::{STORE_FORMAT_VERSION, StoreFormatMajorVersion, StoreVersionIncompatibility},
1124 run_id_index::RunIdIndex,
1125 },
1126 redact::Redactor,
1127 };
1128 use chrono::{DateTime, Utc};
1129 use semver::Version;
1130 use std::{collections::BTreeMap, num::NonZero};
1131
1132 fn test_now() -> DateTime<Utc> {
1137 DateTime::parse_from_rfc3339("2024-06-25T13:00:30+00:00")
1138 .expect("valid datetime")
1139 .with_timezone(&Utc)
1140 }
1141
1142 fn make_run_info(
1144 uuid: &str,
1145 version: &str,
1146 started_at: &str,
1147 total_compressed_size: u64,
1148 status: RecordedRunStatus,
1149 ) -> RecordedRunInfo {
1150 make_run_info_with_duration(
1151 uuid,
1152 version,
1153 started_at,
1154 total_compressed_size,
1155 1.0,
1156 status,
1157 )
1158 }
1159
1160 fn make_run_info_with_duration(
1162 uuid: &str,
1163 version: &str,
1164 started_at: &str,
1165 total_compressed_size: u64,
1166 duration_secs: f64,
1167 status: RecordedRunStatus,
1168 ) -> RecordedRunInfo {
1169 let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
1170 RecordedRunInfo {
1172 run_id: uuid.parse().expect("valid UUID"),
1173 store_format_version: STORE_FORMAT_VERSION,
1174 nextest_version: Version::parse(version).expect("valid version"),
1175 started_at,
1176 last_written_at: started_at,
1177 duration_secs: Some(duration_secs),
1178 cli_args: Vec::new(),
1179 env_vars: BTreeMap::new(),
1180 parent_run_id: None,
1181 sizes: RecordedSizes {
1182 log: ComponentSizes::default(),
1183 store: ComponentSizes {
1184 compressed: total_compressed_size,
1185 uncompressed: total_compressed_size * 3,
1186 entries: 0,
1187 },
1188 },
1189 status,
1190 }
1191 }
1192
1193 fn make_run_info_with_cli_env(
1195 uuid: &str,
1196 version: &str,
1197 started_at: &str,
1198 cli_args: Vec<String>,
1199 env_vars: BTreeMap<String, String>,
1200 status: RecordedRunStatus,
1201 ) -> RecordedRunInfo {
1202 make_run_info_with_parent(uuid, version, started_at, cli_args, env_vars, None, status)
1203 }
1204
1205 fn make_run_info_with_parent(
1207 uuid: &str,
1208 version: &str,
1209 started_at: &str,
1210 cli_args: Vec<String>,
1211 env_vars: BTreeMap<String, String>,
1212 parent_run_id: Option<&str>,
1213 status: RecordedRunStatus,
1214 ) -> RecordedRunInfo {
1215 let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
1216 RecordedRunInfo {
1217 run_id: uuid.parse().expect("valid UUID"),
1218 store_format_version: STORE_FORMAT_VERSION,
1219 nextest_version: Version::parse(version).expect("valid version"),
1220 started_at,
1221 last_written_at: started_at,
1222 duration_secs: Some(12.345),
1223 cli_args,
1224 env_vars,
1225 parent_run_id: parent_run_id.map(|s| s.parse().expect("valid UUID")),
1226 sizes: RecordedSizes {
1227 log: ComponentSizes {
1228 compressed: 1024,
1229 uncompressed: 4096,
1230 entries: 100,
1231 },
1232 store: ComponentSizes {
1233 compressed: 51200,
1234 uncompressed: 204800,
1235 entries: 42,
1236 },
1237 },
1238 status,
1239 }
1240 }
1241
1242 #[test]
1243 fn test_display_prune_result_nothing_to_prune() {
1244 let result = PruneResult::default();
1245 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"no runs to prune");
1246 }
1247
1248 #[test]
1249 fn test_display_prune_result_nothing_pruned_with_error() {
1250 let result = PruneResult {
1251 kind: PruneKind::Implicit,
1252 deleted_count: 0,
1253 orphans_deleted: 0,
1254 freed_bytes: 0,
1255 errors: vec![RecordPruneError::DeleteOrphan {
1256 path: "/some/path".into(),
1257 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1258 }],
1259 };
1260 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"no runs pruned (1 error occurred)");
1261 }
1262
1263 #[test]
1264 fn test_display_prune_result_single_run() {
1265 let result = PruneResult {
1266 kind: PruneKind::Implicit,
1267 deleted_count: 1,
1268 orphans_deleted: 0,
1269 freed_bytes: 1024,
1270 errors: vec![],
1271 };
1272 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 1 run, freed 1 KB");
1273 }
1274
1275 #[test]
1276 fn test_display_prune_result_multiple_runs() {
1277 let result = PruneResult {
1278 kind: PruneKind::Implicit,
1279 deleted_count: 3,
1280 orphans_deleted: 0,
1281 freed_bytes: 5 * 1024 * 1024,
1282 errors: vec![],
1283 };
1284 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 3 runs, freed 5.0 MB");
1285 }
1286
1287 #[test]
1288 fn test_display_prune_result_with_orphan() {
1289 let result = PruneResult {
1290 kind: PruneKind::Implicit,
1291 deleted_count: 2,
1292 orphans_deleted: 1,
1293 freed_bytes: 3 * 1024 * 1024,
1294 errors: vec![],
1295 };
1296 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 2 runs, 1 orphan, freed 3.0 MB");
1297 }
1298
1299 #[test]
1300 fn test_display_prune_result_with_multiple_orphans() {
1301 let result = PruneResult {
1302 kind: PruneKind::Implicit,
1303 deleted_count: 1,
1304 orphans_deleted: 3,
1305 freed_bytes: 2 * 1024 * 1024,
1306 errors: vec![],
1307 };
1308 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 1 run, 3 orphans, freed 2.0 MB");
1309 }
1310
1311 #[test]
1312 fn test_display_prune_result_with_errors_implicit() {
1313 let result = PruneResult {
1314 kind: PruneKind::Implicit,
1315 deleted_count: 2,
1316 orphans_deleted: 0,
1317 freed_bytes: 1024 * 1024,
1318 errors: vec![
1319 RecordPruneError::DeleteOrphan {
1320 path: "/path1".into(),
1321 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1322 },
1323 RecordPruneError::DeleteOrphan {
1324 path: "/path2".into(),
1325 error: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
1326 },
1327 ],
1328 };
1329 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 2 runs, freed 1.0 MB (2 errors occurred)");
1331 }
1332
1333 #[test]
1334 fn test_display_prune_result_with_errors_explicit() {
1335 let result = PruneResult {
1336 kind: PruneKind::Explicit,
1337 deleted_count: 2,
1338 orphans_deleted: 0,
1339 freed_bytes: 1024 * 1024,
1340 errors: vec![
1341 RecordPruneError::DeleteOrphan {
1342 path: "/path1".into(),
1343 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1344 },
1345 RecordPruneError::DeleteOrphan {
1346 path: "/path2".into(),
1347 error: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
1348 },
1349 ],
1350 };
1351 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"
1353 pruned 2 runs, freed 1.0 MB (2 errors occurred)
1354
1355 errors:
1356 - error deleting orphaned directory `/path1`: denied
1357 - error deleting orphaned directory `/path2`: not found
1358 ");
1359 }
1360
1361 #[test]
1362 fn test_display_prune_result_full() {
1363 let result = PruneResult {
1364 kind: PruneKind::Implicit,
1365 deleted_count: 5,
1366 orphans_deleted: 2,
1367 freed_bytes: 10 * 1024 * 1024,
1368 errors: vec![RecordPruneError::DeleteOrphan {
1369 path: "/orphan".into(),
1370 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1371 }],
1372 };
1373 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 5 runs, 2 orphans, freed 10.0 MB (1 error occurred)");
1374 }
1375
1376 #[test]
1377 fn test_display_recorded_run_info_completed() {
1378 let run = make_run_info(
1379 "550e8400-e29b-41d4-a716-446655440000",
1380 "0.9.100",
1381 "2024-06-15T10:30:00+00:00",
1382 102400,
1383 RecordedRunStatus::Completed(CompletedRunStats {
1384 initial_run_count: 100,
1385 passed: 95,
1386 failed: 5,
1387 exit_code: 100,
1388 }),
1389 );
1390 let runs = std::slice::from_ref(&run);
1391 let index = RunIdIndex::new(runs);
1392 let alignment = RunListAlignment::from_runs(runs);
1393 insta::assert_snapshot!(
1394 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1395 .to_string(),
1396 @" 550e8400 2024-06-15 10:30:00 1.000s 100 KB 95 passed / 5 failed"
1397 );
1398 }
1399
1400 #[test]
1401 fn test_display_recorded_run_info_incomplete() {
1402 let run = make_run_info(
1403 "550e8400-e29b-41d4-a716-446655440001",
1404 "0.9.101",
1405 "2024-06-16T11:00:00+00:00",
1406 51200,
1407 RecordedRunStatus::Incomplete,
1408 );
1409 let runs = std::slice::from_ref(&run);
1410 let index = RunIdIndex::new(runs);
1411 let alignment = RunListAlignment::from_runs(runs);
1412 insta::assert_snapshot!(
1413 run.display(&index, &ReplayabilityStatus::Incomplete, alignment, &Styles::default(), &Redactor::noop())
1414 .to_string(),
1415 @" 550e8400 2024-06-16 11:00:00 1.000s 50 KB incomplete"
1416 );
1417 }
1418
1419 #[test]
1420 fn test_display_recorded_run_info_not_run() {
1421 let run = make_run_info(
1423 "550e8400-e29b-41d4-a716-446655440005",
1424 "0.9.105",
1425 "2024-06-20T15:00:00+00:00",
1426 75000,
1427 RecordedRunStatus::Completed(CompletedRunStats {
1428 initial_run_count: 17,
1429 passed: 10,
1430 failed: 6,
1431 exit_code: 100,
1432 }),
1433 );
1434 let runs = std::slice::from_ref(&run);
1435 let index = RunIdIndex::new(runs);
1436 let alignment = RunListAlignment::from_runs(runs);
1437 insta::assert_snapshot!(
1438 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1439 .to_string(),
1440 @" 550e8400 2024-06-20 15:00:00 1.000s 73 KB 10 passed / 6 failed / 1 not run"
1441 );
1442 }
1443
1444 #[test]
1445 fn test_display_recorded_run_info_no_tests() {
1446 let run = make_run_info(
1448 "550e8400-e29b-41d4-a716-44665544000c",
1449 "0.9.112",
1450 "2024-06-23T16:00:00+00:00",
1451 5000,
1452 RecordedRunStatus::Completed(CompletedRunStats {
1453 initial_run_count: 0,
1454 passed: 0,
1455 failed: 0,
1456 exit_code: 0,
1457 }),
1458 );
1459 let runs = std::slice::from_ref(&run);
1460 let index = RunIdIndex::new(runs);
1461 let alignment = RunListAlignment::from_runs(runs);
1462 insta::assert_snapshot!(
1463 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1464 .to_string(),
1465 @" 550e8400 2024-06-23 16:00:00 1.000s 4 KB 0 passed"
1466 );
1467 }
1468
1469 #[test]
1470 fn test_display_recorded_run_info_stress_completed() {
1471 let run = make_run_info(
1473 "550e8400-e29b-41d4-a716-446655440010",
1474 "0.9.120",
1475 "2024-06-25T10:00:00+00:00",
1476 150000,
1477 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1478 initial_iteration_count: NonZero::new(100),
1479 success_count: 100,
1480 failed_count: 0,
1481 exit_code: 0,
1482 }),
1483 );
1484 let runs = std::slice::from_ref(&run);
1485 let index = RunIdIndex::new(runs);
1486 let alignment = RunListAlignment::from_runs(runs);
1487 insta::assert_snapshot!(
1488 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1489 .to_string(),
1490 @" 550e8400 2024-06-25 10:00:00 1.000s 146 KB 100 passed iterations"
1491 );
1492
1493 let run = make_run_info(
1495 "550e8400-e29b-41d4-a716-446655440011",
1496 "0.9.120",
1497 "2024-06-25T11:00:00+00:00",
1498 150000,
1499 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1500 initial_iteration_count: NonZero::new(100),
1501 success_count: 95,
1502 failed_count: 5,
1503 exit_code: 0,
1504 }),
1505 );
1506 let runs = std::slice::from_ref(&run);
1507 let index = RunIdIndex::new(runs);
1508 let alignment = RunListAlignment::from_runs(runs);
1509 insta::assert_snapshot!(
1510 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1511 .to_string(),
1512 @" 550e8400 2024-06-25 11:00:00 1.000s 146 KB 95 passed iterations / 5 failed"
1513 );
1514 }
1515
1516 #[test]
1517 fn test_display_recorded_run_info_stress_cancelled() {
1518 let run = make_run_info(
1520 "550e8400-e29b-41d4-a716-446655440012",
1521 "0.9.120",
1522 "2024-06-25T12:00:00+00:00",
1523 100000,
1524 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
1525 initial_iteration_count: NonZero::new(100),
1526 success_count: 50,
1527 failed_count: 10,
1528 exit_code: 0,
1529 }),
1530 );
1531 let runs = std::slice::from_ref(&run);
1532 let index = RunIdIndex::new(runs);
1533 let alignment = RunListAlignment::from_runs(runs);
1534 insta::assert_snapshot!(
1535 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1536 .to_string(),
1537 @" 550e8400 2024-06-25 12:00:00 1.000s 97 KB 50 passed iterations / 10 failed / 40 not run (cancelled)"
1538 );
1539
1540 let run = make_run_info(
1542 "550e8400-e29b-41d4-a716-446655440013",
1543 "0.9.120",
1544 "2024-06-25T13:00:00+00:00",
1545 100000,
1546 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
1547 initial_iteration_count: None,
1548 success_count: 50,
1549 failed_count: 10,
1550 exit_code: 0,
1551 }),
1552 );
1553 let runs = std::slice::from_ref(&run);
1554 let index = RunIdIndex::new(runs);
1555 let alignment = RunListAlignment::from_runs(runs);
1556 insta::assert_snapshot!(
1557 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1558 .to_string(),
1559 @" 550e8400 2024-06-25 13:00:00 1.000s 97 KB 50 passed iterations / 10 failed (cancelled)"
1560 );
1561 }
1562
1563 #[test]
1564 fn test_display_alignment_multiple_runs() {
1565 let runs = vec![
1567 make_run_info(
1568 "550e8400-e29b-41d4-a716-446655440006",
1569 "0.9.106",
1570 "2024-06-21T10:00:00+00:00",
1571 100000,
1572 RecordedRunStatus::Completed(CompletedRunStats {
1573 initial_run_count: 559,
1574 passed: 559,
1575 failed: 0,
1576 exit_code: 0,
1577 }),
1578 ),
1579 make_run_info(
1580 "550e8400-e29b-41d4-a716-446655440007",
1581 "0.9.107",
1582 "2024-06-21T11:00:00+00:00",
1583 50000,
1584 RecordedRunStatus::Completed(CompletedRunStats {
1585 initial_run_count: 51,
1586 passed: 51,
1587 failed: 0,
1588 exit_code: 0,
1589 }),
1590 ),
1591 make_run_info(
1592 "550e8400-e29b-41d4-a716-446655440008",
1593 "0.9.108",
1594 "2024-06-21T12:00:00+00:00",
1595 30000,
1596 RecordedRunStatus::Completed(CompletedRunStats {
1597 initial_run_count: 17,
1598 passed: 10,
1599 failed: 6,
1600 exit_code: 0,
1601 }),
1602 ),
1603 ];
1604 let index = RunIdIndex::new(&runs);
1605 let alignment = RunListAlignment::from_runs(&runs);
1606
1607 insta::assert_snapshot!(
1609 runs[0]
1610 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1611 .to_string(),
1612 @" 550e8400 2024-06-21 10:00:00 1.000s 97 KB 559 passed"
1613 );
1614 insta::assert_snapshot!(
1615 runs[1]
1616 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1617 .to_string(),
1618 @" 550e8400 2024-06-21 11:00:00 1.000s 48 KB 51 passed"
1619 );
1620 insta::assert_snapshot!(
1621 runs[2]
1622 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1623 .to_string(),
1624 @" 550e8400 2024-06-21 12:00:00 1.000s 29 KB 10 passed / 6 failed / 1 not run"
1625 );
1626 }
1627
1628 #[test]
1629 fn test_display_stress_stats_alignment() {
1630 let runs = vec![
1632 make_run_info(
1633 "550e8400-e29b-41d4-a716-446655440009",
1634 "0.9.109",
1635 "2024-06-22T10:00:00+00:00",
1636 200000,
1637 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1638 initial_iteration_count: NonZero::new(1000),
1639 success_count: 1000,
1640 failed_count: 0,
1641 exit_code: 0,
1642 }),
1643 ),
1644 make_run_info(
1645 "550e8400-e29b-41d4-a716-44665544000a",
1646 "0.9.110",
1647 "2024-06-22T11:00:00+00:00",
1648 100000,
1649 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1650 initial_iteration_count: NonZero::new(100),
1651 success_count: 95,
1652 failed_count: 5,
1653 exit_code: 0,
1654 }),
1655 ),
1656 make_run_info(
1657 "550e8400-e29b-41d4-a716-44665544000b",
1658 "0.9.111",
1659 "2024-06-22T12:00:00+00:00",
1660 80000,
1661 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
1662 initial_iteration_count: NonZero::new(500),
1663 success_count: 45,
1664 failed_count: 5,
1665 exit_code: 0,
1666 }),
1667 ),
1668 ];
1669 let index = RunIdIndex::new(&runs);
1670 let alignment = RunListAlignment::from_runs(&runs);
1671
1672 insta::assert_snapshot!(
1674 runs[0]
1675 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1676 .to_string(),
1677 @" 550e8400 2024-06-22 10:00:00 1.000s 195 KB 1000 passed iterations"
1678 );
1679 insta::assert_snapshot!(
1680 runs[1]
1681 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1682 .to_string(),
1683 @" 550e8400 2024-06-22 11:00:00 1.000s 97 KB 95 passed iterations / 5 failed"
1684 );
1685 insta::assert_snapshot!(
1686 runs[2]
1687 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1688 .to_string(),
1689 @" 550e8400 2024-06-22 12:00:00 1.000s 78 KB 45 passed iterations / 5 failed / 450 not run (cancelled)"
1690 );
1691 }
1692
1693 #[test]
1694 fn test_display_prune_plan_empty() {
1695 let plan = PrunePlan::new(vec![]);
1696 let index = RunIdIndex::new(&[]);
1697 insta::assert_snapshot!(
1698 plan.display(&index, &Styles::default(), &Redactor::noop())
1699 .to_string(),
1700 @"no runs would be pruned"
1701 );
1702 }
1703
1704 #[test]
1705 fn test_display_prune_plan_single_run() {
1706 let runs = vec![make_run_info(
1707 "550e8400-e29b-41d4-a716-446655440002",
1708 "0.9.102",
1709 "2024-06-17T12:00:00+00:00",
1710 2048 * 1024,
1711 RecordedRunStatus::Completed(CompletedRunStats {
1712 initial_run_count: 50,
1713 passed: 50,
1714 failed: 0,
1715 exit_code: 0,
1716 }),
1717 )];
1718 let index = RunIdIndex::new(&runs);
1719 let plan = PrunePlan::new(runs);
1720 insta::assert_snapshot!(
1721 plan.display(&index, &Styles::default(), &Redactor::noop())
1722 .to_string(),
1723 @"
1724 would prune 1 run, freeing 2.0 MB:
1725
1726 550e8400 2024-06-17 12:00:00 1.000s 2.0 MB 50 passed
1727 "
1728 );
1729 }
1730
1731 #[test]
1732 fn test_display_prune_plan_multiple_runs() {
1733 let runs = vec![
1734 make_run_info(
1735 "550e8400-e29b-41d4-a716-446655440003",
1736 "0.9.103",
1737 "2024-06-18T13:00:00+00:00",
1738 1024 * 1024,
1739 RecordedRunStatus::Completed(CompletedRunStats {
1740 initial_run_count: 100,
1741 passed: 100,
1742 failed: 0,
1743 exit_code: 0,
1744 }),
1745 ),
1746 make_run_info(
1747 "550e8400-e29b-41d4-a716-446655440004",
1748 "0.9.104",
1749 "2024-06-19T14:00:00+00:00",
1750 512 * 1024,
1751 RecordedRunStatus::Incomplete,
1752 ),
1753 ];
1754 let index = RunIdIndex::new(&runs);
1755 let plan = PrunePlan::new(runs);
1756 insta::assert_snapshot!(
1757 plan.display(&index, &Styles::default(), &Redactor::noop())
1758 .to_string(),
1759 @"
1760 would prune 2 runs, freeing 1.5 MB:
1761
1762 550e8400 2024-06-18 13:00:00 1.000s 1.0 MB 100 passed
1763 550e8400 2024-06-19 14:00:00 1.000s 512 KB incomplete
1764 "
1765 );
1766 }
1767
1768 #[test]
1769 fn test_display_run_list() {
1770 let theme_characters = ThemeCharacters::default();
1771
1772 let runs = vec![
1774 make_run_info(
1775 "550e8400-e29b-41d4-a716-446655440001",
1776 "0.9.101",
1777 "2024-06-15T10:00:00+00:00",
1778 50 * 1024,
1779 RecordedRunStatus::Completed(CompletedRunStats {
1780 initial_run_count: 10,
1781 passed: 10,
1782 failed: 0,
1783 exit_code: 0,
1784 }),
1785 ),
1786 make_run_info(
1787 "550e8400-e29b-41d4-a716-446655440002",
1788 "0.9.102",
1789 "2024-06-16T11:00:00+00:00",
1790 75 * 1024,
1791 RecordedRunStatus::Completed(CompletedRunStats {
1792 initial_run_count: 20,
1793 passed: 18,
1794 failed: 2,
1795 exit_code: 0,
1796 }),
1797 ),
1798 make_run_info(
1799 "550e8400-e29b-41d4-a716-446655440003",
1800 "0.9.103",
1801 "2024-06-17T12:00:00+00:00",
1802 100 * 1024,
1803 RecordedRunStatus::Completed(CompletedRunStats {
1804 initial_run_count: 30,
1805 passed: 30,
1806 failed: 0,
1807 exit_code: 0,
1808 }),
1809 ),
1810 ];
1811 let snapshot = RunStoreSnapshot::new_for_test(runs);
1812 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1813 insta::assert_snapshot!(
1814 "normal_sizes",
1815 DisplayRunList::new(
1816 &snapshot_with_replayability,
1817 None,
1818 &Styles::default(),
1819 &theme_characters,
1820 &Redactor::noop()
1821 )
1822 .to_string()
1823 );
1824
1825 let runs = vec![
1827 make_run_info(
1828 "550e8400-e29b-41d4-a716-446655440001",
1829 "0.9.101",
1830 "2024-06-15T10:00:00+00:00",
1831 1024, RecordedRunStatus::Completed(CompletedRunStats {
1833 initial_run_count: 5,
1834 passed: 5,
1835 failed: 0,
1836 exit_code: 0,
1837 }),
1838 ),
1839 make_run_info(
1840 "550e8400-e29b-41d4-a716-446655440002",
1841 "0.9.102",
1842 "2024-06-16T11:00:00+00:00",
1843 99 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
1845 initial_run_count: 10,
1846 passed: 10,
1847 failed: 0,
1848 exit_code: 0,
1849 }),
1850 ),
1851 ];
1852 let snapshot = RunStoreSnapshot::new_for_test(runs);
1853 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1854 insta::assert_snapshot!(
1855 "small_sizes",
1856 DisplayRunList::new(
1857 &snapshot_with_replayability,
1858 None,
1859 &Styles::default(),
1860 &theme_characters,
1861 &Redactor::noop()
1862 )
1863 .to_string()
1864 );
1865
1866 let runs = vec![
1870 make_run_info(
1871 "550e8400-e29b-41d4-a716-446655440001",
1872 "0.9.101",
1873 "2024-06-15T10:00:00+00:00",
1874 1_000_000 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
1876 initial_run_count: 100,
1877 passed: 100,
1878 failed: 0,
1879 exit_code: 0,
1880 }),
1881 ),
1882 make_run_info(
1883 "550e8400-e29b-41d4-a716-446655440002",
1884 "0.9.102",
1885 "2024-06-16T11:00:00+00:00",
1886 10_000_000 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
1888 initial_run_count: 200,
1889 passed: 200,
1890 failed: 0,
1891 exit_code: 0,
1892 }),
1893 ),
1894 ];
1895 let snapshot = RunStoreSnapshot::new_for_test(runs);
1896 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1897 insta::assert_snapshot!(
1898 "large_sizes",
1899 DisplayRunList::new(
1900 &snapshot_with_replayability,
1901 None,
1902 &Styles::default(),
1903 &theme_characters,
1904 &Redactor::noop()
1905 )
1906 .to_string()
1907 );
1908
1909 let runs = vec![
1912 make_run_info_with_duration(
1913 "550e8400-e29b-41d4-a716-446655440001",
1914 "0.9.101",
1915 "2024-06-15T10:00:00+00:00",
1916 50 * 1024,
1917 0.123, RecordedRunStatus::Completed(CompletedRunStats {
1919 initial_run_count: 5,
1920 passed: 5,
1921 failed: 0,
1922 exit_code: 0,
1923 }),
1924 ),
1925 make_run_info_with_duration(
1926 "550e8400-e29b-41d4-a716-446655440002",
1927 "0.9.102",
1928 "2024-06-16T11:00:00+00:00",
1929 75 * 1024,
1930 9.876, RecordedRunStatus::Completed(CompletedRunStats {
1932 initial_run_count: 10,
1933 passed: 10,
1934 failed: 0,
1935 exit_code: 0,
1936 }),
1937 ),
1938 make_run_info_with_duration(
1939 "550e8400-e29b-41d4-a716-446655440003",
1940 "0.9.103",
1941 "2024-06-17T12:00:00+00:00",
1942 100 * 1024,
1943 42.5, RecordedRunStatus::Completed(CompletedRunStats {
1945 initial_run_count: 20,
1946 passed: 20,
1947 failed: 0,
1948 exit_code: 0,
1949 }),
1950 ),
1951 make_run_info_with_duration(
1952 "550e8400-e29b-41d4-a716-446655440004",
1953 "0.9.104",
1954 "2024-06-18T13:00:00+00:00",
1955 125 * 1024,
1956 987.654, RecordedRunStatus::Completed(CompletedRunStats {
1958 initial_run_count: 30,
1959 passed: 28,
1960 failed: 2,
1961 exit_code: 0,
1962 }),
1963 ),
1964 make_run_info_with_duration(
1965 "550e8400-e29b-41d4-a716-446655440005",
1966 "0.9.105",
1967 "2024-06-19T14:00:00+00:00",
1968 150 * 1024,
1969 12345.678, RecordedRunStatus::Completed(CompletedRunStats {
1971 initial_run_count: 50,
1972 passed: 50,
1973 failed: 0,
1974 exit_code: 0,
1975 }),
1976 ),
1977 ];
1978 let snapshot = RunStoreSnapshot::new_for_test(runs);
1979 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1980 insta::assert_snapshot!(
1981 "varying_durations",
1982 DisplayRunList::new(
1983 &snapshot_with_replayability,
1984 None,
1985 &Styles::default(),
1986 &theme_characters,
1987 &Redactor::noop()
1988 )
1989 .to_string()
1990 );
1991 }
1992
1993 #[test]
1994 fn test_display_detailed() {
1995 let cli_args = vec![
1997 "cargo".to_string(),
1998 "nextest".to_string(),
1999 "run".to_string(),
2000 "--workspace".to_string(),
2001 "--features".to_string(),
2002 "foo bar".to_string(),
2003 ];
2004 let env_vars = BTreeMap::from([
2005 ("CARGO_HOME".to_string(), "/home/user/.cargo".to_string()),
2006 ("NEXTEST_PROFILE".to_string(), "ci".to_string()),
2007 ]);
2008 let run_with_cli_env = make_run_info_with_cli_env(
2009 "550e8400-e29b-41d4-a716-446655440000",
2010 "0.9.122",
2011 "2024-06-15T10:30:00+00:00",
2012 cli_args,
2013 env_vars,
2014 RecordedRunStatus::Completed(CompletedRunStats {
2015 initial_run_count: 100,
2016 passed: 95,
2017 failed: 5,
2018 exit_code: 100,
2019 }),
2020 );
2021
2022 let run_empty = make_run_info_with_cli_env(
2024 "550e8400-e29b-41d4-a716-446655440001",
2025 "0.9.122",
2026 "2024-06-16T11:00:00+00:00",
2027 Vec::new(),
2028 BTreeMap::new(),
2029 RecordedRunStatus::Incomplete,
2030 );
2031
2032 let stress_all_passed = make_run_info_with_cli_env(
2034 "550e8400-e29b-41d4-a716-446655440010",
2035 "0.9.122",
2036 "2024-06-25T10:00:00+00:00",
2037 Vec::new(),
2038 BTreeMap::new(),
2039 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
2040 initial_iteration_count: NonZero::new(100),
2041 success_count: 100,
2042 failed_count: 0,
2043 exit_code: 0,
2044 }),
2045 );
2046
2047 let stress_with_failures = make_run_info_with_cli_env(
2049 "550e8400-e29b-41d4-a716-446655440011",
2050 "0.9.122",
2051 "2024-06-25T11:00:00+00:00",
2052 Vec::new(),
2053 BTreeMap::new(),
2054 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
2055 initial_iteration_count: NonZero::new(100),
2056 success_count: 95,
2057 failed_count: 5,
2058 exit_code: 0,
2059 }),
2060 );
2061
2062 let stress_cancelled = make_run_info_with_cli_env(
2064 "550e8400-e29b-41d4-a716-446655440012",
2065 "0.9.122",
2066 "2024-06-25T12:00:00+00:00",
2067 Vec::new(),
2068 BTreeMap::new(),
2069 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
2070 initial_iteration_count: NonZero::new(100),
2071 success_count: 50,
2072 failed_count: 10,
2073 exit_code: 0,
2074 }),
2075 );
2076
2077 let stress_cancelled_no_initial = make_run_info_with_cli_env(
2079 "550e8400-e29b-41d4-a716-446655440013",
2080 "0.9.122",
2081 "2024-06-25T13:00:00+00:00",
2082 Vec::new(),
2083 BTreeMap::new(),
2084 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
2085 initial_iteration_count: None,
2086 success_count: 50,
2087 failed_count: 10,
2088 exit_code: 0,
2089 }),
2090 );
2091
2092 let runs = [
2093 run_with_cli_env,
2094 run_empty,
2095 stress_all_passed,
2096 stress_with_failures,
2097 stress_cancelled,
2098 stress_cancelled_no_initial,
2099 ];
2100 let index = RunIdIndex::new(&runs);
2101 let theme_characters = ThemeCharacters::default();
2102 let redactor = Redactor::noop();
2103 let now = test_now();
2104 let replayable = ReplayabilityStatus::Replayable;
2106 let incomplete = ReplayabilityStatus::Incomplete;
2108
2109 insta::assert_snapshot!(
2110 "with_cli_and_env",
2111 runs[0]
2112 .display_detailed(
2113 &index,
2114 &replayable,
2115 now,
2116 &Styles::default(),
2117 &theme_characters,
2118 &redactor
2119 )
2120 .to_string()
2121 );
2122 insta::assert_snapshot!(
2123 "empty_cli_and_env",
2124 runs[1]
2125 .display_detailed(
2126 &index,
2127 &incomplete,
2128 now,
2129 &Styles::default(),
2130 &theme_characters,
2131 &redactor
2132 )
2133 .to_string()
2134 );
2135 insta::assert_snapshot!(
2136 "stress_all_passed",
2137 runs[2]
2138 .display_detailed(
2139 &index,
2140 &replayable,
2141 now,
2142 &Styles::default(),
2143 &theme_characters,
2144 &redactor
2145 )
2146 .to_string()
2147 );
2148 insta::assert_snapshot!(
2149 "stress_with_failures",
2150 runs[3]
2151 .display_detailed(
2152 &index,
2153 &replayable,
2154 now,
2155 &Styles::default(),
2156 &theme_characters,
2157 &redactor
2158 )
2159 .to_string()
2160 );
2161 insta::assert_snapshot!(
2162 "stress_cancelled",
2163 runs[4]
2164 .display_detailed(
2165 &index,
2166 &replayable,
2167 now,
2168 &Styles::default(),
2169 &theme_characters,
2170 &redactor
2171 )
2172 .to_string()
2173 );
2174 insta::assert_snapshot!(
2175 "stress_cancelled_no_initial",
2176 runs[5]
2177 .display_detailed(
2178 &index,
2179 &replayable,
2180 now,
2181 &Styles::default(),
2182 &theme_characters,
2183 &redactor
2184 )
2185 .to_string()
2186 );
2187 }
2188
2189 #[test]
2190 fn test_display_detailed_with_parent_run() {
2191 let parent_run = make_run_info_with_cli_env(
2193 "550e8400-e29b-41d4-a716-446655440000",
2194 "0.9.122",
2195 "2024-06-15T10:00:00+00:00",
2196 Vec::new(),
2197 BTreeMap::new(),
2198 RecordedRunStatus::Completed(CompletedRunStats {
2199 initial_run_count: 100,
2200 passed: 95,
2201 failed: 5,
2202 exit_code: 100,
2203 }),
2204 );
2205
2206 let child_run = make_run_info_with_parent(
2207 "660e8400-e29b-41d4-a716-446655440001",
2208 "0.9.122",
2209 "2024-06-15T11:00:00+00:00",
2210 vec![
2211 "cargo".to_string(),
2212 "nextest".to_string(),
2213 "run".to_string(),
2214 "--rerun".to_string(),
2215 ],
2216 BTreeMap::new(),
2217 Some("550e8400-e29b-41d4-a716-446655440000"),
2218 RecordedRunStatus::Completed(CompletedRunStats {
2219 initial_run_count: 5,
2220 passed: 5,
2221 failed: 0,
2222 exit_code: 0,
2223 }),
2224 );
2225
2226 let runs = [parent_run, child_run];
2228 let index = RunIdIndex::new(&runs);
2229 let theme_characters = ThemeCharacters::default();
2230 let redactor = Redactor::noop();
2231 let now = test_now();
2232 let replayable = ReplayabilityStatus::Replayable;
2233
2234 insta::assert_snapshot!(
2235 "with_parent_run",
2236 runs[1]
2237 .display_detailed(
2238 &index,
2239 &replayable,
2240 now,
2241 &Styles::default(),
2242 &theme_characters,
2243 &redactor
2244 )
2245 .to_string()
2246 );
2247 }
2248
2249 #[test]
2250 fn test_display_replayability_statuses() {
2251 let run = make_run_info(
2253 "550e8400-e29b-41d4-a716-446655440000",
2254 "0.9.100",
2255 "2024-06-15T10:30:00+00:00",
2256 102400,
2257 RecordedRunStatus::Completed(CompletedRunStats {
2258 initial_run_count: 100,
2259 passed: 100,
2260 failed: 0,
2261 exit_code: 0,
2262 }),
2263 );
2264 let runs = std::slice::from_ref(&run);
2265 let index = RunIdIndex::new(runs);
2266 let theme_characters = ThemeCharacters::default();
2267 let redactor = Redactor::noop();
2268 let now = test_now();
2269
2270 let definitely_replayable = ReplayabilityStatus::Replayable;
2272 insta::assert_snapshot!(
2273 "replayability_yes",
2274 run.display_detailed(
2275 &index,
2276 &definitely_replayable,
2277 now,
2278 &Styles::default(),
2279 &theme_characters,
2280 &redactor
2281 )
2282 .to_string()
2283 );
2284
2285 let format_too_new = ReplayabilityStatus::NotReplayable(vec![
2287 NonReplayableReason::StoreVersionIncompatible {
2288 incompatibility: StoreVersionIncompatibility::RecordingTooNew {
2289 recording_major: StoreFormatMajorVersion::new(5),
2290 supported_major: StoreFormatMajorVersion::new(1),
2291 },
2292 },
2293 ]);
2294 insta::assert_snapshot!(
2295 "replayability_format_too_new",
2296 run.display_detailed(
2297 &index,
2298 &format_too_new,
2299 now,
2300 &Styles::default(),
2301 &theme_characters,
2302 &redactor
2303 )
2304 .to_string()
2305 );
2306
2307 let missing_store =
2309 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::MissingStoreZip]);
2310 insta::assert_snapshot!(
2311 "replayability_missing_store_zip",
2312 run.display_detailed(
2313 &index,
2314 &missing_store,
2315 now,
2316 &Styles::default(),
2317 &theme_characters,
2318 &redactor
2319 )
2320 .to_string()
2321 );
2322
2323 let missing_log =
2325 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::MissingRunLog]);
2326 insta::assert_snapshot!(
2327 "replayability_missing_run_log",
2328 run.display_detailed(
2329 &index,
2330 &missing_log,
2331 now,
2332 &Styles::default(),
2333 &theme_characters,
2334 &redactor
2335 )
2336 .to_string()
2337 );
2338
2339 let status_unknown =
2341 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::StatusUnknown]);
2342 insta::assert_snapshot!(
2343 "replayability_status_unknown",
2344 run.display_detailed(
2345 &index,
2346 &status_unknown,
2347 now,
2348 &Styles::default(),
2349 &theme_characters,
2350 &redactor
2351 )
2352 .to_string()
2353 );
2354
2355 let incomplete = ReplayabilityStatus::Incomplete;
2357 insta::assert_snapshot!(
2358 "replayability_incomplete",
2359 run.display_detailed(
2360 &index,
2361 &incomplete,
2362 now,
2363 &Styles::default(),
2364 &theme_characters,
2365 &redactor
2366 )
2367 .to_string()
2368 );
2369
2370 let multiple_blocking = ReplayabilityStatus::NotReplayable(vec![
2372 NonReplayableReason::MissingStoreZip,
2373 NonReplayableReason::MissingRunLog,
2374 ]);
2375 insta::assert_snapshot!(
2376 "replayability_multiple_blocking",
2377 run.display_detailed(
2378 &index,
2379 &multiple_blocking,
2380 now,
2381 &Styles::default(),
2382 &theme_characters,
2383 &redactor
2384 )
2385 .to_string()
2386 );
2387 }
2388
2389 fn make_run_for_tree(
2391 uuid: &str,
2392 started_at: &str,
2393 parent_run_id: Option<&str>,
2394 size_kb: u64,
2395 passed: usize,
2396 failed: usize,
2397 ) -> RecordedRunInfo {
2398 let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
2399 RecordedRunInfo {
2400 run_id: uuid.parse().expect("valid UUID"),
2401 store_format_version: STORE_FORMAT_VERSION,
2402 nextest_version: Version::parse("0.9.100").expect("valid version"),
2403 started_at,
2404 last_written_at: started_at,
2405 duration_secs: Some(1.0),
2406 cli_args: Vec::new(),
2407 env_vars: BTreeMap::new(),
2408 parent_run_id: parent_run_id.map(|s| s.parse().expect("valid UUID")),
2409 sizes: RecordedSizes {
2410 log: ComponentSizes::default(),
2411 store: ComponentSizes {
2412 compressed: size_kb * 1024,
2413 uncompressed: size_kb * 1024 * 3,
2414 entries: 0,
2415 },
2416 },
2417 status: RecordedRunStatus::Completed(CompletedRunStats {
2418 initial_run_count: passed + failed,
2419 passed,
2420 failed,
2421 exit_code: if failed > 0 { 1 } else { 0 },
2422 }),
2423 }
2424 }
2425
2426 #[test]
2427 fn test_tree_linear_chain() {
2428 let runs = vec![
2430 make_run_for_tree(
2431 "50000001-0000-0000-0000-000000000001",
2432 "2024-06-15T10:00:00+00:00",
2433 None,
2434 50,
2435 10,
2436 0,
2437 ),
2438 make_run_for_tree(
2439 "50000002-0000-0000-0000-000000000002",
2440 "2024-06-15T11:00:00+00:00",
2441 Some("50000001-0000-0000-0000-000000000001"),
2442 60,
2443 8,
2444 2,
2445 ),
2446 make_run_for_tree(
2447 "50000003-0000-0000-0000-000000000003",
2448 "2024-06-15T12:00:00+00:00",
2449 Some("50000002-0000-0000-0000-000000000002"),
2450 70,
2451 10,
2452 0,
2453 ),
2454 ];
2455 let snapshot = RunStoreSnapshot::new_for_test(runs);
2456 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2457 let theme_characters = ThemeCharacters::default();
2458
2459 insta::assert_snapshot!(
2460 "tree_linear_chain",
2461 DisplayRunList::new(
2462 &snapshot_with_replayability,
2463 None,
2464 &Styles::default(),
2465 &theme_characters,
2466 &Redactor::noop()
2467 )
2468 .to_string()
2469 );
2470 }
2471
2472 #[test]
2473 fn test_tree_branching() {
2474 let runs = vec![
2477 make_run_for_tree(
2478 "50000001-0000-0000-0000-000000000001",
2479 "2024-06-15T10:00:00+00:00",
2480 None,
2481 50,
2482 10,
2483 0,
2484 ),
2485 make_run_for_tree(
2486 "50000002-0000-0000-0000-000000000002",
2487 "2024-06-15T11:00:00+00:00",
2488 Some("50000001-0000-0000-0000-000000000001"),
2489 60,
2490 5,
2491 0,
2492 ),
2493 make_run_for_tree(
2494 "50000003-0000-0000-0000-000000000003",
2495 "2024-06-15T12:00:00+00:00",
2496 Some("50000001-0000-0000-0000-000000000001"),
2497 70,
2498 3,
2499 0,
2500 ),
2501 ];
2502 let snapshot = RunStoreSnapshot::new_for_test(runs);
2503 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2504 let theme_characters = ThemeCharacters::default();
2505
2506 insta::assert_snapshot!(
2507 "tree_branching",
2508 DisplayRunList::new(
2509 &snapshot_with_replayability,
2510 None,
2511 &Styles::default(),
2512 &theme_characters,
2513 &Redactor::noop()
2514 )
2515 .to_string()
2516 );
2517 }
2518
2519 #[test]
2520 fn test_tree_pruned_parent() {
2521 let runs = vec![make_run_for_tree(
2524 "50000002-0000-0000-0000-000000000002",
2525 "2024-06-15T11:00:00+00:00",
2526 Some("50000001-0000-0000-0000-000000000001"), 60,
2528 5,
2529 0,
2530 )];
2531 let snapshot = RunStoreSnapshot::new_for_test(runs);
2532 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2533 let theme_characters = ThemeCharacters::default();
2534
2535 insta::assert_snapshot!(
2536 "tree_pruned_parent",
2537 DisplayRunList::new(
2538 &snapshot_with_replayability,
2539 None,
2540 &Styles::default(),
2541 &theme_characters,
2542 &Redactor::noop()
2543 )
2544 .to_string()
2545 );
2546 }
2547
2548 #[test]
2549 fn test_tree_mixed_independent_and_chain() {
2550 let runs = vec![
2553 make_run_for_tree(
2554 "50000001-0000-0000-0000-000000000001",
2555 "2024-06-15T10:00:00+00:00",
2556 None,
2557 50,
2558 10,
2559 0,
2560 ),
2561 make_run_for_tree(
2562 "50000002-0000-0000-0000-000000000002",
2563 "2024-06-15T11:00:00+00:00",
2564 None,
2565 60,
2566 8,
2567 0,
2568 ),
2569 make_run_for_tree(
2570 "50000003-0000-0000-0000-000000000003",
2571 "2024-06-15T12:00:00+00:00",
2572 Some("50000002-0000-0000-0000-000000000002"),
2573 70,
2574 5,
2575 0,
2576 ),
2577 ];
2578 let snapshot = RunStoreSnapshot::new_for_test(runs);
2579 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2580 let theme_characters = ThemeCharacters::default();
2581
2582 insta::assert_snapshot!(
2583 "tree_mixed_independent_and_chain",
2584 DisplayRunList::new(
2585 &snapshot_with_replayability,
2586 None,
2587 &Styles::default(),
2588 &theme_characters,
2589 &Redactor::noop()
2590 )
2591 .to_string()
2592 );
2593 }
2594
2595 #[test]
2596 fn test_tree_deep_chain() {
2597 let runs = vec![
2600 make_run_for_tree(
2601 "50000001-0000-0000-0000-000000000001",
2602 "2024-06-15T10:00:00+00:00",
2603 None,
2604 50,
2605 10,
2606 0,
2607 ),
2608 make_run_for_tree(
2609 "50000002-0000-0000-0000-000000000002",
2610 "2024-06-15T11:00:00+00:00",
2611 Some("50000001-0000-0000-0000-000000000001"),
2612 60,
2613 10,
2614 0,
2615 ),
2616 make_run_for_tree(
2617 "50000003-0000-0000-0000-000000000003",
2618 "2024-06-15T12:00:00+00:00",
2619 Some("50000002-0000-0000-0000-000000000002"),
2620 70,
2621 10,
2622 0,
2623 ),
2624 make_run_for_tree(
2625 "50000004-0000-0000-0000-000000000004",
2626 "2024-06-15T13:00:00+00:00",
2627 Some("50000003-0000-0000-0000-000000000003"),
2628 80,
2629 10,
2630 0,
2631 ),
2632 make_run_for_tree(
2633 "50000005-0000-0000-0000-000000000005",
2634 "2024-06-15T14:00:00+00:00",
2635 Some("50000004-0000-0000-0000-000000000004"),
2636 90,
2637 10,
2638 0,
2639 ),
2640 ];
2641 let snapshot = RunStoreSnapshot::new_for_test(runs);
2642 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2643 let theme_characters = ThemeCharacters::default();
2644
2645 insta::assert_snapshot!(
2646 "tree_deep_chain",
2647 DisplayRunList::new(
2648 &snapshot_with_replayability,
2649 None,
2650 &Styles::default(),
2651 &theme_characters,
2652 &Redactor::noop()
2653 )
2654 .to_string()
2655 );
2656 }
2657
2658 #[test]
2659 fn test_tree_branching_with_chains() {
2660 let runs = vec![
2664 make_run_for_tree(
2665 "50000001-0000-0000-0000-000000000001",
2666 "2024-06-15T10:00:00+00:00",
2667 None,
2668 50,
2669 10,
2670 0,
2671 ),
2672 make_run_for_tree(
2673 "50000002-0000-0000-0000-000000000002",
2674 "2024-06-15T11:00:00+00:00",
2675 Some("50000001-0000-0000-0000-000000000001"),
2676 60,
2677 8,
2678 0,
2679 ),
2680 make_run_for_tree(
2681 "50000003-0000-0000-0000-000000000003",
2682 "2024-06-15T12:00:00+00:00",
2683 Some("50000002-0000-0000-0000-000000000002"),
2684 70,
2685 5,
2686 0,
2687 ),
2688 make_run_for_tree(
2689 "50000004-0000-0000-0000-000000000004",
2690 "2024-06-15T13:00:00+00:00",
2691 Some("50000001-0000-0000-0000-000000000001"),
2692 80,
2693 3,
2694 0,
2695 ),
2696 ];
2697 let snapshot = RunStoreSnapshot::new_for_test(runs);
2698 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2699 let theme_characters = ThemeCharacters::default();
2700
2701 insta::assert_snapshot!(
2702 "tree_branching_with_chains",
2703 DisplayRunList::new(
2704 &snapshot_with_replayability,
2705 None,
2706 &Styles::default(),
2707 &theme_characters,
2708 &Redactor::noop()
2709 )
2710 .to_string()
2711 );
2712 }
2713
2714 #[test]
2715 fn test_tree_continuation_flags() {
2716 let runs = vec![
2721 make_run_for_tree(
2722 "50000001-0000-0000-0000-000000000001",
2723 "2024-06-15T10:00:00+00:00",
2724 None,
2725 50,
2726 10,
2727 0,
2728 ),
2729 make_run_for_tree(
2730 "50000002-0000-0000-0000-000000000002",
2731 "2024-06-15T13:00:00+00:00", Some("50000001-0000-0000-0000-000000000001"),
2733 60,
2734 8,
2735 0,
2736 ),
2737 make_run_for_tree(
2738 "50000003-0000-0000-0000-000000000003",
2739 "2024-06-15T14:00:00+00:00",
2740 Some("50000002-0000-0000-0000-000000000002"),
2741 70,
2742 5,
2743 0,
2744 ),
2745 make_run_for_tree(
2746 "50000004-0000-0000-0000-000000000004",
2747 "2024-06-15T11:00:00+00:00", Some("50000001-0000-0000-0000-000000000001"),
2749 80,
2750 3,
2751 0,
2752 ),
2753 ];
2754 let snapshot = RunStoreSnapshot::new_for_test(runs);
2755 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2756 let theme_characters = ThemeCharacters::default();
2757
2758 insta::assert_snapshot!(
2759 "tree_continuation_flags",
2760 DisplayRunList::new(
2761 &snapshot_with_replayability,
2762 None,
2763 &Styles::default(),
2764 &theme_characters,
2765 &Redactor::noop()
2766 )
2767 .to_string()
2768 );
2769 }
2770
2771 #[test]
2772 fn test_tree_pruned_parent_with_chain() {
2773 let runs = vec![
2776 make_run_for_tree(
2777 "50000002-0000-0000-0000-000000000002",
2778 "2024-06-15T11:00:00+00:00",
2779 Some("50000001-0000-0000-0000-000000000001"), 60,
2781 8,
2782 0,
2783 ),
2784 make_run_for_tree(
2785 "50000003-0000-0000-0000-000000000003",
2786 "2024-06-15T12:00:00+00:00",
2787 Some("50000002-0000-0000-0000-000000000002"),
2788 70,
2789 5,
2790 0,
2791 ),
2792 ];
2793 let snapshot = RunStoreSnapshot::new_for_test(runs);
2794 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2795 let theme_characters = ThemeCharacters::default();
2796
2797 insta::assert_snapshot!(
2798 "tree_pruned_parent_with_chain",
2799 DisplayRunList::new(
2800 &snapshot_with_replayability,
2801 None,
2802 &Styles::default(),
2803 &theme_characters,
2804 &Redactor::noop()
2805 )
2806 .to_string()
2807 );
2808 }
2809
2810 #[test]
2811 fn test_tree_pruned_parent_with_multiple_children() {
2812 let runs = vec![
2815 make_run_for_tree(
2816 "50000002-0000-0000-0000-000000000002",
2817 "2024-06-15T11:00:00+00:00",
2818 Some("50000001-0000-0000-0000-000000000001"), 60,
2820 5,
2821 0,
2822 ),
2823 make_run_for_tree(
2824 "50000003-0000-0000-0000-000000000003",
2825 "2024-06-15T12:00:00+00:00",
2826 Some("50000001-0000-0000-0000-000000000001"), 70,
2828 3,
2829 0,
2830 ),
2831 ];
2832 let snapshot = RunStoreSnapshot::new_for_test(runs);
2833 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2834 let theme_characters = ThemeCharacters::default();
2835
2836 insta::assert_snapshot!(
2837 "tree_pruned_parent_with_multiple_children",
2838 DisplayRunList::new(
2839 &snapshot_with_replayability,
2840 None,
2841 &Styles::default(),
2842 &theme_characters,
2843 &Redactor::noop()
2844 )
2845 .to_string()
2846 );
2847 }
2848
2849 #[test]
2850 fn test_tree_unicode_characters() {
2851 let runs = vec![
2853 make_run_for_tree(
2854 "50000001-0000-0000-0000-000000000001",
2855 "2024-06-15T10:00:00+00:00",
2856 None,
2857 50,
2858 10,
2859 0,
2860 ),
2861 make_run_for_tree(
2862 "50000002-0000-0000-0000-000000000002",
2863 "2024-06-15T11:00:00+00:00",
2864 Some("50000001-0000-0000-0000-000000000001"),
2865 60,
2866 8,
2867 0,
2868 ),
2869 make_run_for_tree(
2870 "50000003-0000-0000-0000-000000000003",
2871 "2024-06-15T12:00:00+00:00",
2872 Some("50000001-0000-0000-0000-000000000001"),
2873 70,
2874 5,
2875 0,
2876 ),
2877 ];
2878 let snapshot = RunStoreSnapshot::new_for_test(runs);
2879 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2880 let mut theme_characters = ThemeCharacters::default();
2882 theme_characters.use_unicode();
2883
2884 insta::assert_snapshot!(
2885 "tree_unicode_characters",
2886 DisplayRunList::new(
2887 &snapshot_with_replayability,
2888 None,
2889 &Styles::default(),
2890 &theme_characters,
2891 &Redactor::noop()
2892 )
2893 .to_string()
2894 );
2895 }
2896
2897 #[test]
2898 fn test_tree_compressed_with_branching() {
2899 let runs = vec![
2914 make_run_for_tree(
2915 "50000001-0000-0000-0000-000000000001",
2916 "2024-06-15T10:00:00+00:00",
2917 None,
2918 50,
2919 10,
2920 0,
2921 ),
2922 make_run_for_tree(
2923 "50000002-0000-0000-0000-000000000002",
2924 "2024-06-15T11:00:00+00:00",
2925 Some("50000001-0000-0000-0000-000000000001"),
2926 60,
2927 8,
2928 0,
2929 ),
2930 make_run_for_tree(
2931 "50000003-0000-0000-0000-000000000003",
2932 "2024-06-15T14:00:00+00:00", Some("50000002-0000-0000-0000-000000000002"),
2934 70,
2935 5,
2936 0,
2937 ),
2938 make_run_for_tree(
2939 "50000004-0000-0000-0000-000000000004",
2940 "2024-06-15T15:00:00+00:00",
2941 Some("50000003-0000-0000-0000-000000000003"),
2942 80,
2943 3,
2944 0,
2945 ),
2946 make_run_for_tree(
2947 "50000005-0000-0000-0000-000000000005",
2948 "2024-06-15T12:00:00+00:00", Some("50000002-0000-0000-0000-000000000002"),
2950 90,
2951 2,
2952 0,
2953 ),
2954 ];
2955 let snapshot = RunStoreSnapshot::new_for_test(runs);
2956 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2957 let theme_characters = ThemeCharacters::default();
2958
2959 insta::assert_snapshot!(
2960 "tree_compressed_with_branching",
2961 DisplayRunList::new(
2962 &snapshot_with_replayability,
2963 None,
2964 &Styles::default(),
2965 &theme_characters,
2966 &Redactor::noop()
2967 )
2968 .to_string()
2969 );
2970 }
2971}