nextest_runner/record/
replay.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Replay infrastructure for recorded test runs.
5//!
6//! This module provides the [`ReplayContext`] type for converting recorded events
7//! back into [`TestEvent`]s that can be displayed through the normal reporter
8//! infrastructure.
9
10use crate::{
11    errors::RecordReadError,
12    list::{OwnedTestInstanceId, TestInstanceId, TestList},
13    output_spec::{LiveSpec, RecordingSpec},
14    record::{
15        CoreEventKind, OutputEventKind, OutputFileName, StoreReader, StressConditionSummary,
16        StressIndexSummary, TestEventKindSummary, TestEventSummary, ZipStoreOutput,
17        ZipStoreOutputDescription,
18    },
19    reporter::events::{
20        ChildExecutionOutputDescription, ChildOutputDescription, ExecuteStatus, ExecutionStatuses,
21        RunStats, SetupScriptExecuteStatus, StressIndex, TestEvent, TestEventKind, TestsNotSeen,
22    },
23    run_mode::NextestRunMode,
24    runner::{StressCondition, StressCount},
25    test_output::ChildSingleOutput,
26};
27use bytes::Bytes;
28use nextest_metadata::{RustBinaryId, TestCaseName};
29use std::{collections::HashSet, num::NonZero};
30
31/// Whether to load output from the archive during replay conversion.
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33pub enum LoadOutput {
34    /// Load output from the archive.
35    Load,
36    /// Skip loading output.
37    Skip,
38}
39
40/// Context for replaying recorded test events.
41///
42/// This struct owns the data necessary to convert [`TestEventSummary`] back into
43/// [`TestEvent`] for display through the normal reporter infrastructure.
44///
45/// The lifetime `'a` is tied to the [`TestList`] that was reconstructed from the
46/// archived metadata.
47pub struct ReplayContext<'a> {
48    /// Set of test instances, used for lifetime ownership.
49    test_data: HashSet<OwnedTestInstanceId>,
50
51    /// The test list reconstructed from the archive.
52    test_list: &'a TestList<'a>,
53}
54
55impl<'a> ReplayContext<'a> {
56    /// Creates a new replay context with the given test list.
57    ///
58    /// The test list should be reconstructed from the archived metadata using
59    /// [`TestList::from_summary`].
60    pub fn new(test_list: &'a TestList<'a>) -> Self {
61        Self {
62            test_data: HashSet::new(),
63            test_list,
64        }
65    }
66
67    /// Returns the run mode.
68    pub fn mode(&self) -> NextestRunMode {
69        self.test_list.mode()
70    }
71
72    /// Registers a test instance.
73    ///
74    /// This is required for lifetime reasons. This must be called before
75    /// converting events that reference this test.
76    pub fn register_test(&mut self, test_instance: OwnedTestInstanceId) {
77        self.test_data.insert(test_instance);
78    }
79
80    /// Looks up a test instance ID by its owned form.
81    ///
82    /// Returns `None` if the test was not previously registered.
83    pub fn lookup_test_instance_id(
84        &self,
85        test_instance: &OwnedTestInstanceId,
86    ) -> Option<TestInstanceId<'_>> {
87        self.test_data.get(test_instance).map(|data| data.as_ref())
88    }
89
90    /// Converts a test event summary to a test event.
91    ///
92    /// Returns `None` for events that cannot be converted (e.g., because they
93    /// reference tests that weren't registered).
94    pub fn convert_event<'cx>(
95        &'cx self,
96        summary: &TestEventSummary<RecordingSpec>,
97        reader: &mut dyn StoreReader,
98        load_output: LoadOutput,
99    ) -> Result<TestEvent<'cx>, ReplayConversionError> {
100        let kind = self.convert_event_kind(&summary.kind, reader, load_output)?;
101        Ok(TestEvent {
102            timestamp: summary.timestamp,
103            elapsed: summary.elapsed,
104            kind,
105        })
106    }
107
108    fn convert_event_kind<'cx>(
109        &'cx self,
110        kind: &TestEventKindSummary<RecordingSpec>,
111        reader: &mut dyn StoreReader,
112        load_output: LoadOutput,
113    ) -> Result<TestEventKind<'cx>, ReplayConversionError> {
114        match kind {
115            TestEventKindSummary::Core(core) => self.convert_core_event(core),
116            TestEventKindSummary::Output(output) => {
117                self.convert_output_event(output, reader, load_output)
118            }
119        }
120    }
121
122    fn convert_core_event<'cx>(
123        &'cx self,
124        kind: &CoreEventKind,
125    ) -> Result<TestEventKind<'cx>, ReplayConversionError> {
126        match kind {
127            CoreEventKind::RunStarted {
128                run_id,
129                profile_name,
130                cli_args,
131                stress_condition,
132            } => {
133                let stress_condition = stress_condition
134                    .as_ref()
135                    .map(convert_stress_condition)
136                    .transpose()?;
137                Ok(TestEventKind::RunStarted {
138                    test_list: self.test_list,
139                    run_id: *run_id,
140                    profile_name: profile_name.clone(),
141                    cli_args: cli_args.clone(),
142                    stress_condition,
143                })
144            }
145
146            CoreEventKind::StressSubRunStarted { progress } => {
147                Ok(TestEventKind::StressSubRunStarted {
148                    progress: *progress,
149                })
150            }
151
152            CoreEventKind::SetupScriptStarted {
153                stress_index,
154                index,
155                total,
156                script_id,
157                program,
158                args,
159                no_capture,
160            } => Ok(TestEventKind::SetupScriptStarted {
161                stress_index: stress_index.as_ref().map(convert_stress_index),
162                index: *index,
163                total: *total,
164                script_id: script_id.clone(),
165                program: program.clone(),
166                args: args.clone(),
167                no_capture: *no_capture,
168            }),
169
170            CoreEventKind::SetupScriptSlow {
171                stress_index,
172                script_id,
173                program,
174                args,
175                elapsed,
176                will_terminate,
177            } => Ok(TestEventKind::SetupScriptSlow {
178                stress_index: stress_index.as_ref().map(convert_stress_index),
179                script_id: script_id.clone(),
180                program: program.clone(),
181                args: args.clone(),
182                elapsed: *elapsed,
183                will_terminate: *will_terminate,
184            }),
185
186            CoreEventKind::TestStarted {
187                stress_index,
188                test_instance,
189                current_stats,
190                running,
191                command_line,
192            } => {
193                let instance_id = self.lookup_test_instance_id(test_instance).ok_or_else(|| {
194                    ReplayConversionError::TestNotFound {
195                        binary_id: test_instance.binary_id.clone(),
196                        test_name: test_instance.test_name.clone(),
197                    }
198                })?;
199                Ok(TestEventKind::TestStarted {
200                    stress_index: stress_index.as_ref().map(convert_stress_index),
201                    test_instance: instance_id,
202                    current_stats: *current_stats,
203                    running: *running,
204                    command_line: command_line.clone(),
205                })
206            }
207
208            CoreEventKind::TestSlow {
209                stress_index,
210                test_instance,
211                retry_data,
212                elapsed,
213                will_terminate,
214            } => {
215                let instance_id = self.lookup_test_instance_id(test_instance).ok_or_else(|| {
216                    ReplayConversionError::TestNotFound {
217                        binary_id: test_instance.binary_id.clone(),
218                        test_name: test_instance.test_name.clone(),
219                    }
220                })?;
221                Ok(TestEventKind::TestSlow {
222                    stress_index: stress_index.as_ref().map(convert_stress_index),
223                    test_instance: instance_id,
224                    retry_data: *retry_data,
225                    elapsed: *elapsed,
226                    will_terminate: *will_terminate,
227                })
228            }
229
230            CoreEventKind::TestRetryStarted {
231                stress_index,
232                test_instance,
233                retry_data,
234                running,
235                command_line,
236            } => {
237                let instance_id = self.lookup_test_instance_id(test_instance).ok_or_else(|| {
238                    ReplayConversionError::TestNotFound {
239                        binary_id: test_instance.binary_id.clone(),
240                        test_name: test_instance.test_name.clone(),
241                    }
242                })?;
243                Ok(TestEventKind::TestRetryStarted {
244                    stress_index: stress_index.as_ref().map(convert_stress_index),
245                    test_instance: instance_id,
246                    retry_data: *retry_data,
247                    running: *running,
248                    command_line: command_line.clone(),
249                })
250            }
251
252            CoreEventKind::TestSkipped {
253                stress_index,
254                test_instance,
255                reason,
256            } => {
257                let instance_id = self.lookup_test_instance_id(test_instance).ok_or_else(|| {
258                    ReplayConversionError::TestNotFound {
259                        binary_id: test_instance.binary_id.clone(),
260                        test_name: test_instance.test_name.clone(),
261                    }
262                })?;
263                Ok(TestEventKind::TestSkipped {
264                    stress_index: stress_index.as_ref().map(convert_stress_index),
265                    test_instance: instance_id,
266                    reason: *reason,
267                })
268            }
269
270            CoreEventKind::RunBeginCancel {
271                setup_scripts_running,
272                running,
273                reason,
274            } => {
275                let stats = RunStats {
276                    cancel_reason: Some(*reason),
277                    ..Default::default()
278                };
279                Ok(TestEventKind::RunBeginCancel {
280                    setup_scripts_running: *setup_scripts_running,
281                    current_stats: stats,
282                    running: *running,
283                })
284            }
285
286            CoreEventKind::RunPaused {
287                setup_scripts_running,
288                running,
289            } => Ok(TestEventKind::RunPaused {
290                setup_scripts_running: *setup_scripts_running,
291                running: *running,
292            }),
293
294            CoreEventKind::RunContinued {
295                setup_scripts_running,
296                running,
297            } => Ok(TestEventKind::RunContinued {
298                setup_scripts_running: *setup_scripts_running,
299                running: *running,
300            }),
301
302            CoreEventKind::StressSubRunFinished {
303                progress,
304                sub_elapsed,
305                sub_stats,
306            } => Ok(TestEventKind::StressSubRunFinished {
307                progress: *progress,
308                sub_elapsed: *sub_elapsed,
309                sub_stats: *sub_stats,
310            }),
311
312            CoreEventKind::RunFinished {
313                run_id,
314                start_time,
315                elapsed,
316                run_stats,
317                outstanding_not_seen,
318            } => Ok(TestEventKind::RunFinished {
319                run_id: *run_id,
320                start_time: *start_time,
321                elapsed: *elapsed,
322                run_stats: *run_stats,
323                outstanding_not_seen: outstanding_not_seen.as_ref().map(|t| TestsNotSeen {
324                    not_seen: t.not_seen.clone(),
325                    total_not_seen: t.total_not_seen,
326                }),
327            }),
328        }
329    }
330
331    fn convert_output_event<'cx>(
332        &'cx self,
333        kind: &OutputEventKind<RecordingSpec>,
334        reader: &mut dyn StoreReader,
335        load_output: LoadOutput,
336    ) -> Result<TestEventKind<'cx>, ReplayConversionError> {
337        match kind {
338            OutputEventKind::SetupScriptFinished {
339                stress_index,
340                index,
341                total,
342                script_id,
343                program,
344                args,
345                no_capture,
346                run_status,
347            } => Ok(TestEventKind::SetupScriptFinished {
348                stress_index: stress_index.as_ref().map(convert_stress_index),
349                index: *index,
350                total: *total,
351                script_id: script_id.clone(),
352                program: program.clone(),
353                args: args.clone(),
354                junit_store_success_output: false,
355                junit_store_failure_output: false,
356                no_capture: *no_capture,
357                run_status: convert_setup_script_status(run_status, reader, load_output)?,
358            }),
359
360            OutputEventKind::TestAttemptFailedWillRetry {
361                stress_index,
362                test_instance,
363                run_status,
364                delay_before_next_attempt,
365                failure_output,
366                running,
367            } => {
368                let instance_id = self.lookup_test_instance_id(test_instance).ok_or_else(|| {
369                    ReplayConversionError::TestNotFound {
370                        binary_id: test_instance.binary_id.clone(),
371                        test_name: test_instance.test_name.clone(),
372                    }
373                })?;
374                Ok(TestEventKind::TestAttemptFailedWillRetry {
375                    stress_index: stress_index.as_ref().map(convert_stress_index),
376                    test_instance: instance_id,
377                    run_status: convert_execute_status(run_status, reader, load_output)?,
378                    delay_before_next_attempt: *delay_before_next_attempt,
379                    failure_output: *failure_output,
380                    running: *running,
381                })
382            }
383
384            OutputEventKind::TestFinished {
385                stress_index,
386                test_instance,
387                success_output,
388                failure_output,
389                junit_store_success_output,
390                junit_store_failure_output,
391                run_statuses,
392                current_stats,
393                running,
394            } => {
395                let instance_id = self.lookup_test_instance_id(test_instance).ok_or_else(|| {
396                    ReplayConversionError::TestNotFound {
397                        binary_id: test_instance.binary_id.clone(),
398                        test_name: test_instance.test_name.clone(),
399                    }
400                })?;
401                Ok(TestEventKind::TestFinished {
402                    stress_index: stress_index.as_ref().map(convert_stress_index),
403                    test_instance: instance_id,
404                    success_output: *success_output,
405                    failure_output: *failure_output,
406                    junit_store_success_output: *junit_store_success_output,
407                    junit_store_failure_output: *junit_store_failure_output,
408                    run_statuses: convert_execution_statuses(run_statuses, reader, load_output)?,
409                    current_stats: *current_stats,
410                    running: *running,
411                })
412            }
413        }
414    }
415}
416
417/// Error during replay event conversion.
418#[derive(Debug, thiserror::Error)]
419#[non_exhaustive]
420pub enum ReplayConversionError {
421    /// Test not found in replay context.
422    #[error("test not found under `{binary_id}`: {test_name}")]
423    TestNotFound {
424        /// The binary ID.
425        binary_id: RustBinaryId,
426        /// The test name.
427        test_name: TestCaseName,
428    },
429
430    /// Error reading a record.
431    #[error("error reading record")]
432    RecordRead(#[from] RecordReadError),
433
434    /// Invalid stress count in recorded data.
435    #[error("invalid stress count: expected non-zero value, got 0")]
436    InvalidStressCount,
437}
438
439// --- Conversion helpers ---
440
441fn convert_stress_condition(
442    summary: &StressConditionSummary,
443) -> Result<StressCondition, ReplayConversionError> {
444    match summary {
445        StressConditionSummary::Count { count } => {
446            let stress_count = match count {
447                Some(n) => {
448                    let non_zero =
449                        NonZero::new(*n).ok_or(ReplayConversionError::InvalidStressCount)?;
450                    StressCount::Count { count: non_zero }
451                }
452                None => StressCount::Infinite,
453            };
454            Ok(StressCondition::Count(stress_count))
455        }
456        StressConditionSummary::Duration { duration } => Ok(StressCondition::Duration(*duration)),
457    }
458}
459
460fn convert_stress_index(summary: &StressIndexSummary) -> StressIndex {
461    StressIndex {
462        current: summary.current,
463        total: summary.total,
464    }
465}
466
467fn convert_execute_status(
468    status: &ExecuteStatus<RecordingSpec>,
469    reader: &mut dyn StoreReader,
470    load_output: LoadOutput,
471) -> Result<ExecuteStatus<LiveSpec>, ReplayConversionError> {
472    let output = convert_child_execution_output(&status.output, reader, load_output)?;
473    Ok(ExecuteStatus {
474        retry_data: status.retry_data,
475        output,
476        result: status.result.clone(),
477        start_time: status.start_time,
478        time_taken: status.time_taken,
479        is_slow: status.is_slow,
480        delay_before_start: status.delay_before_start,
481        error_summary: status.error_summary.clone(),
482        output_error_slice: status.output_error_slice.clone(),
483    })
484}
485
486fn convert_execution_statuses(
487    statuses: &ExecutionStatuses<RecordingSpec>,
488    reader: &mut dyn StoreReader,
489    load_output: LoadOutput,
490) -> Result<ExecutionStatuses<LiveSpec>, ReplayConversionError> {
491    let statuses: Vec<ExecuteStatus<LiveSpec>> = statuses
492        .iter()
493        .map(|s| convert_execute_status(s, reader, load_output))
494        .collect::<Result<_, _>>()?;
495
496    Ok(ExecutionStatuses::new(statuses))
497}
498
499fn convert_setup_script_status(
500    status: &SetupScriptExecuteStatus<RecordingSpec>,
501    reader: &mut dyn StoreReader,
502    load_output: LoadOutput,
503) -> Result<SetupScriptExecuteStatus<LiveSpec>, ReplayConversionError> {
504    let output = convert_child_execution_output(&status.output, reader, load_output)?;
505    Ok(SetupScriptExecuteStatus {
506        output,
507        result: status.result.clone(),
508        start_time: status.start_time,
509        time_taken: status.time_taken,
510        is_slow: status.is_slow,
511        env_map: status.env_map.clone(),
512        error_summary: status.error_summary.clone(),
513    })
514}
515
516fn convert_child_execution_output(
517    output: &ChildExecutionOutputDescription<RecordingSpec>,
518    reader: &mut dyn StoreReader,
519    load_output: LoadOutput,
520) -> Result<ChildExecutionOutputDescription<LiveSpec>, ReplayConversionError> {
521    match output {
522        ChildExecutionOutputDescription::Output {
523            result,
524            output,
525            errors,
526        } => {
527            let output = convert_child_output(output, reader, load_output)?;
528            Ok(ChildExecutionOutputDescription::Output {
529                result: result.clone(),
530                output,
531                errors: errors.clone(),
532            })
533        }
534        ChildExecutionOutputDescription::StartError(err) => {
535            Ok(ChildExecutionOutputDescription::StartError(err.clone()))
536        }
537    }
538}
539
540fn convert_child_output(
541    output: &ZipStoreOutputDescription,
542    reader: &mut dyn StoreReader,
543    load_output: LoadOutput,
544) -> Result<ChildOutputDescription, ReplayConversionError> {
545    if load_output == LoadOutput::Skip {
546        return Ok(ChildOutputDescription::NotLoaded);
547    }
548
549    match output {
550        ZipStoreOutputDescription::Split { stdout, stderr } => {
551            let stdout = stdout
552                .as_ref()
553                .map(|o| read_output_as_child_single(reader, o))
554                .transpose()?;
555            let stderr = stderr
556                .as_ref()
557                .map(|o| read_output_as_child_single(reader, o))
558                .transpose()?;
559            Ok(ChildOutputDescription::Split { stdout, stderr })
560        }
561        ZipStoreOutputDescription::Combined { output } => {
562            let output = read_output_as_child_single(reader, output)?;
563            Ok(ChildOutputDescription::Combined { output })
564        }
565    }
566}
567
568fn read_output_as_child_single(
569    reader: &mut dyn StoreReader,
570    output: &ZipStoreOutput,
571) -> Result<ChildSingleOutput, ReplayConversionError> {
572    let bytes = read_output_file(reader, output.file_name().map(OutputFileName::as_str))?;
573    Ok(ChildSingleOutput::from(bytes.unwrap_or_default()))
574}
575
576fn read_output_file(
577    reader: &mut dyn StoreReader,
578    file_name: Option<&str>,
579) -> Result<Option<Bytes>, ReplayConversionError> {
580    match file_name {
581        Some(name) => {
582            let bytes = reader.read_output(name)?;
583            Ok(Some(Bytes::from(bytes)))
584        }
585        None => Ok(None),
586    }
587}
588
589// --- ReplayReporter ---
590
591use crate::{
592    config::overrides::CompiledDefaultFilter,
593    errors::WriteEventError,
594    record::{
595        run_id_index::{RunIdIndex, ShortestRunIdPrefix},
596        store::{RecordedRunInfo, RecordedRunStatus},
597    },
598    reporter::{
599        DisplayConfig, DisplayReporter, DisplayReporterBuilder, DisplayerKind, FinalStatusLevel,
600        MaxProgressRunning, OutputLoadDecider, ReporterOutput, ShowProgress, ShowTerminalProgress,
601        StatusLevel, TestOutputDisplay,
602    },
603};
604use chrono::{DateTime, FixedOffset};
605use quick_junit::ReportUuid;
606
607/// Header information for a replay session.
608///
609/// This struct contains metadata about the recorded run being replayed,
610/// which is displayed at the start of replay output.
611#[derive(Clone, Debug)]
612pub struct ReplayHeader {
613    /// The run ID being replayed.
614    pub run_id: ReportUuid,
615    /// The shortest unique prefix for the run ID, used for highlighting.
616    ///
617    /// This is `None` if a run ID index was not provided during construction
618    /// (e.g., when replaying a single run without store context).
619    pub unique_prefix: Option<ShortestRunIdPrefix>,
620    /// When the run started.
621    pub started_at: DateTime<FixedOffset>,
622    /// The status of the run.
623    pub status: RecordedRunStatus,
624}
625
626impl ReplayHeader {
627    /// Creates a new replay header from run info.
628    ///
629    /// The `run_id_index` parameter enables unique prefix highlighting similar
630    /// to `cargo nextest store list`. If provided, the shortest unique prefix
631    /// for this run ID will be computed and stored for highlighted display.
632    pub fn new(
633        run_id: ReportUuid,
634        run_info: &RecordedRunInfo,
635        run_id_index: Option<&RunIdIndex>,
636    ) -> Self {
637        let unique_prefix = run_id_index.and_then(|index| index.shortest_unique_prefix(run_id));
638        Self {
639            run_id,
640            unique_prefix,
641            started_at: run_info.started_at,
642            status: run_info.status.clone(),
643        }
644    }
645}
646
647/// Builder for creating a [`ReplayReporter`].
648#[derive(Debug)]
649pub struct ReplayReporterBuilder {
650    status_level: StatusLevel,
651    final_status_level: FinalStatusLevel,
652    success_output: Option<TestOutputDisplay>,
653    failure_output: Option<TestOutputDisplay>,
654    should_colorize: bool,
655    verbose: bool,
656    show_progress: ShowProgress,
657    max_progress_running: MaxProgressRunning,
658    no_output_indent: bool,
659}
660
661impl Default for ReplayReporterBuilder {
662    fn default() -> Self {
663        Self {
664            status_level: StatusLevel::Pass,
665            final_status_level: FinalStatusLevel::Fail,
666            success_output: None,
667            failure_output: None,
668            should_colorize: false,
669            verbose: false,
670            show_progress: ShowProgress::default(),
671            max_progress_running: MaxProgressRunning::default(),
672            no_output_indent: false,
673        }
674    }
675}
676
677impl ReplayReporterBuilder {
678    /// Creates a new builder with default settings.
679    pub fn new() -> Self {
680        Self::default()
681    }
682
683    /// Sets the status level for output during the run.
684    pub fn set_status_level(&mut self, status_level: StatusLevel) -> &mut Self {
685        self.status_level = status_level;
686        self
687    }
688
689    /// Sets the final status level for output at the end of the run.
690    pub fn set_final_status_level(&mut self, final_status_level: FinalStatusLevel) -> &mut Self {
691        self.final_status_level = final_status_level;
692        self
693    }
694
695    /// Sets the success output display mode.
696    pub fn set_success_output(&mut self, output: TestOutputDisplay) -> &mut Self {
697        self.success_output = Some(output);
698        self
699    }
700
701    /// Sets the failure output display mode.
702    pub fn set_failure_output(&mut self, output: TestOutputDisplay) -> &mut Self {
703        self.failure_output = Some(output);
704        self
705    }
706
707    /// Sets whether output should be colorized.
708    pub fn set_colorize(&mut self, colorize: bool) -> &mut Self {
709        self.should_colorize = colorize;
710        self
711    }
712
713    /// Sets whether verbose output is enabled.
714    pub fn set_verbose(&mut self, verbose: bool) -> &mut Self {
715        self.verbose = verbose;
716        self
717    }
718
719    /// Sets the progress display mode.
720    pub fn set_show_progress(&mut self, show_progress: ShowProgress) -> &mut Self {
721        self.show_progress = show_progress;
722        self
723    }
724
725    /// Sets the maximum number of running tests to show in progress.
726    pub fn set_max_progress_running(
727        &mut self,
728        max_progress_running: MaxProgressRunning,
729    ) -> &mut Self {
730        self.max_progress_running = max_progress_running;
731        self
732    }
733
734    /// Sets whether to disable output indentation.
735    pub fn set_no_output_indent(&mut self, no_output_indent: bool) -> &mut Self {
736        self.no_output_indent = no_output_indent;
737        self
738    }
739
740    /// Builds the replay reporter with the given output destination.
741    pub fn build<'a>(
742        self,
743        mode: NextestRunMode,
744        run_count: usize,
745        output: ReporterOutput<'a>,
746    ) -> ReplayReporter<'a> {
747        let display_reporter = DisplayReporterBuilder {
748            mode,
749            default_filter: CompiledDefaultFilter::for_default_config(),
750            display_config: DisplayConfig::with_overrides(
751                self.show_progress,
752                false, // Replay never uses no-capture.
753                self.status_level,
754                self.final_status_level,
755            ),
756            run_count,
757            success_output: self.success_output,
758            failure_output: self.failure_output,
759            should_colorize: self.should_colorize,
760            verbose: self.verbose,
761            no_output_indent: self.no_output_indent,
762            max_progress_running: self.max_progress_running,
763            // For replay, we don't show terminal progress (OSC 9;4 codes) since
764            // we're replaying events, not running live tests.
765            show_term_progress: ShowTerminalProgress::No,
766            displayer_kind: DisplayerKind::Replay,
767        }
768        .build(output);
769
770        ReplayReporter { display_reporter }
771    }
772}
773
774/// Reporter for replaying recorded test runs.
775///
776/// This struct wraps a `DisplayReporter` configured for replay mode. It does
777/// not include terminal progress reporting (OSC 9;4 codes) since replays are
778/// not live test runs.
779///
780/// The lifetime `'a` represents the lifetime of the data backing the events.
781/// Typically this is the lifetime of the [`ReplayContext`] being used to
782/// convert recorded events.
783pub struct ReplayReporter<'a> {
784    display_reporter: DisplayReporter<'a>,
785}
786
787impl<'a> ReplayReporter<'a> {
788    /// Returns an [`OutputLoadDecider`] for this reporter.
789    ///
790    /// The decider examines event metadata and the reporter's display
791    /// configuration to decide whether output should be loaded from the
792    /// archive during replay.
793    pub fn output_load_decider(&self) -> OutputLoadDecider {
794        self.display_reporter.output_load_decider()
795    }
796
797    /// Writes the replay header to the output.
798    ///
799    /// This should be called before processing any recorded events to display
800    /// information about the run being replayed.
801    pub fn write_header(&mut self, header: &ReplayHeader) -> Result<(), WriteEventError> {
802        self.display_reporter.write_replay_header(header)
803    }
804
805    /// Writes a test event to the reporter.
806    pub fn write_event(&mut self, event: &TestEvent<'a>) -> Result<(), WriteEventError> {
807        self.display_reporter.write_event(event)
808    }
809
810    /// Finishes the reporter, writing any final output.
811    pub fn finish(mut self) {
812        self.display_reporter.finish();
813    }
814}