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