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