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