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