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