Skip to main content

nextest_runner/record/
display.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Display wrappers for record store types.
5//!
6//! This module provides formatting and display utilities for recorded run
7//! information, prune operations, and run lists.
8
9use 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/// Styles for displaying record store information.
28#[derive(Clone, Debug, Default)]
29pub struct Styles {
30    /// Style for the unique prefix portion of run IDs.
31    pub run_id_prefix: Style,
32    /// Style for the non-unique rest portion of run IDs.
33    pub run_id_rest: Style,
34    /// Style for timestamps.
35    pub timestamp: Style,
36    /// Style for duration values.
37    pub duration: Style,
38    /// Style for size values.
39    pub size: Style,
40    /// Style for counts and numbers.
41    pub count: Style,
42    /// Style for "passed" status.
43    pub passed: Style,
44    /// Style for "failed" status.
45    pub failed: Style,
46    /// Style for "cancelled" or "incomplete" status.
47    pub cancelled: Style,
48    /// Style for field labels in detailed view.
49    pub label: Style,
50    /// Style for section headers in detailed view.
51    pub section: Style,
52}
53
54impl Styles {
55    /// Colorizes the styles for terminal output.
56    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/// Alignment information for displaying a list of runs.
72///
73/// This struct precomputes the maximum widths needed for aligned display of
74/// run statistics. Use [`RunListAlignment::from_runs`] to create an instance
75/// from a slice of runs.
76#[derive(Clone, Copy, Debug, Default)]
77pub struct RunListAlignment {
78    /// Maximum width of the "passed" count across all runs.
79    pub passed_width: usize,
80    /// Maximum width of the size column across all runs.
81    ///
82    /// This is dynamically computed based on the formatted size display width
83    /// (e.g., "123 KB" or "1.5 MB"). The minimum width is 6 to maintain
84    /// alignment for typical sizes.
85    pub size_width: usize,
86    /// Maximum tree prefix width across all nodes, in units of 2-char segments.
87    ///
88    /// Used to align columns when displaying runs in a tree structure. Nodes
89    /// with smaller prefix widths get additional padding to align the timestamp
90    /// column.
91    pub tree_prefix_width: usize,
92}
93
94impl RunListAlignment {
95    /// Minimum width for the size column to maintain visual consistency.
96    const MIN_SIZE_WIDTH: usize = 9;
97
98    /// Creates alignment information from a slice of runs.
99    ///
100    /// Computes the maximum widths needed to align the statistics columns.
101    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    /// Creates alignment information from a slice of runs, also considering
123    /// the total size for the size column width.
124    ///
125    /// This should be used when displaying a run list with a total, to ensure
126    /// the total size aligns properly with individual run sizes.
127    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    /// Sets the tree prefix width from a [`RunTree`].
154    ///
155    /// Used when displaying runs in a tree structure to ensure proper column
156    /// alignment across nodes with different tree depths.
157    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/// A display wrapper for [`PruneResult`].
168///
169/// This wrapper implements [`fmt::Display`] to format the prune result as a
170/// human-readable summary.
171#[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        // For explicit pruning, show error details as a bulleted list.
222        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/// A display wrapper for [`PrunePlan`].
241///
242/// This wrapper implements [`fmt::Display`] to format the prune plan as a
243/// human-readable summary showing what would be deleted.
244#[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            // For prune display, we don't show replayability status.
268            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/// A display wrapper for [`RecordedRunInfo`].
288#[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 to display before the run ID. Defaults to "  " (base indent).
297    prefix: &'a str,
298    /// Padding to add after the run ID to align columns. Defaults to 0.
299    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        // Get the shortest unique prefix for jj-style highlighting.
307        let run_id_display =
308            if let Some(prefix_info) = self.run_id_index.shortest_unique_prefix(run.run_id) {
309                // Show the first 8 characters of the UUID with the unique
310                // prefix highlighted.
311                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                // Fallback if run ID not in index.
321                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        // Show replayability status if not replayable.
346        match self.replayability {
347            ReplayabilityStatus::Replayable => {}
348            ReplayabilityStatus::NotReplayable(_) => {
349                write!(f, "  ({})", "not replayable".style(self.styles.failed))?;
350            }
351            ReplayabilityStatus::Incomplete => {
352                // Don't show "incomplete" here because we already show that in
353                // the status column.
354            }
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    /// Sets the prefix and run ID padding for tree display.
383    ///
384    /// Used by [`DisplayRunList`] for tree-formatted output.
385    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    /// Formats the status portion of the display.
392    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    /// Formats statistics for a normal test run.
418    fn format_normal_stats(&self, stats: &CompletedRunStats, cancelled: bool) -> String {
419        // When no tests are run, show "passed" in yellow since it'll result in
420        // a failure most of the time.
421        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        // Right-align the passed count based on max width, then "passed".
433        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        // Calculate tests that were not run (neither passed nor failed).
451        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    /// Formats statistics for a stress test run.
472    fn format_stress_stats(&self, stats: &StressCompletedRunStats, cancelled: bool) -> String {
473        let mut result = String::new();
474
475        // Right-align the passed count based on max width, then "passed iterations".
476        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        // Calculate iterations that were not run (neither passed nor failed).
495        // Only shown when initial_iteration_count is known.
496        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
519/// A detailed display wrapper for [`RecordedRunInfo`].
520///
521/// Unlike [`DisplayRecordedRunInfo`] which produces a compact table row,
522/// this produces a multi-line detailed view with labeled fields.
523pub 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    /// Formats the run ID header with jj-style prefix highlighting.
555    fn format_run_id(&self) -> String {
556        self.format_run_id_with_prefix(self.run.run_id)
557    }
558
559    /// Formats a run ID with jj-style prefix highlighting.
560    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    /// Writes a labeled field.
576    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    /// Writes the status field with exit code for completed runs.
591    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    /// Writes the replayable field with yes/no/maybe styling and reasons.
613    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    /// Writes the stats section (tests or iterations).
645    fn write_stats_section(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
646        match &self.run.status {
647            RecordedRunStatus::Incomplete | RecordedRunStatus::Unknown => {
648                // No stats to show.
649                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    /// Writes the sizes section with compressed/uncompressed breakdown.
718    fn write_sizes_section(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
719        // Put "sizes:" on the same line as the column headers, using the same
720        // format as data rows for alignment.
721        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        // Draw a horizontal line before "total".
759        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            // Format total entries similar to KB and MB sizes.
779            sizes.total_entries().style(self.styles.size),
780        )
781    }
782
783    /// Formats env vars with redaction.
784    fn format_env_vars(&self) -> String {
785        self.redactor.redact_env_vars(&self.run.env_vars)
786    }
787
788    /// Formats CLI args with redaction.
789    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        // Header with full run ID.
799        writeln!(
800            f,
801            "{} {}",
802            "Run".style(self.styles.section),
803            self.format_run_id()
804        )?;
805        writeln!(f)?;
806
807        // Basic info fields.
808        self.write_field(
809            f,
810            "nextest version",
811            self.redactor.redact_version(&run.nextest_version),
812        )?;
813
814        // Parent run ID (if this is a rerun).
815        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        // CLI args (if present).
824        if !run.cli_args.is_empty() {
825            self.write_field(f, "command", self.format_cli_args())?;
826        }
827
828        // Environment variables (if present).
829        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        // Compute and display started at with relative duration.
835        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        // Compute and display last written at with relative duration.
852        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        // Stats section (tests or iterations).
882        self.write_stats_section(f)?;
883
884        // Sizes section.
885        self.write_sizes_section(f)?;
886
887        Ok(())
888    }
889}
890
891/// A display wrapper for a list of recorded runs.
892///
893/// This struct handles the full table display including:
894/// - Optional store path header (when verbose)
895/// - Run count header
896/// - Individual run rows with replayability status
897/// - Total size footer with separator
898pub 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    /// Creates a new display wrapper for a run list.
908    ///
909    /// The `snapshot_with_replayability` provides the runs and their precomputed
910    /// replayability status. Non-replayable runs will show a suffix with the reason.
911    /// The most recent run by start time will be marked with `*latest`.
912    ///
913    /// If `store_path` is provided, it will be displayed at the top of the output.
914    ///
915    /// If `redactor` is provided, timestamps, durations, and sizes will be
916    /// redacted for snapshot testing while preserving column alignment.
917    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        // Show store path if provided.
939        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            // No runs to display; the caller should handle the "no recorded runs" message
945            // via logging or other means.
946            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            // Nodes with smaller tree_prefix_width need more padding to align the timestamp column.
979            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                    // "???" is 3 chars, run_id is 8 chars, so we need 5 extra padding.
1008                    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        // Column positions: base_indent (2) + tree_prefix_width * 2 (tree chars) + run_id (8)
1023        // + separator (2) + timestamp (19) + separator (2) + duration (10) + separator (2).
1024        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
1049/// Formats the tree prefix for a given tree item.
1050///
1051/// The prefix includes:
1052/// - Base indent (2 spaces)
1053/// - Continuation lines for ancestor levels (`│ ` or `  `)
1054/// - Branch character (`├─` or `└─`) unless is_only_child
1055fn format_tree_prefix(theme_characters: &ThemeCharacters, item: &TreeIterItem) -> String {
1056    let mut prefix = String::new();
1057
1058    // Base indent (2 spaces, matching original format).
1059    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    /// Returns a fixed "now" time for testing relative duration display.
1104    ///
1105    /// This time is 30 seconds after the latest test timestamp used in the tests,
1106    /// which is "2024-06-25T13:00:00+00:00".
1107    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    /// Creates a `RecordedRunInfo` for testing display functions.
1114    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    /// Creates a `RecordedRunInfo` with a custom duration for testing.
1132    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        // For simplicity in tests, put all size in the store component.
1142        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    /// Creates a `RecordedRunInfo` for testing with cli_args and env_vars.
1166    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    /// Creates a `RecordedRunInfo` for testing with parent_run_id support.
1178    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        // Implicit pruning shows summary only, no error details.
1303        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        // Explicit pruning shows error details as a bulleted list.
1325        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        // Test case where some tests are not run (neither passed nor failed).
1395        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        // Test case where no tests were expected to run.
1420        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        // Test StressCompleted with all iterations passing.
1445        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        // Test StressCompleted with some failures.
1467        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        // Test StressCancelled with some iterations not run.
1492        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        // Test StressCancelled without initial_iteration_count (not run count unknown).
1514        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        // Test that alignment works correctly when runs have different passed counts.
1539        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        // All passed counts should be right-aligned to 3 digits (width of 559).
1581        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        // Test that stress test alignment works correctly.
1604        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        // Passed counts should be right-aligned to 4 digits (width of 1000).
1646        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        // Test 1: Normal sizes (typical case with multiple runs).
1746        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        // Test 2: Small sizes (1-2 digits, below the 6-char minimum width).
1799        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, // 1 KB
1805                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, // 99 KB
1817                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        // Test 3: Large sizes (7-8 digits, exceeding the 6-char minimum width).
1840        // 1,000,000 KB = ~1 GB, 10,000,000 KB = ~10 GB.
1841        // The size column and horizontal bar should dynamically expand.
1842        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, // 1,000,000 KB (~1 GB)
1848                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, // 10,000,000 KB (~10 GB)
1860                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        // Test 4: Varying durations (sub-second, seconds, tens, hundreds, thousands).
1883        // Duration is formatted as {:>9.3}s, so all should right-align properly.
1884        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, // sub-second
1891                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, // single digit seconds
1904                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, // tens of seconds
1917                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, // hundreds of seconds
1930                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, // thousands of seconds (~3.4 hours)
1943                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        // Test with CLI args and env vars populated.
1969        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        // Test with empty CLI args and env vars.
1996        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        // Test StressCompleted with all iterations passing.
2006        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        // Test StressCompleted with some failures.
2021        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        // Test StressCancelled with some iterations not run.
2036        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        // Test StressCancelled without initial_iteration_count.
2051        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        // Use default (definitely replayable) status for most tests.
2078        let replayable = ReplayabilityStatus::Replayable;
2079        // Use incomplete status for the incomplete run.
2080        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        // Test displaying a run with a parent run ID (rerun scenario).
2165        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        // Include both runs in the index so the parent run ID can be resolved.
2200        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        // Create a simple run for testing replayability display.
2225        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        // Test: definitely replayable (no issues).
2244        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        // Test: store format incompatible (major mismatch).
2259        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        // Test: missing store.zip.
2281        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        // Test: missing run.log.zst.
2297        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        // Test: status unknown.
2313        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        // Test: incomplete (maybe replayable).
2329        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        // Test: multiple blocking reasons.
2344        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    /// Creates a `RecordedRunInfo` for tree display tests with custom size.
2363    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        // parent -> child -> grandchild (compressed chain display).
2403        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        // parent -> child1, parent -> child2.
2449        // Children sorted by started_at descending (child2 newer, comes first).
2450        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        // Run whose parent doesn't exist (pruned).
2496        // Should show virtual parent with "???" indicator.
2497        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"), // Parent doesn't exist.
2501            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        // Independent run followed by a parent-child chain.
2525        // Blank line between them since the chain has structure.
2526        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        // 5-level deep chain to test continuation lines.
2572        // a -> b -> c -> d -> e
2573        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        // parent -> child1 -> grandchild1
2635        // parent -> child2
2636        // child1 is older, child2 is newer, so order: parent, child2, child1, grandchild1.
2637        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        // Test that continuation lines (│) appear correctly.
2691        // parent -> child1 -> grandchild1 (child1 is not last, needs │ continuation)
2692        // parent -> child2
2693        // child1 is newer, child2 is older, so child1 is not last.
2694        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", // Newer than child2.
2706                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", // Older than child1.
2722                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        // Pruned parent with a chain of descendants.
2748        // ??? (pruned) -> child -> grandchild
2749        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"), // Parent doesn't exist.
2754                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        // Virtual (pruned) parent with multiple direct children.
2787        // Both children should show branch characters (|- and \-).
2788        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"), // Parent doesn't exist.
2793                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"), // Same pruned parent.
2801                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        // Test with Unicode characters (├─, └─, │).
2826        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        // Create ThemeCharacters with Unicode enabled.
2855        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        // Tests the case where a compressed (only-child) node has multiple
2874        // children, and one child also has children.
2875        //
2876        // root (1) -> X (2, only child - compressed)
2877        //             ├─ Z1 (3, newer, not last)
2878        //             │  └─ W (4, only child - compressed)
2879        //             └─ Z2 (5, older, last)
2880        //
2881        // Expected display:
2882        //   1
2883        //   \-2        <- compressed (no branch for 2's only-child status)
2884        //     |-3      <- 2's first child (Z1)
2885        //     | 4      <- Z1's only child (W), compressed, with continuation for Z1
2886        //     \-5      <- 2's last child (Z2)
2887        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", // Newer - will be first
2907                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", // Older - will be last
2923                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}