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    record::{RecordOpts, RunRecorder, StoreSizes, TestEventSummary},
9    reporter::events::TestEvent,
10    test_output::ChildSingleOutput,
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<'a> {
26    // Invariant: sender is always Some while the reporter is alive.
27    sender: Option<mpsc::SyncSender<RecordEvent>>,
28    handle: JoinHandle<Result<StoreSizes, RecordReporterError>>,
29    _marker: std::marker::PhantomData<&'a ()>,
30}
31
32impl<'a> RecordReporter<'a> {
33    /// Creates a new `RecordReporter` with the given recorder.
34    pub fn new(run_recorder: RunRecorder) -> Self {
35        // Spawn a thread to do the writing. Use a bounded channel with backpressure.
36        let (sender, receiver) = mpsc::sync_channel(128);
37        let handle = std::thread::spawn(move || {
38            let mut writer = RecordReporterWriter { run_recorder };
39            while let Ok(event) = receiver.recv() {
40                writer.handle_event(event)?;
41            }
42
43            // The sender has been dropped. Finish writing and exit.
44            writer.finish()
45        });
46
47        Self {
48            sender: Some(sender),
49            handle,
50            _marker: std::marker::PhantomData,
51        }
52    }
53
54    /// Writes metadata to the recorder.
55    ///
56    /// This should be called once at the beginning of a test run.
57    pub fn write_meta(
58        &self,
59        cargo_metadata_json: Arc<String>,
60        test_list: TestListSummary,
61        opts: RecordOpts,
62    ) {
63        let event = RecordEvent::Meta {
64            cargo_metadata_json,
65            test_list,
66            opts,
67        };
68        // Ignore send errors because they indicate that the receiver has exited
69        // (likely due to an error, which is dealt with in finish()).
70        _ = self
71            .sender
72            .as_ref()
73            .expect("sender is always Some")
74            .send(event);
75    }
76
77    /// Writes a test event to the recorder.
78    ///
79    /// Events that should not be recorded (informational/interactive) are
80    /// silently skipped.
81    pub fn write_event(&self, event: TestEvent<'_>) {
82        let Some(summary) = TestEventSummary::from_test_event(event) else {
83            // Non-recordable event, skip it.
84            return;
85        };
86        let event = RecordEvent::TestEvent(summary);
87        // Ignore send errors because they indicate that the receiver has exited
88        // (likely due to an error, which is dealt with in finish()).
89        _ = self
90            .sender
91            .as_ref()
92            .expect("sender is always Some")
93            .send(event);
94    }
95
96    /// Finishes writing and waits for the recorder thread to exit.
97    ///
98    /// Returns the sizes of the recording (compressed and uncompressed), or an error if recording
99    /// failed.
100    ///
101    /// This must be called before the reporter is dropped.
102    pub fn finish(mut self) -> Result<StoreSizes, RecordReporterError> {
103        // Drop the sender, which signals the receiver to exit.
104        let sender = self.sender.take();
105        std::mem::drop(sender);
106
107        // Wait for the thread to finish writing and exit.
108        match self.handle.join() {
109            Ok(result) => result,
110            Err(panic_payload) => Err(RecordReporterError::WriterPanic {
111                message: panic_payload_to_string(panic_payload),
112            }),
113        }
114    }
115}
116
117/// Extracts a string message from a panic payload.
118fn panic_payload_to_string(payload: Box<dyn Any + Send + 'static>) -> String {
119    if let Some(s) = payload.downcast_ref::<&str>() {
120        (*s).to_owned()
121    } else if let Some(s) = payload.downcast_ref::<String>() {
122        s.clone()
123    } else {
124        "(unknown panic payload)".to_owned()
125    }
126}
127
128/// Internal writer that runs in the recording thread.
129struct RecordReporterWriter {
130    run_recorder: RunRecorder,
131}
132
133impl RecordReporterWriter {
134    fn handle_event(&mut self, event: RecordEvent) -> Result<(), RecordReporterError> {
135        match event {
136            RecordEvent::Meta {
137                cargo_metadata_json,
138                test_list,
139                opts,
140            } => self
141                .run_recorder
142                .write_meta(&cargo_metadata_json, &test_list, &opts)
143                .map_err(RecordReporterError::RunStore),
144            RecordEvent::TestEvent(event) => self
145                .run_recorder
146                .write_event(event)
147                .map_err(RecordReporterError::RunStore),
148        }
149    }
150
151    fn finish(self) -> Result<StoreSizes, RecordReporterError> {
152        self.run_recorder
153            .finish()
154            .map_err(RecordReporterError::RunStore)
155    }
156}
157
158/// Events sent to the recording thread.
159#[derive(Debug)]
160enum RecordEvent {
161    /// Metadata about the test run.
162    Meta {
163        cargo_metadata_json: Arc<String>,
164        test_list: TestListSummary,
165        opts: RecordOpts,
166    },
167    /// A test event.
168    TestEvent(TestEventSummary<ChildSingleOutput>),
169}