Skip to main content

nextest_runner/reporter/structured/
recorder.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Reporter for recording test runs to disk.
5
6use crate::{
7    errors::RecordReporterError,
8    output_spec::LiveSpec,
9    record::{RecordOpts, RunRecorder, StoreSizes, TestEventSummary},
10    reporter::events::TestEvent,
11};
12use nextest_metadata::TestListSummary;
13use std::{
14    any::Any,
15    sync::{Arc, mpsc},
16    thread::JoinHandle,
17};
18
19/// A reporter that records test runs to disk.
20///
21/// This reporter runs in a separate thread, receiving events via a bounded
22/// channel. Events are converted to serializable form and written to the
23/// archive asynchronously.
24#[derive(Debug)]
25pub struct RecordReporter {
26    sender: mpsc::SyncSender<RecordEvent>,
27    handle: JoinHandle<Result<StoreSizes, RecordReporterError>>,
28}
29
30impl RecordReporter {
31    /// Creates a new `RecordReporter` with the given recorder.
32    pub fn new(run_recorder: RunRecorder) -> Self {
33        // Spawn a thread to do the writing. Use a bounded channel with backpressure.
34        let (sender, receiver) = mpsc::sync_channel(128);
35        let handle = std::thread::spawn(move || {
36            let mut writer = RecordReporterWriter { run_recorder };
37            while let Ok(event) = receiver.recv() {
38                writer.handle_event(event)?;
39            }
40
41            // The sender has been dropped. Finish writing and exit.
42            writer.finish()
43        });
44
45        Self { sender, handle }
46    }
47
48    /// Writes metadata to the recorder.
49    ///
50    /// This should be called once at the beginning of a test run.
51    pub fn write_meta(
52        &self,
53        cargo_metadata_json: Arc<String>,
54        test_list: TestListSummary,
55        opts: RecordOpts,
56    ) {
57        let event = RecordEvent::Meta {
58            cargo_metadata_json,
59            test_list,
60            opts,
61        };
62        // Ignore send errors because they indicate that the receiver has exited
63        // (likely due to an error, which is dealt with in finish()).
64        _ = self.sender.send(event);
65    }
66
67    /// Writes a test event to the recorder.
68    ///
69    /// Events that should not be recorded (informational/interactive) are
70    /// silently skipped.
71    pub fn write_event(&self, event: TestEvent<'_>) {
72        let Some(summary) = TestEventSummary::from_test_event(event) else {
73            // Non-recordable event, skip it.
74            return;
75        };
76        let event = RecordEvent::TestEvent(summary);
77        // Ignore send errors because they indicate that the receiver has exited
78        // (likely due to an error, which is dealt with in finish()).
79        _ = self.sender.send(event);
80    }
81
82    /// Finishes writing and waits for the recorder thread to exit.
83    ///
84    /// Returns the sizes of the recording (compressed and uncompressed), or an error if recording
85    /// failed.
86    ///
87    /// This must be called before the reporter is dropped.
88    pub fn finish(self) -> Result<StoreSizes, RecordReporterError> {
89        // Drop the sender, which signals the receiver to exit.
90        std::mem::drop(self.sender);
91
92        // Wait for the thread to finish writing and exit.
93        match self.handle.join() {
94            Ok(result) => result,
95            Err(panic_payload) => Err(RecordReporterError::WriterPanic {
96                message: panic_payload_to_string(panic_payload),
97            }),
98        }
99    }
100}
101
102/// Extracts a string message from a panic payload.
103fn panic_payload_to_string(payload: Box<dyn Any + Send + 'static>) -> String {
104    if let Some(s) = payload.downcast_ref::<&str>() {
105        (*s).to_owned()
106    } else if let Some(s) = payload.downcast_ref::<String>() {
107        s.clone()
108    } else {
109        "(unknown panic payload)".to_owned()
110    }
111}
112
113/// Internal writer that runs in the recording thread.
114struct RecordReporterWriter {
115    run_recorder: RunRecorder,
116}
117
118impl RecordReporterWriter {
119    fn handle_event(&mut self, event: RecordEvent) -> Result<(), RecordReporterError> {
120        match event {
121            RecordEvent::Meta {
122                cargo_metadata_json,
123                test_list,
124                opts,
125            } => self
126                .run_recorder
127                .write_meta(&cargo_metadata_json, &test_list, &opts)
128                .map_err(RecordReporterError::RunStore),
129            RecordEvent::TestEvent(event) => self
130                .run_recorder
131                .write_event(event)
132                .map_err(RecordReporterError::RunStore),
133        }
134    }
135
136    fn finish(self) -> Result<StoreSizes, RecordReporterError> {
137        self.run_recorder
138            .finish()
139            .map_err(RecordReporterError::RunStore)
140    }
141}
142
143/// Events sent to the recording thread.
144#[derive(Debug)]
145enum RecordEvent {
146    /// Metadata about the test run.
147    Meta {
148        cargo_metadata_json: Arc<String>,
149        test_list: TestListSummary,
150        opts: RecordOpts,
151    },
152    /// A test event.
153    TestEvent(TestEventSummary<LiveSpec>),
154}