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    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}
605
606impl ReplayHeader {
607    /// Creates a new replay header from run info.
608    ///
609    /// The `run_id_index` parameter enables unique prefix highlighting similar
610    /// to `cargo nextest store list`. If provided, the shortest unique prefix
611    /// for this run ID will be computed and stored for highlighted display.
612    pub fn new(
613        run_id: ReportUuid,
614        run_info: &RecordedRunInfo,
615        run_id_index: Option<&RunIdIndex>,
616    ) -> Self {
617        let unique_prefix = run_id_index.and_then(|index| index.shortest_unique_prefix(run_id));
618        Self {
619            run_id,
620            unique_prefix,
621            started_at: run_info.started_at,
622            status: run_info.status.clone(),
623        }
624    }
625}
626
627/// Builder for creating a [`ReplayReporter`].
628#[derive(Debug)]
629pub struct ReplayReporterBuilder {
630    status_level: StatusLevel,
631    final_status_level: FinalStatusLevel,
632    success_output: Option<TestOutputDisplay>,
633    failure_output: Option<TestOutputDisplay>,
634    should_colorize: bool,
635    verbose: bool,
636    show_progress: ShowProgress,
637    max_progress_running: MaxProgressRunning,
638    no_output_indent: bool,
639}
640
641impl Default for ReplayReporterBuilder {
642    fn default() -> Self {
643        Self {
644            status_level: StatusLevel::Pass,
645            final_status_level: FinalStatusLevel::Fail,
646            success_output: None,
647            failure_output: None,
648            should_colorize: false,
649            verbose: false,
650            show_progress: ShowProgress::Auto,
651            max_progress_running: MaxProgressRunning::default(),
652            no_output_indent: false,
653        }
654    }
655}
656
657impl ReplayReporterBuilder {
658    /// Creates a new builder with default settings.
659    pub fn new() -> Self {
660        Self::default()
661    }
662
663    /// Sets the status level for output during the run.
664    pub fn set_status_level(&mut self, status_level: StatusLevel) -> &mut Self {
665        self.status_level = status_level;
666        self
667    }
668
669    /// Sets the final status level for output at the end of the run.
670    pub fn set_final_status_level(&mut self, final_status_level: FinalStatusLevel) -> &mut Self {
671        self.final_status_level = final_status_level;
672        self
673    }
674
675    /// Sets the success output display mode.
676    pub fn set_success_output(&mut self, output: TestOutputDisplay) -> &mut Self {
677        self.success_output = Some(output);
678        self
679    }
680
681    /// Sets the failure output display mode.
682    pub fn set_failure_output(&mut self, output: TestOutputDisplay) -> &mut Self {
683        self.failure_output = Some(output);
684        self
685    }
686
687    /// Sets whether output should be colorized.
688    pub fn set_colorize(&mut self, colorize: bool) -> &mut Self {
689        self.should_colorize = colorize;
690        self
691    }
692
693    /// Sets whether verbose output is enabled.
694    pub fn set_verbose(&mut self, verbose: bool) -> &mut Self {
695        self.verbose = verbose;
696        self
697    }
698
699    /// Sets the progress display mode.
700    pub fn set_show_progress(&mut self, show_progress: ShowProgress) -> &mut Self {
701        self.show_progress = show_progress;
702        self
703    }
704
705    /// Sets the maximum number of running tests to show in progress.
706    pub fn set_max_progress_running(
707        &mut self,
708        max_progress_running: MaxProgressRunning,
709    ) -> &mut Self {
710        self.max_progress_running = max_progress_running;
711        self
712    }
713
714    /// Sets whether to disable output indentation.
715    pub fn set_no_output_indent(&mut self, no_output_indent: bool) -> &mut Self {
716        self.no_output_indent = no_output_indent;
717        self
718    }
719
720    /// Builds the replay reporter with the given output destination.
721    pub fn build<'a>(
722        self,
723        mode: NextestRunMode,
724        test_count: usize,
725        output: ReporterOutput<'a>,
726    ) -> ReplayReporter<'a> {
727        let display_reporter = DisplayReporterBuilder {
728            mode,
729            default_filter: CompiledDefaultFilter::for_default_config(),
730            status_levels: StatusLevels {
731                status_level: self.status_level,
732                final_status_level: self.final_status_level,
733            },
734            test_count,
735            success_output: self.success_output,
736            failure_output: self.failure_output,
737            should_colorize: self.should_colorize,
738            no_capture: false,
739            verbose: self.verbose,
740            show_progress: self.show_progress,
741            no_output_indent: self.no_output_indent,
742            max_progress_running: self.max_progress_running,
743            // For replay, we don't show terminal progress (OSC 9;4 codes) since
744            // we're replaying events, not running live tests.
745            show_term_progress: ShowTerminalProgress::No,
746        }
747        .build(output);
748
749        ReplayReporter { display_reporter }
750    }
751}
752
753/// Reporter for replaying recorded test runs.
754///
755/// This struct wraps a `DisplayReporter` configured for replay mode. It does
756/// not include terminal progress reporting (OSC 9;4 codes) since replays are
757/// not live test runs.
758///
759/// The lifetime `'a` represents the lifetime of the data backing the events.
760/// Typically this is the lifetime of the [`ReplayContext`] being used to
761/// convert recorded events.
762pub struct ReplayReporter<'a> {
763    display_reporter: DisplayReporter<'a>,
764}
765
766impl<'a> ReplayReporter<'a> {
767    /// Writes the replay header to the output.
768    ///
769    /// This should be called before processing any recorded events to display
770    /// information about the run being replayed.
771    pub fn write_header(&mut self, header: &ReplayHeader) -> Result<(), WriteEventError> {
772        self.display_reporter.write_replay_header(header)
773    }
774
775    /// Writes a test event to the reporter.
776    pub fn write_event(&mut self, event: &TestEvent<'a>) -> Result<(), WriteEventError> {
777        self.display_reporter.write_event(event)
778    }
779
780    /// Finishes the reporter, writing any final output.
781    pub fn finish(mut self) {
782        self.display_reporter.finish();
783    }
784}