Skip to main content

test_r_core/
ipc.rs

1use crate::internal::{CapturedOutput, FailureCause, TestResult};
2use crate::stats::Summary;
3use desert_rust::BinaryCodec;
4use interprocess::local_socket::{
5    GenericFilePath, GenericNamespaced, Name, NameType, ToFsName, ToNsName,
6};
7use std::io::{self, Read, Write};
8use std::time::Duration;
9
10/// Length-prefix width used to frame all IPC messages. A `u32` allows payloads
11/// up to 4 GiB which comfortably covers Cloneable payloads such as
12/// precompiled wasm components.
13pub const FRAME_LEN_BYTES: usize = 4;
14
15/// Writes a length-prefixed frame to the writer. The length is encoded as a
16/// little-endian `u32`.
17pub fn write_frame<W: Write>(writer: &mut W, payload: &[u8]) -> io::Result<()> {
18    let len = u32::try_from(payload.len()).map_err(|_| {
19        io::Error::new(
20            io::ErrorKind::InvalidData,
21            "IPC payload size exceeds u32::MAX",
22        )
23    })?;
24    writer.write_all(&len.to_le_bytes())?;
25    writer.write_all(payload)
26}
27
28/// Reads a length-prefixed frame produced by [`write_frame`].
29pub fn read_frame<R: Read>(reader: &mut R) -> io::Result<Vec<u8>> {
30    let mut len_bytes = [0u8; FRAME_LEN_BYTES];
31    reader.read_exact(&mut len_bytes)?;
32    let len = u32::from_le_bytes(len_bytes) as usize;
33    let mut payload = vec![0; len];
34    reader.read_exact(&mut payload)?;
35    Ok(payload)
36}
37
38#[cfg(feature = "tokio")]
39pub async fn write_frame_async<W>(writer: &mut W, payload: &[u8]) -> io::Result<()>
40where
41    W: tokio::io::AsyncWriteExt + Unpin,
42{
43    let len = u32::try_from(payload.len()).map_err(|_| {
44        io::Error::new(
45            io::ErrorKind::InvalidData,
46            "IPC payload size exceeds u32::MAX",
47        )
48    })?;
49    writer.write_all(&len.to_le_bytes()).await?;
50    writer.write_all(payload).await
51}
52
53#[cfg(feature = "tokio")]
54pub async fn read_frame_async<R>(reader: &mut R) -> io::Result<Vec<u8>>
55where
56    R: tokio::io::AsyncReadExt + Unpin,
57{
58    let mut len_bytes = [0u8; FRAME_LEN_BYTES];
59    reader.read_exact(&mut len_bytes).await?;
60    let len = u32::from_le_bytes(len_bytes) as usize;
61    let mut payload = vec![0; len];
62    reader.read_exact(&mut payload).await?;
63    Ok(payload)
64}
65
66/// Commands sent from the primary test runner to the spawned worker processes.
67#[derive(Debug, BinaryCodec)]
68pub enum IpcCommand {
69    RunTest {
70        name: String,
71        crate_name: String,
72        module_path: String,
73    },
74    /// Provide the wire bytes for a `Cloneable` dependency to the worker.
75    /// Sent before any test that requires the dep. The `dep_id` is the
76    /// dep's fully-qualified id (`{crate}::{module}::{name}`) so that
77    /// same-named deps registered in different modules don't collide.
78    /// Workers cache the bytes and pass them to the worker reconstructor
79    /// when materializing.
80    ProvideCloneable { dep_id: String, wire_bytes: Vec<u8> },
81    /// Provide the descriptor bytes for a `Hosted` dependency to a worker.
82    /// Same shape as [`Self::ProvideCloneable`]; on the worker side the
83    /// bytes are fed to `HostedDep::from_descriptor` (via the registered
84    /// worker reconstructor) instead of being treated as the dep value
85    /// directly. The `dep_id` is the dep's fully-qualified id.
86    ProvideHostedDescriptor { dep_id: String, wire_bytes: Vec<u8> },
87    /// Phase 1C: parent's response to a worker-initiated
88    /// [`IpcResponse::HostedRpcCall`]. Carries the same `request_id`
89    /// echoed back so the worker's stub can match the reply to the
90    /// outstanding in-flight call. `body` is `Ok(result_bytes)` if the
91    /// owner-side dispatcher succeeded, or `Err(message)` if it failed
92    /// (owner panic, unknown method, codec error, …).
93    HostedRpcReply {
94        request_id: u64,
95        body: HostedRpcReplyBody,
96    },
97    /// Tells an IPC worker that its owning parent scheduler thread will not
98    /// send any more commands. Worker lifetime is parent-controlled rather
99    /// than inferred from the worker's private execution plan, whose
100    /// `remaining_count` can reach zero before the parent is done dispatching.
101    Shutdown,
102}
103
104/// Returns whether a test loop should stop based on its execution plan.
105///
106/// An in-process runner owns its scheduler and stops when that scheduler is
107/// done. An IPC worker must ignore its private scheduler's completion state:
108/// only an explicit [`IpcCommand::Shutdown`] from its owning parent proves
109/// that no future [`IpcCommand::RunTest`] will arrive. Conversely, a parent
110/// scheduler thread must retire when its owned worker reports that its private
111/// plan is exhausted, even if the shared parent plan still has tests for other
112/// workers.
113pub(crate) fn test_loop_should_exit(
114    is_ipc_worker: bool,
115    execution_done: bool,
116    owned_worker_exhausted: bool,
117) -> bool {
118    owned_worker_exhausted || (!is_ipc_worker && execution_done)
119}
120
121/// Body of a [`IpcCommand::HostedRpcReply`]. Either the serialized return
122/// value of the owner's method, or a human-readable error describing why
123/// dispatch failed.
124#[derive(Debug, BinaryCodec)]
125pub enum HostedRpcReplyBody {
126    Ok { result_bytes: Vec<u8> },
127    Err { message: String },
128}
129
130#[derive(Debug, BinaryCodec)]
131pub enum SerializableTestResult {
132    Passed {
133        exec_time: Duration,
134    },
135    Benchmarked {
136        exec_time: Duration,
137        ns_iter_summ: Summary,
138        mb_s: usize,
139    },
140    Failed {
141        exec_time: Duration,
142        rendered_failure_cause: String,
143    },
144    Ignored,
145}
146
147impl SerializableTestResult {
148    pub fn into_test_result(
149        self,
150        stdout: Vec<CapturedOutput>,
151        stderr: Vec<CapturedOutput>,
152    ) -> TestResult {
153        let mut captured = [stdout, stderr].concat();
154        captured.sort();
155
156        let mut result: TestResult = self.into();
157        result.set_captured_output(captured);
158        result
159    }
160}
161
162impl From<&TestResult> for SerializableTestResult {
163    fn from(result: &TestResult) -> Self {
164        match &result {
165            TestResult::Passed { exec_time, .. } => SerializableTestResult::Passed {
166                exec_time: *exec_time,
167            },
168            TestResult::Benchmarked {
169                exec_time,
170                ns_iter_summ,
171                mb_s,
172                ..
173            } => SerializableTestResult::Benchmarked {
174                exec_time: *exec_time,
175                ns_iter_summ: *ns_iter_summ,
176                mb_s: *mb_s,
177            },
178            TestResult::Failed {
179                exec_time, cause, ..
180            } => SerializableTestResult::Failed {
181                exec_time: *exec_time,
182                rendered_failure_cause: cause.render(),
183            },
184            TestResult::Ignored { .. } => SerializableTestResult::Ignored,
185        }
186    }
187}
188
189impl From<SerializableTestResult> for TestResult {
190    fn from(result: SerializableTestResult) -> Self {
191        match result {
192            SerializableTestResult::Passed { exec_time } => TestResult::passed(exec_time),
193            SerializableTestResult::Failed {
194                exec_time,
195                rendered_failure_cause,
196            } => TestResult::failed(
197                exec_time,
198                FailureCause::HarnessError(rendered_failure_cause),
199            ),
200            SerializableTestResult::Ignored => TestResult::ignored(),
201            SerializableTestResult::Benchmarked {
202                exec_time,
203                ns_iter_summ,
204                mb_s,
205            } => TestResult::benchmarked(exec_time, ns_iter_summ, mb_s),
206        }
207    }
208}
209
210/// Responses sent from the spawned worker processes to the primary test
211/// runner.
212#[derive(Debug, BinaryCodec)]
213pub enum IpcResponse {
214    TestFinished {
215        result: SerializableTestResult,
216        finish_marker: String,
217        /// Whether the worker's private execution plan was exhausted while
218        /// locating this test. The owning parent scheduler thread must retire
219        /// after this response because the worker cannot accept another
220        /// `RunTest`, but the worker process remains alive until `Shutdown`.
221        worker_exhausted: bool,
222    },
223    /// Acknowledges a [`IpcCommand::ProvideCloneable`]. Echoes back the
224    /// fully-qualified `dep_id` the command carried.
225    CloneableAccepted { dep_id: String },
226    /// Acknowledges a [`IpcCommand::ProvideHostedDescriptor`]. Echoes back
227    /// the fully-qualified `dep_id`.
228    HostedDescriptorAccepted { dep_id: String },
229    /// Phase 1C: worker-initiated remote procedure call against a
230    /// `HostedRpc` dep owned by the parent. The worker's stub assigns a
231    /// monotonically-increasing `request_id`, serializes its method
232    /// arguments into `args_bytes`, and writes this frame on the shared
233    /// IPC stream. The parent's `Worker::run_test` loop dispatches the
234    /// call to the right owner via `dep_id`, and responds with a matching
235    /// [`IpcCommand::HostedRpcReply`].
236    HostedRpcCall {
237        request_id: u64,
238        dep_id: String,
239        method_idx: u32,
240        args_bytes: Vec<u8>,
241    },
242}
243
244pub fn ipc_name<'s>(name: String) -> Name<'s> {
245    if GenericNamespaced::is_supported() {
246        name.to_ns_name::<GenericNamespaced>()
247            .expect("Invalid local socket name")
248    } else {
249        name.to_fs_name::<GenericFilePath>()
250            .expect("Invalid local socket name")
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use std::io::Cursor;
258
259    #[test]
260    fn write_then_read_round_trip_empty() {
261        let mut buf: Vec<u8> = Vec::new();
262        write_frame(&mut buf, &[]).expect("write");
263        let mut cursor = Cursor::new(&buf);
264        let payload = read_frame(&mut cursor).expect("read");
265        assert!(payload.is_empty());
266    }
267
268    #[test]
269    fn write_then_read_round_trip_small() {
270        let mut buf: Vec<u8> = Vec::new();
271        let data = b"hello, world";
272        write_frame(&mut buf, data).expect("write");
273        let mut cursor = Cursor::new(&buf);
274        let payload = read_frame(&mut cursor).expect("read");
275        assert_eq!(payload, data);
276    }
277
278    #[test]
279    fn write_then_read_round_trip_large_payload_exceeds_u16() {
280        // 200 KiB — larger than the old u16 length prefix could express.
281        let mut data = vec![0u8; 200 * 1024];
282        for (i, b) in data.iter_mut().enumerate() {
283            *b = (i % 251) as u8;
284        }
285        let mut buf: Vec<u8> = Vec::new();
286        write_frame(&mut buf, &data).expect("write");
287        assert_eq!(buf.len(), FRAME_LEN_BYTES + data.len());
288        let mut cursor = Cursor::new(&buf);
289        let payload = read_frame(&mut cursor).expect("read");
290        assert_eq!(payload.len(), data.len());
291        assert_eq!(payload, data);
292    }
293
294    #[test]
295    fn read_frame_propagates_eof() {
296        let buf: Vec<u8> = Vec::new();
297        let mut cursor = Cursor::new(&buf);
298        let err = read_frame(&mut cursor).expect_err("must fail on empty");
299        assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
300    }
301
302    #[test]
303    fn worker_lifetime_and_parent_scheduler_capacity_are_distinct() {
304        assert!(test_loop_should_exit(false, true, false));
305        assert!(!test_loop_should_exit(true, true, false));
306        assert!(!test_loop_should_exit(true, false, false));
307        assert!(test_loop_should_exit(false, false, true));
308    }
309
310    #[cfg(feature = "tokio")]
311    #[test]
312    fn async_round_trip_large_payload_exceeds_u16() {
313        let runtime = tokio::runtime::Runtime::new().unwrap();
314        runtime.block_on(async {
315            let mut data = vec![0u8; 200 * 1024];
316            for (i, b) in data.iter_mut().enumerate() {
317                *b = (i % 251) as u8;
318            }
319            let mut buf: Vec<u8> = Vec::new();
320            write_frame_async(&mut buf, &data).await.expect("write");
321            assert_eq!(buf.len(), FRAME_LEN_BYTES + data.len());
322            let mut slice: &[u8] = &buf;
323            let payload = read_frame_async(&mut slice).await.expect("read");
324            assert_eq!(payload, data);
325        });
326    }
327}