Skip to main content

nextest_runner/record/
display.rs

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