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