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, format::RECORD_FORMAT_VERSION,
1094 run_id_index::RunIdIndex,
1095 },
1096 redact::Redactor,
1097 };
1098 use chrono::{DateTime, Utc};
1099 use semver::Version;
1100 use std::{collections::BTreeMap, num::NonZero};
1101
1102 fn test_now() -> DateTime<Utc> {
1107 DateTime::parse_from_rfc3339("2024-06-25T13:00:30+00:00")
1108 .expect("valid datetime")
1109 .with_timezone(&Utc)
1110 }
1111
1112 fn make_run_info(
1114 uuid: &str,
1115 version: &str,
1116 started_at: &str,
1117 total_compressed_size: u64,
1118 status: RecordedRunStatus,
1119 ) -> RecordedRunInfo {
1120 make_run_info_with_duration(
1121 uuid,
1122 version,
1123 started_at,
1124 total_compressed_size,
1125 1.0,
1126 status,
1127 )
1128 }
1129
1130 fn make_run_info_with_duration(
1132 uuid: &str,
1133 version: &str,
1134 started_at: &str,
1135 total_compressed_size: u64,
1136 duration_secs: f64,
1137 status: RecordedRunStatus,
1138 ) -> RecordedRunInfo {
1139 let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
1140 RecordedRunInfo {
1142 run_id: uuid.parse().expect("valid UUID"),
1143 store_format_version: RECORD_FORMAT_VERSION,
1144 nextest_version: Version::parse(version).expect("valid version"),
1145 started_at,
1146 last_written_at: started_at,
1147 duration_secs: Some(duration_secs),
1148 cli_args: Vec::new(),
1149 build_scope_args: Vec::new(),
1150 env_vars: BTreeMap::new(),
1151 parent_run_id: None,
1152 sizes: RecordedSizes {
1153 log: ComponentSizes::default(),
1154 store: ComponentSizes {
1155 compressed: total_compressed_size,
1156 uncompressed: total_compressed_size * 3,
1157 entries: 0,
1158 },
1159 },
1160 status,
1161 }
1162 }
1163
1164 fn make_run_info_with_cli_env(
1166 uuid: &str,
1167 version: &str,
1168 started_at: &str,
1169 cli_args: Vec<String>,
1170 env_vars: BTreeMap<String, String>,
1171 status: RecordedRunStatus,
1172 ) -> RecordedRunInfo {
1173 make_run_info_with_parent(uuid, version, started_at, cli_args, env_vars, None, status)
1174 }
1175
1176 fn make_run_info_with_parent(
1178 uuid: &str,
1179 version: &str,
1180 started_at: &str,
1181 cli_args: Vec<String>,
1182 env_vars: BTreeMap<String, String>,
1183 parent_run_id: Option<&str>,
1184 status: RecordedRunStatus,
1185 ) -> RecordedRunInfo {
1186 let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
1187 RecordedRunInfo {
1188 run_id: uuid.parse().expect("valid UUID"),
1189 store_format_version: RECORD_FORMAT_VERSION,
1190 nextest_version: Version::parse(version).expect("valid version"),
1191 started_at,
1192 last_written_at: started_at,
1193 duration_secs: Some(12.345),
1194 cli_args,
1195 build_scope_args: Vec::new(),
1196 env_vars,
1197 parent_run_id: parent_run_id.map(|s| s.parse().expect("valid UUID")),
1198 sizes: RecordedSizes {
1199 log: ComponentSizes {
1200 compressed: 1024,
1201 uncompressed: 4096,
1202 entries: 100,
1203 },
1204 store: ComponentSizes {
1205 compressed: 51200,
1206 uncompressed: 204800,
1207 entries: 42,
1208 },
1209 },
1210 status,
1211 }
1212 }
1213
1214 #[test]
1215 fn test_display_prune_result_nothing_to_prune() {
1216 let result = PruneResult::default();
1217 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"no runs to prune");
1218 }
1219
1220 #[test]
1221 fn test_display_prune_result_nothing_pruned_with_error() {
1222 let result = PruneResult {
1223 kind: PruneKind::Implicit,
1224 deleted_count: 0,
1225 orphans_deleted: 0,
1226 freed_bytes: 0,
1227 errors: vec![RecordPruneError::DeleteOrphan {
1228 path: "/some/path".into(),
1229 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1230 }],
1231 };
1232 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"no runs pruned (1 error occurred)");
1233 }
1234
1235 #[test]
1236 fn test_display_prune_result_single_run() {
1237 let result = PruneResult {
1238 kind: PruneKind::Implicit,
1239 deleted_count: 1,
1240 orphans_deleted: 0,
1241 freed_bytes: 1024,
1242 errors: vec![],
1243 };
1244 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 1 run, freed 1 KB");
1245 }
1246
1247 #[test]
1248 fn test_display_prune_result_multiple_runs() {
1249 let result = PruneResult {
1250 kind: PruneKind::Implicit,
1251 deleted_count: 3,
1252 orphans_deleted: 0,
1253 freed_bytes: 5 * 1024 * 1024,
1254 errors: vec![],
1255 };
1256 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 3 runs, freed 5.0 MB");
1257 }
1258
1259 #[test]
1260 fn test_display_prune_result_with_orphan() {
1261 let result = PruneResult {
1262 kind: PruneKind::Implicit,
1263 deleted_count: 2,
1264 orphans_deleted: 1,
1265 freed_bytes: 3 * 1024 * 1024,
1266 errors: vec![],
1267 };
1268 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 2 runs, 1 orphan, freed 3.0 MB");
1269 }
1270
1271 #[test]
1272 fn test_display_prune_result_with_multiple_orphans() {
1273 let result = PruneResult {
1274 kind: PruneKind::Implicit,
1275 deleted_count: 1,
1276 orphans_deleted: 3,
1277 freed_bytes: 2 * 1024 * 1024,
1278 errors: vec![],
1279 };
1280 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 1 run, 3 orphans, freed 2.0 MB");
1281 }
1282
1283 #[test]
1284 fn test_display_prune_result_with_errors_implicit() {
1285 let result = PruneResult {
1286 kind: PruneKind::Implicit,
1287 deleted_count: 2,
1288 orphans_deleted: 0,
1289 freed_bytes: 1024 * 1024,
1290 errors: vec![
1291 RecordPruneError::DeleteOrphan {
1292 path: "/path1".into(),
1293 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1294 },
1295 RecordPruneError::DeleteOrphan {
1296 path: "/path2".into(),
1297 error: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
1298 },
1299 ],
1300 };
1301 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 2 runs, freed 1.0 MB (2 errors occurred)");
1303 }
1304
1305 #[test]
1306 fn test_display_prune_result_with_errors_explicit() {
1307 let result = PruneResult {
1308 kind: PruneKind::Explicit,
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(), @"
1325 pruned 2 runs, freed 1.0 MB (2 errors occurred)
1326
1327 errors:
1328 - error deleting orphaned directory `/path1`: denied
1329 - error deleting orphaned directory `/path2`: not found
1330 ");
1331 }
1332
1333 #[test]
1334 fn test_display_prune_result_full() {
1335 let result = PruneResult {
1336 kind: PruneKind::Implicit,
1337 deleted_count: 5,
1338 orphans_deleted: 2,
1339 freed_bytes: 10 * 1024 * 1024,
1340 errors: vec![RecordPruneError::DeleteOrphan {
1341 path: "/orphan".into(),
1342 error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
1343 }],
1344 };
1345 insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 5 runs, 2 orphans, freed 10.0 MB (1 error occurred)");
1346 }
1347
1348 #[test]
1349 fn test_display_recorded_run_info_completed() {
1350 let run = make_run_info(
1351 "550e8400-e29b-41d4-a716-446655440000",
1352 "0.9.100",
1353 "2024-06-15T10:30:00+00:00",
1354 102400,
1355 RecordedRunStatus::Completed(CompletedRunStats {
1356 initial_run_count: 100,
1357 passed: 95,
1358 failed: 5,
1359 exit_code: 100,
1360 }),
1361 );
1362 let runs = std::slice::from_ref(&run);
1363 let index = RunIdIndex::new(runs);
1364 let alignment = RunListAlignment::from_runs(runs);
1365 insta::assert_snapshot!(
1366 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1367 .to_string(),
1368 @" 550e8400 2024-06-15 10:30:00 1.000s 100 KB 95 passed / 5 failed"
1369 );
1370 }
1371
1372 #[test]
1373 fn test_display_recorded_run_info_incomplete() {
1374 let run = make_run_info(
1375 "550e8400-e29b-41d4-a716-446655440001",
1376 "0.9.101",
1377 "2024-06-16T11:00:00+00:00",
1378 51200,
1379 RecordedRunStatus::Incomplete,
1380 );
1381 let runs = std::slice::from_ref(&run);
1382 let index = RunIdIndex::new(runs);
1383 let alignment = RunListAlignment::from_runs(runs);
1384 insta::assert_snapshot!(
1385 run.display(&index, &ReplayabilityStatus::Incomplete, alignment, &Styles::default(), &Redactor::noop())
1386 .to_string(),
1387 @" 550e8400 2024-06-16 11:00:00 1.000s 50 KB incomplete"
1388 );
1389 }
1390
1391 #[test]
1392 fn test_display_recorded_run_info_not_run() {
1393 let run = make_run_info(
1395 "550e8400-e29b-41d4-a716-446655440005",
1396 "0.9.105",
1397 "2024-06-20T15:00:00+00:00",
1398 75000,
1399 RecordedRunStatus::Completed(CompletedRunStats {
1400 initial_run_count: 17,
1401 passed: 10,
1402 failed: 6,
1403 exit_code: 100,
1404 }),
1405 );
1406 let runs = std::slice::from_ref(&run);
1407 let index = RunIdIndex::new(runs);
1408 let alignment = RunListAlignment::from_runs(runs);
1409 insta::assert_snapshot!(
1410 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1411 .to_string(),
1412 @" 550e8400 2024-06-20 15:00:00 1.000s 73 KB 10 passed / 6 failed / 1 not run"
1413 );
1414 }
1415
1416 #[test]
1417 fn test_display_recorded_run_info_no_tests() {
1418 let run = make_run_info(
1420 "550e8400-e29b-41d4-a716-44665544000c",
1421 "0.9.112",
1422 "2024-06-23T16:00:00+00:00",
1423 5000,
1424 RecordedRunStatus::Completed(CompletedRunStats {
1425 initial_run_count: 0,
1426 passed: 0,
1427 failed: 0,
1428 exit_code: 0,
1429 }),
1430 );
1431 let runs = std::slice::from_ref(&run);
1432 let index = RunIdIndex::new(runs);
1433 let alignment = RunListAlignment::from_runs(runs);
1434 insta::assert_snapshot!(
1435 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1436 .to_string(),
1437 @" 550e8400 2024-06-23 16:00:00 1.000s 4 KB 0 passed"
1438 );
1439 }
1440
1441 #[test]
1442 fn test_display_recorded_run_info_stress_completed() {
1443 let run = make_run_info(
1445 "550e8400-e29b-41d4-a716-446655440010",
1446 "0.9.120",
1447 "2024-06-25T10:00:00+00:00",
1448 150000,
1449 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1450 initial_iteration_count: NonZero::new(100),
1451 success_count: 100,
1452 failed_count: 0,
1453 exit_code: 0,
1454 }),
1455 );
1456 let runs = std::slice::from_ref(&run);
1457 let index = RunIdIndex::new(runs);
1458 let alignment = RunListAlignment::from_runs(runs);
1459 insta::assert_snapshot!(
1460 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1461 .to_string(),
1462 @" 550e8400 2024-06-25 10:00:00 1.000s 146 KB 100 passed iterations"
1463 );
1464
1465 let run = make_run_info(
1467 "550e8400-e29b-41d4-a716-446655440011",
1468 "0.9.120",
1469 "2024-06-25T11:00:00+00:00",
1470 150000,
1471 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1472 initial_iteration_count: NonZero::new(100),
1473 success_count: 95,
1474 failed_count: 5,
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 11:00:00 1.000s 146 KB 95 passed iterations / 5 failed"
1485 );
1486 }
1487
1488 #[test]
1489 fn test_display_recorded_run_info_stress_cancelled() {
1490 let run = make_run_info(
1492 "550e8400-e29b-41d4-a716-446655440012",
1493 "0.9.120",
1494 "2024-06-25T12:00:00+00:00",
1495 100000,
1496 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
1497 initial_iteration_count: NonZero::new(100),
1498 success_count: 50,
1499 failed_count: 10,
1500 exit_code: 0,
1501 }),
1502 );
1503 let runs = std::slice::from_ref(&run);
1504 let index = RunIdIndex::new(runs);
1505 let alignment = RunListAlignment::from_runs(runs);
1506 insta::assert_snapshot!(
1507 run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1508 .to_string(),
1509 @" 550e8400 2024-06-25 12:00:00 1.000s 97 KB 50 passed iterations / 10 failed / 40 not run (cancelled)"
1510 );
1511
1512 let run = make_run_info(
1514 "550e8400-e29b-41d4-a716-446655440013",
1515 "0.9.120",
1516 "2024-06-25T13:00:00+00:00",
1517 100000,
1518 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
1519 initial_iteration_count: None,
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 13:00:00 1.000s 97 KB 50 passed iterations / 10 failed (cancelled)"
1532 );
1533 }
1534
1535 #[test]
1536 fn test_display_alignment_multiple_runs() {
1537 let runs = vec![
1539 make_run_info(
1540 "550e8400-e29b-41d4-a716-446655440006",
1541 "0.9.106",
1542 "2024-06-21T10:00:00+00:00",
1543 100000,
1544 RecordedRunStatus::Completed(CompletedRunStats {
1545 initial_run_count: 559,
1546 passed: 559,
1547 failed: 0,
1548 exit_code: 0,
1549 }),
1550 ),
1551 make_run_info(
1552 "550e8400-e29b-41d4-a716-446655440007",
1553 "0.9.107",
1554 "2024-06-21T11:00:00+00:00",
1555 50000,
1556 RecordedRunStatus::Completed(CompletedRunStats {
1557 initial_run_count: 51,
1558 passed: 51,
1559 failed: 0,
1560 exit_code: 0,
1561 }),
1562 ),
1563 make_run_info(
1564 "550e8400-e29b-41d4-a716-446655440008",
1565 "0.9.108",
1566 "2024-06-21T12:00:00+00:00",
1567 30000,
1568 RecordedRunStatus::Completed(CompletedRunStats {
1569 initial_run_count: 17,
1570 passed: 10,
1571 failed: 6,
1572 exit_code: 0,
1573 }),
1574 ),
1575 ];
1576 let index = RunIdIndex::new(&runs);
1577 let alignment = RunListAlignment::from_runs(&runs);
1578
1579 insta::assert_snapshot!(
1581 runs[0]
1582 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1583 .to_string(),
1584 @" 550e8400 2024-06-21 10:00:00 1.000s 97 KB 559 passed"
1585 );
1586 insta::assert_snapshot!(
1587 runs[1]
1588 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1589 .to_string(),
1590 @" 550e8400 2024-06-21 11:00:00 1.000s 48 KB 51 passed"
1591 );
1592 insta::assert_snapshot!(
1593 runs[2]
1594 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1595 .to_string(),
1596 @" 550e8400 2024-06-21 12:00:00 1.000s 29 KB 10 passed / 6 failed / 1 not run"
1597 );
1598 }
1599
1600 #[test]
1601 fn test_display_stress_stats_alignment() {
1602 let runs = vec![
1604 make_run_info(
1605 "550e8400-e29b-41d4-a716-446655440009",
1606 "0.9.109",
1607 "2024-06-22T10:00:00+00:00",
1608 200000,
1609 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1610 initial_iteration_count: NonZero::new(1000),
1611 success_count: 1000,
1612 failed_count: 0,
1613 exit_code: 0,
1614 }),
1615 ),
1616 make_run_info(
1617 "550e8400-e29b-41d4-a716-44665544000a",
1618 "0.9.110",
1619 "2024-06-22T11:00:00+00:00",
1620 100000,
1621 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
1622 initial_iteration_count: NonZero::new(100),
1623 success_count: 95,
1624 failed_count: 5,
1625 exit_code: 0,
1626 }),
1627 ),
1628 make_run_info(
1629 "550e8400-e29b-41d4-a716-44665544000b",
1630 "0.9.111",
1631 "2024-06-22T12:00:00+00:00",
1632 80000,
1633 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
1634 initial_iteration_count: NonZero::new(500),
1635 success_count: 45,
1636 failed_count: 5,
1637 exit_code: 0,
1638 }),
1639 ),
1640 ];
1641 let index = RunIdIndex::new(&runs);
1642 let alignment = RunListAlignment::from_runs(&runs);
1643
1644 insta::assert_snapshot!(
1646 runs[0]
1647 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1648 .to_string(),
1649 @" 550e8400 2024-06-22 10:00:00 1.000s 195 KB 1000 passed iterations"
1650 );
1651 insta::assert_snapshot!(
1652 runs[1]
1653 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1654 .to_string(),
1655 @" 550e8400 2024-06-22 11:00:00 1.000s 97 KB 95 passed iterations / 5 failed"
1656 );
1657 insta::assert_snapshot!(
1658 runs[2]
1659 .display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
1660 .to_string(),
1661 @" 550e8400 2024-06-22 12:00:00 1.000s 78 KB 45 passed iterations / 5 failed / 450 not run (cancelled)"
1662 );
1663 }
1664
1665 #[test]
1666 fn test_display_prune_plan_empty() {
1667 let plan = PrunePlan::new(vec![]);
1668 let index = RunIdIndex::new(&[]);
1669 insta::assert_snapshot!(
1670 plan.display(&index, &Styles::default(), &Redactor::noop())
1671 .to_string(),
1672 @"no runs would be pruned"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_display_prune_plan_single_run() {
1678 let runs = vec![make_run_info(
1679 "550e8400-e29b-41d4-a716-446655440002",
1680 "0.9.102",
1681 "2024-06-17T12:00:00+00:00",
1682 2048 * 1024,
1683 RecordedRunStatus::Completed(CompletedRunStats {
1684 initial_run_count: 50,
1685 passed: 50,
1686 failed: 0,
1687 exit_code: 0,
1688 }),
1689 )];
1690 let index = RunIdIndex::new(&runs);
1691 let plan = PrunePlan::new(runs);
1692 insta::assert_snapshot!(
1693 plan.display(&index, &Styles::default(), &Redactor::noop())
1694 .to_string(),
1695 @"
1696 would prune 1 run, freeing 2.0 MB:
1697
1698 550e8400 2024-06-17 12:00:00 1.000s 2.0 MB 50 passed
1699 "
1700 );
1701 }
1702
1703 #[test]
1704 fn test_display_prune_plan_multiple_runs() {
1705 let runs = vec![
1706 make_run_info(
1707 "550e8400-e29b-41d4-a716-446655440003",
1708 "0.9.103",
1709 "2024-06-18T13:00:00+00:00",
1710 1024 * 1024,
1711 RecordedRunStatus::Completed(CompletedRunStats {
1712 initial_run_count: 100,
1713 passed: 100,
1714 failed: 0,
1715 exit_code: 0,
1716 }),
1717 ),
1718 make_run_info(
1719 "550e8400-e29b-41d4-a716-446655440004",
1720 "0.9.104",
1721 "2024-06-19T14:00:00+00:00",
1722 512 * 1024,
1723 RecordedRunStatus::Incomplete,
1724 ),
1725 ];
1726 let index = RunIdIndex::new(&runs);
1727 let plan = PrunePlan::new(runs);
1728 insta::assert_snapshot!(
1729 plan.display(&index, &Styles::default(), &Redactor::noop())
1730 .to_string(),
1731 @"
1732 would prune 2 runs, freeing 1.5 MB:
1733
1734 550e8400 2024-06-18 13:00:00 1.000s 1.0 MB 100 passed
1735 550e8400 2024-06-19 14:00:00 1.000s 512 KB incomplete
1736 "
1737 );
1738 }
1739
1740 #[test]
1741 fn test_display_run_list() {
1742 let theme_characters = ThemeCharacters::default();
1743
1744 let runs = vec![
1746 make_run_info(
1747 "550e8400-e29b-41d4-a716-446655440001",
1748 "0.9.101",
1749 "2024-06-15T10:00:00+00:00",
1750 50 * 1024,
1751 RecordedRunStatus::Completed(CompletedRunStats {
1752 initial_run_count: 10,
1753 passed: 10,
1754 failed: 0,
1755 exit_code: 0,
1756 }),
1757 ),
1758 make_run_info(
1759 "550e8400-e29b-41d4-a716-446655440002",
1760 "0.9.102",
1761 "2024-06-16T11:00:00+00:00",
1762 75 * 1024,
1763 RecordedRunStatus::Completed(CompletedRunStats {
1764 initial_run_count: 20,
1765 passed: 18,
1766 failed: 2,
1767 exit_code: 0,
1768 }),
1769 ),
1770 make_run_info(
1771 "550e8400-e29b-41d4-a716-446655440003",
1772 "0.9.103",
1773 "2024-06-17T12:00:00+00:00",
1774 100 * 1024,
1775 RecordedRunStatus::Completed(CompletedRunStats {
1776 initial_run_count: 30,
1777 passed: 30,
1778 failed: 0,
1779 exit_code: 0,
1780 }),
1781 ),
1782 ];
1783 let snapshot = RunStoreSnapshot::new_for_test(runs);
1784 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1785 insta::assert_snapshot!(
1786 "normal_sizes",
1787 DisplayRunList::new(
1788 &snapshot_with_replayability,
1789 None,
1790 &Styles::default(),
1791 &theme_characters,
1792 &Redactor::noop()
1793 )
1794 .to_string()
1795 );
1796
1797 let runs = vec![
1799 make_run_info(
1800 "550e8400-e29b-41d4-a716-446655440001",
1801 "0.9.101",
1802 "2024-06-15T10:00:00+00:00",
1803 1024, RecordedRunStatus::Completed(CompletedRunStats {
1805 initial_run_count: 5,
1806 passed: 5,
1807 failed: 0,
1808 exit_code: 0,
1809 }),
1810 ),
1811 make_run_info(
1812 "550e8400-e29b-41d4-a716-446655440002",
1813 "0.9.102",
1814 "2024-06-16T11:00:00+00:00",
1815 99 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
1817 initial_run_count: 10,
1818 passed: 10,
1819 failed: 0,
1820 exit_code: 0,
1821 }),
1822 ),
1823 ];
1824 let snapshot = RunStoreSnapshot::new_for_test(runs);
1825 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1826 insta::assert_snapshot!(
1827 "small_sizes",
1828 DisplayRunList::new(
1829 &snapshot_with_replayability,
1830 None,
1831 &Styles::default(),
1832 &theme_characters,
1833 &Redactor::noop()
1834 )
1835 .to_string()
1836 );
1837
1838 let runs = vec![
1842 make_run_info(
1843 "550e8400-e29b-41d4-a716-446655440001",
1844 "0.9.101",
1845 "2024-06-15T10:00:00+00:00",
1846 1_000_000 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
1848 initial_run_count: 100,
1849 passed: 100,
1850 failed: 0,
1851 exit_code: 0,
1852 }),
1853 ),
1854 make_run_info(
1855 "550e8400-e29b-41d4-a716-446655440002",
1856 "0.9.102",
1857 "2024-06-16T11:00:00+00:00",
1858 10_000_000 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
1860 initial_run_count: 200,
1861 passed: 200,
1862 failed: 0,
1863 exit_code: 0,
1864 }),
1865 ),
1866 ];
1867 let snapshot = RunStoreSnapshot::new_for_test(runs);
1868 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1869 insta::assert_snapshot!(
1870 "large_sizes",
1871 DisplayRunList::new(
1872 &snapshot_with_replayability,
1873 None,
1874 &Styles::default(),
1875 &theme_characters,
1876 &Redactor::noop()
1877 )
1878 .to_string()
1879 );
1880
1881 let runs = vec![
1884 make_run_info_with_duration(
1885 "550e8400-e29b-41d4-a716-446655440001",
1886 "0.9.101",
1887 "2024-06-15T10:00:00+00:00",
1888 50 * 1024,
1889 0.123, RecordedRunStatus::Completed(CompletedRunStats {
1891 initial_run_count: 5,
1892 passed: 5,
1893 failed: 0,
1894 exit_code: 0,
1895 }),
1896 ),
1897 make_run_info_with_duration(
1898 "550e8400-e29b-41d4-a716-446655440002",
1899 "0.9.102",
1900 "2024-06-16T11:00:00+00:00",
1901 75 * 1024,
1902 9.876, RecordedRunStatus::Completed(CompletedRunStats {
1904 initial_run_count: 10,
1905 passed: 10,
1906 failed: 0,
1907 exit_code: 0,
1908 }),
1909 ),
1910 make_run_info_with_duration(
1911 "550e8400-e29b-41d4-a716-446655440003",
1912 "0.9.103",
1913 "2024-06-17T12:00:00+00:00",
1914 100 * 1024,
1915 42.5, RecordedRunStatus::Completed(CompletedRunStats {
1917 initial_run_count: 20,
1918 passed: 20,
1919 failed: 0,
1920 exit_code: 0,
1921 }),
1922 ),
1923 make_run_info_with_duration(
1924 "550e8400-e29b-41d4-a716-446655440004",
1925 "0.9.104",
1926 "2024-06-18T13:00:00+00:00",
1927 125 * 1024,
1928 987.654, RecordedRunStatus::Completed(CompletedRunStats {
1930 initial_run_count: 30,
1931 passed: 28,
1932 failed: 2,
1933 exit_code: 0,
1934 }),
1935 ),
1936 make_run_info_with_duration(
1937 "550e8400-e29b-41d4-a716-446655440005",
1938 "0.9.105",
1939 "2024-06-19T14:00:00+00:00",
1940 150 * 1024,
1941 12345.678, RecordedRunStatus::Completed(CompletedRunStats {
1943 initial_run_count: 50,
1944 passed: 50,
1945 failed: 0,
1946 exit_code: 0,
1947 }),
1948 ),
1949 ];
1950 let snapshot = RunStoreSnapshot::new_for_test(runs);
1951 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
1952 insta::assert_snapshot!(
1953 "varying_durations",
1954 DisplayRunList::new(
1955 &snapshot_with_replayability,
1956 None,
1957 &Styles::default(),
1958 &theme_characters,
1959 &Redactor::noop()
1960 )
1961 .to_string()
1962 );
1963 }
1964
1965 #[test]
1966 fn test_display_detailed() {
1967 let cli_args = vec![
1969 "cargo".to_string(),
1970 "nextest".to_string(),
1971 "run".to_string(),
1972 "--workspace".to_string(),
1973 "--features".to_string(),
1974 "foo bar".to_string(),
1975 ];
1976 let env_vars = BTreeMap::from([
1977 ("CARGO_HOME".to_string(), "/home/user/.cargo".to_string()),
1978 ("NEXTEST_PROFILE".to_string(), "ci".to_string()),
1979 ]);
1980 let run_with_cli_env = make_run_info_with_cli_env(
1981 "550e8400-e29b-41d4-a716-446655440000",
1982 "0.9.122",
1983 "2024-06-15T10:30:00+00:00",
1984 cli_args,
1985 env_vars,
1986 RecordedRunStatus::Completed(CompletedRunStats {
1987 initial_run_count: 100,
1988 passed: 95,
1989 failed: 5,
1990 exit_code: 100,
1991 }),
1992 );
1993
1994 let run_empty = make_run_info_with_cli_env(
1996 "550e8400-e29b-41d4-a716-446655440001",
1997 "0.9.122",
1998 "2024-06-16T11:00:00+00:00",
1999 Vec::new(),
2000 BTreeMap::new(),
2001 RecordedRunStatus::Incomplete,
2002 );
2003
2004 let stress_all_passed = make_run_info_with_cli_env(
2006 "550e8400-e29b-41d4-a716-446655440010",
2007 "0.9.122",
2008 "2024-06-25T10:00:00+00:00",
2009 Vec::new(),
2010 BTreeMap::new(),
2011 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
2012 initial_iteration_count: NonZero::new(100),
2013 success_count: 100,
2014 failed_count: 0,
2015 exit_code: 0,
2016 }),
2017 );
2018
2019 let stress_with_failures = make_run_info_with_cli_env(
2021 "550e8400-e29b-41d4-a716-446655440011",
2022 "0.9.122",
2023 "2024-06-25T11:00:00+00:00",
2024 Vec::new(),
2025 BTreeMap::new(),
2026 RecordedRunStatus::StressCompleted(StressCompletedRunStats {
2027 initial_iteration_count: NonZero::new(100),
2028 success_count: 95,
2029 failed_count: 5,
2030 exit_code: 0,
2031 }),
2032 );
2033
2034 let stress_cancelled = make_run_info_with_cli_env(
2036 "550e8400-e29b-41d4-a716-446655440012",
2037 "0.9.122",
2038 "2024-06-25T12:00:00+00:00",
2039 Vec::new(),
2040 BTreeMap::new(),
2041 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
2042 initial_iteration_count: NonZero::new(100),
2043 success_count: 50,
2044 failed_count: 10,
2045 exit_code: 0,
2046 }),
2047 );
2048
2049 let stress_cancelled_no_initial = make_run_info_with_cli_env(
2051 "550e8400-e29b-41d4-a716-446655440013",
2052 "0.9.122",
2053 "2024-06-25T13:00:00+00:00",
2054 Vec::new(),
2055 BTreeMap::new(),
2056 RecordedRunStatus::StressCancelled(StressCompletedRunStats {
2057 initial_iteration_count: None,
2058 success_count: 50,
2059 failed_count: 10,
2060 exit_code: 0,
2061 }),
2062 );
2063
2064 let runs = [
2065 run_with_cli_env,
2066 run_empty,
2067 stress_all_passed,
2068 stress_with_failures,
2069 stress_cancelled,
2070 stress_cancelled_no_initial,
2071 ];
2072 let index = RunIdIndex::new(&runs);
2073 let theme_characters = ThemeCharacters::default();
2074 let redactor = Redactor::noop();
2075 let now = test_now();
2076 let replayable = ReplayabilityStatus::Replayable;
2078 let incomplete = ReplayabilityStatus::Incomplete;
2080
2081 insta::assert_snapshot!(
2082 "with_cli_and_env",
2083 runs[0]
2084 .display_detailed(
2085 &index,
2086 &replayable,
2087 now,
2088 &Styles::default(),
2089 &theme_characters,
2090 &redactor
2091 )
2092 .to_string()
2093 );
2094 insta::assert_snapshot!(
2095 "empty_cli_and_env",
2096 runs[1]
2097 .display_detailed(
2098 &index,
2099 &incomplete,
2100 now,
2101 &Styles::default(),
2102 &theme_characters,
2103 &redactor
2104 )
2105 .to_string()
2106 );
2107 insta::assert_snapshot!(
2108 "stress_all_passed",
2109 runs[2]
2110 .display_detailed(
2111 &index,
2112 &replayable,
2113 now,
2114 &Styles::default(),
2115 &theme_characters,
2116 &redactor
2117 )
2118 .to_string()
2119 );
2120 insta::assert_snapshot!(
2121 "stress_with_failures",
2122 runs[3]
2123 .display_detailed(
2124 &index,
2125 &replayable,
2126 now,
2127 &Styles::default(),
2128 &theme_characters,
2129 &redactor
2130 )
2131 .to_string()
2132 );
2133 insta::assert_snapshot!(
2134 "stress_cancelled",
2135 runs[4]
2136 .display_detailed(
2137 &index,
2138 &replayable,
2139 now,
2140 &Styles::default(),
2141 &theme_characters,
2142 &redactor
2143 )
2144 .to_string()
2145 );
2146 insta::assert_snapshot!(
2147 "stress_cancelled_no_initial",
2148 runs[5]
2149 .display_detailed(
2150 &index,
2151 &replayable,
2152 now,
2153 &Styles::default(),
2154 &theme_characters,
2155 &redactor
2156 )
2157 .to_string()
2158 );
2159 }
2160
2161 #[test]
2162 fn test_display_detailed_with_parent_run() {
2163 let parent_run = make_run_info_with_cli_env(
2165 "550e8400-e29b-41d4-a716-446655440000",
2166 "0.9.122",
2167 "2024-06-15T10:00:00+00:00",
2168 Vec::new(),
2169 BTreeMap::new(),
2170 RecordedRunStatus::Completed(CompletedRunStats {
2171 initial_run_count: 100,
2172 passed: 95,
2173 failed: 5,
2174 exit_code: 100,
2175 }),
2176 );
2177
2178 let child_run = make_run_info_with_parent(
2179 "660e8400-e29b-41d4-a716-446655440001",
2180 "0.9.122",
2181 "2024-06-15T11:00:00+00:00",
2182 vec![
2183 "cargo".to_string(),
2184 "nextest".to_string(),
2185 "run".to_string(),
2186 "--rerun".to_string(),
2187 ],
2188 BTreeMap::new(),
2189 Some("550e8400-e29b-41d4-a716-446655440000"),
2190 RecordedRunStatus::Completed(CompletedRunStats {
2191 initial_run_count: 5,
2192 passed: 5,
2193 failed: 0,
2194 exit_code: 0,
2195 }),
2196 );
2197
2198 let runs = [parent_run, child_run];
2200 let index = RunIdIndex::new(&runs);
2201 let theme_characters = ThemeCharacters::default();
2202 let redactor = Redactor::noop();
2203 let now = test_now();
2204 let replayable = ReplayabilityStatus::Replayable;
2205
2206 insta::assert_snapshot!(
2207 "with_parent_run",
2208 runs[1]
2209 .display_detailed(
2210 &index,
2211 &replayable,
2212 now,
2213 &Styles::default(),
2214 &theme_characters,
2215 &redactor
2216 )
2217 .to_string()
2218 );
2219 }
2220
2221 #[test]
2222 fn test_display_replayability_statuses() {
2223 let run = make_run_info(
2225 "550e8400-e29b-41d4-a716-446655440000",
2226 "0.9.100",
2227 "2024-06-15T10:30:00+00:00",
2228 102400,
2229 RecordedRunStatus::Completed(CompletedRunStats {
2230 initial_run_count: 100,
2231 passed: 100,
2232 failed: 0,
2233 exit_code: 0,
2234 }),
2235 );
2236 let runs = std::slice::from_ref(&run);
2237 let index = RunIdIndex::new(runs);
2238 let theme_characters = ThemeCharacters::default();
2239 let redactor = Redactor::noop();
2240 let now = test_now();
2241
2242 let definitely_replayable = ReplayabilityStatus::Replayable;
2244 insta::assert_snapshot!(
2245 "replayability_yes",
2246 run.display_detailed(
2247 &index,
2248 &definitely_replayable,
2249 now,
2250 &Styles::default(),
2251 &theme_characters,
2252 &redactor
2253 )
2254 .to_string()
2255 );
2256
2257 let format_too_new =
2259 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::StoreFormatTooNew {
2260 run_version: 5,
2261 max_supported: 1,
2262 }]);
2263 insta::assert_snapshot!(
2264 "replayability_format_too_new",
2265 run.display_detailed(
2266 &index,
2267 &format_too_new,
2268 now,
2269 &Styles::default(),
2270 &theme_characters,
2271 &redactor
2272 )
2273 .to_string()
2274 );
2275
2276 let missing_store =
2278 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::MissingStoreZip]);
2279 insta::assert_snapshot!(
2280 "replayability_missing_store_zip",
2281 run.display_detailed(
2282 &index,
2283 &missing_store,
2284 now,
2285 &Styles::default(),
2286 &theme_characters,
2287 &redactor
2288 )
2289 .to_string()
2290 );
2291
2292 let missing_log =
2294 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::MissingRunLog]);
2295 insta::assert_snapshot!(
2296 "replayability_missing_run_log",
2297 run.display_detailed(
2298 &index,
2299 &missing_log,
2300 now,
2301 &Styles::default(),
2302 &theme_characters,
2303 &redactor
2304 )
2305 .to_string()
2306 );
2307
2308 let status_unknown =
2310 ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::StatusUnknown]);
2311 insta::assert_snapshot!(
2312 "replayability_status_unknown",
2313 run.display_detailed(
2314 &index,
2315 &status_unknown,
2316 now,
2317 &Styles::default(),
2318 &theme_characters,
2319 &redactor
2320 )
2321 .to_string()
2322 );
2323
2324 let incomplete = ReplayabilityStatus::Incomplete;
2326 insta::assert_snapshot!(
2327 "replayability_incomplete",
2328 run.display_detailed(
2329 &index,
2330 &incomplete,
2331 now,
2332 &Styles::default(),
2333 &theme_characters,
2334 &redactor
2335 )
2336 .to_string()
2337 );
2338
2339 let multiple_blocking = ReplayabilityStatus::NotReplayable(vec![
2341 NonReplayableReason::MissingStoreZip,
2342 NonReplayableReason::MissingRunLog,
2343 ]);
2344 insta::assert_snapshot!(
2345 "replayability_multiple_blocking",
2346 run.display_detailed(
2347 &index,
2348 &multiple_blocking,
2349 now,
2350 &Styles::default(),
2351 &theme_characters,
2352 &redactor
2353 )
2354 .to_string()
2355 );
2356 }
2357
2358 fn make_run_for_tree(
2360 uuid: &str,
2361 started_at: &str,
2362 parent_run_id: Option<&str>,
2363 size_kb: u64,
2364 passed: usize,
2365 failed: usize,
2366 ) -> RecordedRunInfo {
2367 let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
2368 RecordedRunInfo {
2369 run_id: uuid.parse().expect("valid UUID"),
2370 store_format_version: RECORD_FORMAT_VERSION,
2371 nextest_version: Version::parse("0.9.100").expect("valid version"),
2372 started_at,
2373 last_written_at: started_at,
2374 duration_secs: Some(1.0),
2375 cli_args: Vec::new(),
2376 build_scope_args: Vec::new(),
2377 env_vars: BTreeMap::new(),
2378 parent_run_id: parent_run_id.map(|s| s.parse().expect("valid UUID")),
2379 sizes: RecordedSizes {
2380 log: ComponentSizes::default(),
2381 store: ComponentSizes {
2382 compressed: size_kb * 1024,
2383 uncompressed: size_kb * 1024 * 3,
2384 entries: 0,
2385 },
2386 },
2387 status: RecordedRunStatus::Completed(CompletedRunStats {
2388 initial_run_count: passed + failed,
2389 passed,
2390 failed,
2391 exit_code: if failed > 0 { 1 } else { 0 },
2392 }),
2393 }
2394 }
2395
2396 #[test]
2397 fn test_tree_linear_chain() {
2398 let runs = vec![
2400 make_run_for_tree(
2401 "50000001-0000-0000-0000-000000000001",
2402 "2024-06-15T10:00:00+00:00",
2403 None,
2404 50,
2405 10,
2406 0,
2407 ),
2408 make_run_for_tree(
2409 "50000002-0000-0000-0000-000000000002",
2410 "2024-06-15T11:00:00+00:00",
2411 Some("50000001-0000-0000-0000-000000000001"),
2412 60,
2413 8,
2414 2,
2415 ),
2416 make_run_for_tree(
2417 "50000003-0000-0000-0000-000000000003",
2418 "2024-06-15T12:00:00+00:00",
2419 Some("50000002-0000-0000-0000-000000000002"),
2420 70,
2421 10,
2422 0,
2423 ),
2424 ];
2425 let snapshot = RunStoreSnapshot::new_for_test(runs);
2426 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2427 let theme_characters = ThemeCharacters::default();
2428
2429 insta::assert_snapshot!(
2430 "tree_linear_chain",
2431 DisplayRunList::new(
2432 &snapshot_with_replayability,
2433 None,
2434 &Styles::default(),
2435 &theme_characters,
2436 &Redactor::noop()
2437 )
2438 .to_string()
2439 );
2440 }
2441
2442 #[test]
2443 fn test_tree_branching() {
2444 let runs = vec![
2447 make_run_for_tree(
2448 "50000001-0000-0000-0000-000000000001",
2449 "2024-06-15T10:00:00+00:00",
2450 None,
2451 50,
2452 10,
2453 0,
2454 ),
2455 make_run_for_tree(
2456 "50000002-0000-0000-0000-000000000002",
2457 "2024-06-15T11:00:00+00:00",
2458 Some("50000001-0000-0000-0000-000000000001"),
2459 60,
2460 5,
2461 0,
2462 ),
2463 make_run_for_tree(
2464 "50000003-0000-0000-0000-000000000003",
2465 "2024-06-15T12:00:00+00:00",
2466 Some("50000001-0000-0000-0000-000000000001"),
2467 70,
2468 3,
2469 0,
2470 ),
2471 ];
2472 let snapshot = RunStoreSnapshot::new_for_test(runs);
2473 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2474 let theme_characters = ThemeCharacters::default();
2475
2476 insta::assert_snapshot!(
2477 "tree_branching",
2478 DisplayRunList::new(
2479 &snapshot_with_replayability,
2480 None,
2481 &Styles::default(),
2482 &theme_characters,
2483 &Redactor::noop()
2484 )
2485 .to_string()
2486 );
2487 }
2488
2489 #[test]
2490 fn test_tree_pruned_parent() {
2491 let runs = vec![make_run_for_tree(
2494 "50000002-0000-0000-0000-000000000002",
2495 "2024-06-15T11:00:00+00:00",
2496 Some("50000001-0000-0000-0000-000000000001"), 60,
2498 5,
2499 0,
2500 )];
2501 let snapshot = RunStoreSnapshot::new_for_test(runs);
2502 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2503 let theme_characters = ThemeCharacters::default();
2504
2505 insta::assert_snapshot!(
2506 "tree_pruned_parent",
2507 DisplayRunList::new(
2508 &snapshot_with_replayability,
2509 None,
2510 &Styles::default(),
2511 &theme_characters,
2512 &Redactor::noop()
2513 )
2514 .to_string()
2515 );
2516 }
2517
2518 #[test]
2519 fn test_tree_mixed_independent_and_chain() {
2520 let runs = vec![
2523 make_run_for_tree(
2524 "50000001-0000-0000-0000-000000000001",
2525 "2024-06-15T10:00:00+00:00",
2526 None,
2527 50,
2528 10,
2529 0,
2530 ),
2531 make_run_for_tree(
2532 "50000002-0000-0000-0000-000000000002",
2533 "2024-06-15T11:00:00+00:00",
2534 None,
2535 60,
2536 8,
2537 0,
2538 ),
2539 make_run_for_tree(
2540 "50000003-0000-0000-0000-000000000003",
2541 "2024-06-15T12:00:00+00:00",
2542 Some("50000002-0000-0000-0000-000000000002"),
2543 70,
2544 5,
2545 0,
2546 ),
2547 ];
2548 let snapshot = RunStoreSnapshot::new_for_test(runs);
2549 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2550 let theme_characters = ThemeCharacters::default();
2551
2552 insta::assert_snapshot!(
2553 "tree_mixed_independent_and_chain",
2554 DisplayRunList::new(
2555 &snapshot_with_replayability,
2556 None,
2557 &Styles::default(),
2558 &theme_characters,
2559 &Redactor::noop()
2560 )
2561 .to_string()
2562 );
2563 }
2564
2565 #[test]
2566 fn test_tree_deep_chain() {
2567 let runs = vec![
2570 make_run_for_tree(
2571 "50000001-0000-0000-0000-000000000001",
2572 "2024-06-15T10:00:00+00:00",
2573 None,
2574 50,
2575 10,
2576 0,
2577 ),
2578 make_run_for_tree(
2579 "50000002-0000-0000-0000-000000000002",
2580 "2024-06-15T11:00:00+00:00",
2581 Some("50000001-0000-0000-0000-000000000001"),
2582 60,
2583 10,
2584 0,
2585 ),
2586 make_run_for_tree(
2587 "50000003-0000-0000-0000-000000000003",
2588 "2024-06-15T12:00:00+00:00",
2589 Some("50000002-0000-0000-0000-000000000002"),
2590 70,
2591 10,
2592 0,
2593 ),
2594 make_run_for_tree(
2595 "50000004-0000-0000-0000-000000000004",
2596 "2024-06-15T13:00:00+00:00",
2597 Some("50000003-0000-0000-0000-000000000003"),
2598 80,
2599 10,
2600 0,
2601 ),
2602 make_run_for_tree(
2603 "50000005-0000-0000-0000-000000000005",
2604 "2024-06-15T14:00:00+00:00",
2605 Some("50000004-0000-0000-0000-000000000004"),
2606 90,
2607 10,
2608 0,
2609 ),
2610 ];
2611 let snapshot = RunStoreSnapshot::new_for_test(runs);
2612 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2613 let theme_characters = ThemeCharacters::default();
2614
2615 insta::assert_snapshot!(
2616 "tree_deep_chain",
2617 DisplayRunList::new(
2618 &snapshot_with_replayability,
2619 None,
2620 &Styles::default(),
2621 &theme_characters,
2622 &Redactor::noop()
2623 )
2624 .to_string()
2625 );
2626 }
2627
2628 #[test]
2629 fn test_tree_branching_with_chains() {
2630 let runs = vec![
2634 make_run_for_tree(
2635 "50000001-0000-0000-0000-000000000001",
2636 "2024-06-15T10:00:00+00:00",
2637 None,
2638 50,
2639 10,
2640 0,
2641 ),
2642 make_run_for_tree(
2643 "50000002-0000-0000-0000-000000000002",
2644 "2024-06-15T11:00:00+00:00",
2645 Some("50000001-0000-0000-0000-000000000001"),
2646 60,
2647 8,
2648 0,
2649 ),
2650 make_run_for_tree(
2651 "50000003-0000-0000-0000-000000000003",
2652 "2024-06-15T12:00:00+00:00",
2653 Some("50000002-0000-0000-0000-000000000002"),
2654 70,
2655 5,
2656 0,
2657 ),
2658 make_run_for_tree(
2659 "50000004-0000-0000-0000-000000000004",
2660 "2024-06-15T13:00:00+00:00",
2661 Some("50000001-0000-0000-0000-000000000001"),
2662 80,
2663 3,
2664 0,
2665 ),
2666 ];
2667 let snapshot = RunStoreSnapshot::new_for_test(runs);
2668 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2669 let theme_characters = ThemeCharacters::default();
2670
2671 insta::assert_snapshot!(
2672 "tree_branching_with_chains",
2673 DisplayRunList::new(
2674 &snapshot_with_replayability,
2675 None,
2676 &Styles::default(),
2677 &theme_characters,
2678 &Redactor::noop()
2679 )
2680 .to_string()
2681 );
2682 }
2683
2684 #[test]
2685 fn test_tree_continuation_flags() {
2686 let runs = vec![
2691 make_run_for_tree(
2692 "50000001-0000-0000-0000-000000000001",
2693 "2024-06-15T10:00:00+00:00",
2694 None,
2695 50,
2696 10,
2697 0,
2698 ),
2699 make_run_for_tree(
2700 "50000002-0000-0000-0000-000000000002",
2701 "2024-06-15T13:00:00+00:00", Some("50000001-0000-0000-0000-000000000001"),
2703 60,
2704 8,
2705 0,
2706 ),
2707 make_run_for_tree(
2708 "50000003-0000-0000-0000-000000000003",
2709 "2024-06-15T14:00:00+00:00",
2710 Some("50000002-0000-0000-0000-000000000002"),
2711 70,
2712 5,
2713 0,
2714 ),
2715 make_run_for_tree(
2716 "50000004-0000-0000-0000-000000000004",
2717 "2024-06-15T11:00:00+00:00", Some("50000001-0000-0000-0000-000000000001"),
2719 80,
2720 3,
2721 0,
2722 ),
2723 ];
2724 let snapshot = RunStoreSnapshot::new_for_test(runs);
2725 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2726 let theme_characters = ThemeCharacters::default();
2727
2728 insta::assert_snapshot!(
2729 "tree_continuation_flags",
2730 DisplayRunList::new(
2731 &snapshot_with_replayability,
2732 None,
2733 &Styles::default(),
2734 &theme_characters,
2735 &Redactor::noop()
2736 )
2737 .to_string()
2738 );
2739 }
2740
2741 #[test]
2742 fn test_tree_pruned_parent_with_chain() {
2743 let runs = vec![
2746 make_run_for_tree(
2747 "50000002-0000-0000-0000-000000000002",
2748 "2024-06-15T11:00:00+00:00",
2749 Some("50000001-0000-0000-0000-000000000001"), 60,
2751 8,
2752 0,
2753 ),
2754 make_run_for_tree(
2755 "50000003-0000-0000-0000-000000000003",
2756 "2024-06-15T12:00:00+00:00",
2757 Some("50000002-0000-0000-0000-000000000002"),
2758 70,
2759 5,
2760 0,
2761 ),
2762 ];
2763 let snapshot = RunStoreSnapshot::new_for_test(runs);
2764 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2765 let theme_characters = ThemeCharacters::default();
2766
2767 insta::assert_snapshot!(
2768 "tree_pruned_parent_with_chain",
2769 DisplayRunList::new(
2770 &snapshot_with_replayability,
2771 None,
2772 &Styles::default(),
2773 &theme_characters,
2774 &Redactor::noop()
2775 )
2776 .to_string()
2777 );
2778 }
2779
2780 #[test]
2781 fn test_tree_pruned_parent_with_multiple_children() {
2782 let runs = vec![
2785 make_run_for_tree(
2786 "50000002-0000-0000-0000-000000000002",
2787 "2024-06-15T11:00:00+00:00",
2788 Some("50000001-0000-0000-0000-000000000001"), 60,
2790 5,
2791 0,
2792 ),
2793 make_run_for_tree(
2794 "50000003-0000-0000-0000-000000000003",
2795 "2024-06-15T12:00:00+00:00",
2796 Some("50000001-0000-0000-0000-000000000001"), 70,
2798 3,
2799 0,
2800 ),
2801 ];
2802 let snapshot = RunStoreSnapshot::new_for_test(runs);
2803 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2804 let theme_characters = ThemeCharacters::default();
2805
2806 insta::assert_snapshot!(
2807 "tree_pruned_parent_with_multiple_children",
2808 DisplayRunList::new(
2809 &snapshot_with_replayability,
2810 None,
2811 &Styles::default(),
2812 &theme_characters,
2813 &Redactor::noop()
2814 )
2815 .to_string()
2816 );
2817 }
2818
2819 #[test]
2820 fn test_tree_unicode_characters() {
2821 let runs = vec![
2823 make_run_for_tree(
2824 "50000001-0000-0000-0000-000000000001",
2825 "2024-06-15T10:00:00+00:00",
2826 None,
2827 50,
2828 10,
2829 0,
2830 ),
2831 make_run_for_tree(
2832 "50000002-0000-0000-0000-000000000002",
2833 "2024-06-15T11:00:00+00:00",
2834 Some("50000001-0000-0000-0000-000000000001"),
2835 60,
2836 8,
2837 0,
2838 ),
2839 make_run_for_tree(
2840 "50000003-0000-0000-0000-000000000003",
2841 "2024-06-15T12:00:00+00:00",
2842 Some("50000001-0000-0000-0000-000000000001"),
2843 70,
2844 5,
2845 0,
2846 ),
2847 ];
2848 let snapshot = RunStoreSnapshot::new_for_test(runs);
2849 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2850 let mut theme_characters = ThemeCharacters::default();
2852 theme_characters.use_unicode();
2853
2854 insta::assert_snapshot!(
2855 "tree_unicode_characters",
2856 DisplayRunList::new(
2857 &snapshot_with_replayability,
2858 None,
2859 &Styles::default(),
2860 &theme_characters,
2861 &Redactor::noop()
2862 )
2863 .to_string()
2864 );
2865 }
2866
2867 #[test]
2868 fn test_tree_compressed_with_branching() {
2869 let runs = vec![
2884 make_run_for_tree(
2885 "50000001-0000-0000-0000-000000000001",
2886 "2024-06-15T10:00:00+00:00",
2887 None,
2888 50,
2889 10,
2890 0,
2891 ),
2892 make_run_for_tree(
2893 "50000002-0000-0000-0000-000000000002",
2894 "2024-06-15T11:00:00+00:00",
2895 Some("50000001-0000-0000-0000-000000000001"),
2896 60,
2897 8,
2898 0,
2899 ),
2900 make_run_for_tree(
2901 "50000003-0000-0000-0000-000000000003",
2902 "2024-06-15T14:00:00+00:00", Some("50000002-0000-0000-0000-000000000002"),
2904 70,
2905 5,
2906 0,
2907 ),
2908 make_run_for_tree(
2909 "50000004-0000-0000-0000-000000000004",
2910 "2024-06-15T15:00:00+00:00",
2911 Some("50000003-0000-0000-0000-000000000003"),
2912 80,
2913 3,
2914 0,
2915 ),
2916 make_run_for_tree(
2917 "50000005-0000-0000-0000-000000000005",
2918 "2024-06-15T12:00:00+00:00", Some("50000002-0000-0000-0000-000000000002"),
2920 90,
2921 2,
2922 0,
2923 ),
2924 ];
2925 let snapshot = RunStoreSnapshot::new_for_test(runs);
2926 let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
2927 let theme_characters = ThemeCharacters::default();
2928
2929 insta::assert_snapshot!(
2930 "tree_compressed_with_branching",
2931 DisplayRunList::new(
2932 &snapshot_with_replayability,
2933 None,
2934 &Styles::default(),
2935 &theme_characters,
2936 &Redactor::noop()
2937 )
2938 .to_string()
2939 );
2940 }
2941}