Skip to main content

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