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