Skip to main content

test_r_core/
tokio.rs

1use crate::args::{Arguments, TimeThreshold};
2use crate::bench::AsyncBencher;
3use crate::execution::{DepWireBytes, TestExecution, TestSuiteExecution};
4use crate::internal;
5use crate::internal::{
6    generate_tests, get_ensure_time, CapturedOutput, CloneableCodec, FailureCause,
7    FlakinessControl, HostedRpcChannel, HostedRpcError, HostedRpcOwnerCell, HostedRpcTransport,
8    InProcessHostedRpcTransport, RegisteredTest, RpcFactory, SuiteResult, TestFunction, TestResult,
9    WorkerReconstructor,
10};
11use crate::ipc::{
12    ipc_name, read_frame_async, write_frame_async, HostedRpcReplyBody, IpcCommand, IpcResponse,
13};
14use crate::output::{test_runner_output, TestRunnerOutput};
15use desert_rust::{deserialize, serialize_to_byte_vec};
16use futures::FutureExt;
17use interprocess::local_socket::tokio::prelude::*;
18use interprocess::local_socket::tokio::{Listener, Stream};
19use interprocess::local_socket::{GenericNamespaced, ListenerOptions};
20use std::any::Any;
21use std::collections::HashMap;
22use std::collections::VecDeque;
23use std::future::Future;
24use std::panic::AssertUnwindSafe;
25use std::pin::Pin;
26use std::process::{ExitCode, Stdio};
27use std::sync::atomic::{AtomicU64, Ordering};
28use std::sync::Arc;
29use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
30use tokio::process::{Child, Command};
31use tokio::spawn;
32use tokio::sync::Mutex;
33use tokio::task::{spawn_blocking, JoinHandle, JoinSet};
34use tokio::time::Instant;
35use uuid::Uuid;
36
37pub fn test_runner() -> ExitCode {
38    tokio::runtime::Builder::new_multi_thread()
39        .enable_all()
40        .build()
41        .unwrap()
42        .block_on(async_test_runner())
43}
44
45#[allow(clippy::await_holding_lock)]
46async fn async_test_runner() -> ExitCode {
47    crate::panic_hook::install_panic_hook();
48    let mut args = Arguments::from_args();
49    // When the parent spawned this process as a worker it passed
50    // `--worker-index <N>`. Stash it so `crate::worker::worker_index()`
51    // returns the correct value for PerWorker dep constructors.
52    if let Some(idx) = args.worker_index {
53        crate::worker::set_worker_index(idx);
54    }
55    // Host-side output capture is installed PER retry attempt below
56    // (after `finalize_for_execution`), mirroring the sync runner.
57    // See `crate::host_capture` for the pipeline.
58    let output = test_runner_output(&args);
59
60    let registered_tests = internal::REGISTERED_TESTS.lock().unwrap();
61    let registered_dependency_constructors =
62        internal::REGISTERED_DEPENDENCY_CONSTRUCTORS.lock().unwrap();
63    let registered_testsuite_props = internal::REGISTERED_TESTSUITE_PROPS.lock().unwrap();
64    let registered_test_generators = internal::REGISTERED_TEST_GENERATORS.lock().unwrap();
65
66    let generated_tests = generate_tests(&registered_test_generators).await;
67
68    let all_tests: Vec<RegisteredTest> = registered_tests
69        .iter()
70        .cloned()
71        .chain(generated_tests)
72        .collect();
73
74    if args.list {
75        // Apply suite properties (including runtime matrix-suite multiplication)
76        // before listing, so matrix-multiplied cases appear in `--list` output
77        // with their `<test>_<case>` names and `:tag:`-selectable auto-tags.
78        let tests_with_props =
79            internal::apply_suite_props_to_tests(&all_tests, &registered_testsuite_props);
80        output.test_list(&tests_with_props);
81        ExitCode::SUCCESS
82    } else {
83        let mut remaining_retries = args.flaky_run.unwrap_or(1);
84
85        let mut exit_code = ExitCode::from(101);
86        while remaining_retries > 0 {
87            let (mut execution, filtered_tests) = TestSuiteExecution::construct(
88                &args,
89                registered_dependency_constructors.as_slice(),
90                &all_tests,
91                registered_testsuite_props.as_slice(),
92            );
93            args.finalize_for_execution(&execution, output.clone());
94            // Install host capture for this attempt, after
95            // `finalize_for_execution` has decided whether workers
96            // will spawn. See `sync::test_runner` for the rationale.
97            let mut host_capture = crate::host_capture::install_if_needed(&args);
98            let host_capture_epoch: Option<std::time::Instant> =
99                host_capture.as_ref().map(|hc| hc.epoch());
100            let host_capture_epoch_wall: Option<std::time::SystemTime> =
101                host_capture.as_ref().map(|hc| hc.epoch_wall());
102            let is_top_level_parent = args.is_top_level_parent();
103            let has_selected_tests = execution.remaining() > 0;
104            // Parent-side collection for dependency scopes whose worker-side
105            // value is shipped as bytes or represented as an RPC stub. Async
106            // constructors are awaited here, before workers receive their
107            // reconstructed values. Skip it for empty filtered runs.
108            let needs_parent_shared = execution.has_cloneable_dependencies()
109                || execution.has_hosted_dependencies()
110                || execution.has_hosted_rpc_dependencies();
111            let parent_shared = if is_top_level_parent && has_selected_tests && needs_parent_shared
112            {
113                execution.collect_parent_shared_dependencies_async().await
114            } else {
115                crate::execution::ParentSharedDependencies {
116                    cloneable_wire_bytes: Vec::new(),
117                    cloneable_local_values: Vec::new(),
118                    hosted_descriptor_bytes: Vec::new(),
119                    hosted_owners: Vec::new(),
120                    hosted_rpc_owner_cells: Vec::new(),
121                    parent_constructed_shared_values: Vec::new(),
122                }
123            };
124            let cloneable_wire_bytes = parent_shared.cloneable_wire_bytes;
125            let cloneable_local_values = parent_shared.cloneable_local_values;
126            let hosted_descriptor_bytes = parent_shared.hosted_descriptor_bytes;
127            let _hosted_owners = parent_shared.hosted_owners;
128            let hosted_rpc_owner_cells: HashMap<String, Arc<HostedRpcOwnerCell>> =
129                parent_shared.hosted_rpc_owner_cells.into_iter().collect();
130            let parent_constructed_shared_values = parent_shared.parent_constructed_shared_values;
131            // Pre-built RpcFactory lookup keyed by qualified id, so worker
132            // subprocesses can build stubs without re-locking the global
133            // REGISTERED_DEPENDENCY_CONSTRUCTORS.
134            let rpc_factories: HashMap<String, RpcFactory> = registered_dependency_constructors
135                .iter()
136                .filter_map(|d| {
137                    if d.scope == crate::internal::DepScope::HostedRpc {
138                        d.rpc_factory
139                            .as_ref()
140                            .map(|f| (d.qualified_id(), f.clone()))
141                    } else {
142                        None
143                    }
144                })
145                .collect();
146            // Build a combined Cloneable + Hosted codec/worker lookup table
147            // now, before `test_thread` workers are spawned (see sync.rs
148            // for rationale). Keyed by the dep's fully-qualified id
149            // (`{crate}::{module}::{name}`) so workers can route an incoming
150            // `ProvideCloneable` / `ProvideHostedDescriptor` to the correct
151            // dep even when two deps share a local `name` in different
152            // modules.
153            let cloneable_codecs: HashMap<String, (CloneableCodec, WorkerReconstructor)> =
154                registered_dependency_constructors
155                    .iter()
156                    .filter_map(|d| {
157                        let codec_opt = match d.scope {
158                            crate::internal::DepScope::Cloneable => d.cloneable_codec.as_ref(),
159                            crate::internal::DepScope::Hosted => d.hosted_codec.as_ref(),
160                            _ => None,
161                        };
162                        match (codec_opt, &d.worker_fn) {
163                            (Some(codec), Some(worker_fn)) => {
164                                Some((d.qualified_id(), (codec.clone(), worker_fn.clone())))
165                            }
166                            _ => None,
167                        }
168                    })
169                    .collect();
170            // Mode-consistent Cloneable semantics for the no-spawn-workers
171            // path (e.g. `--nocapture`): reuse the parent-constructed value
172            // directly instead of re-running the user constructor in
173            // `materialize_deps`. Without this, a Cloneable dep's
174            // constructor would run twice (once for parent-side
175            // `collect_parent_shared_dependencies_async`, once for the
176            // in-process test execution), which both violates the
177            // "constructor runs once" expectation and can deadlock when
178            // the constructor takes a runtime-wide lock.
179            if is_top_level_parent && !args.spawn_workers && !cloneable_local_values.is_empty() {
180                apply_cloneable_values_locally(&mut execution, &cloneable_local_values);
181            }
182            // Mode-consistent Hosted semantics: when this is the top-level
183            // parent AND we do NOT spawn workers (e.g. --nocapture), the
184            // test functions run in this same process, but they must still
185            // see the *worker-side handle* produced by
186            // `HostedDep::from_descriptor`. Reconstruct each handle locally
187            // via the descriptor round-trip and pre-populate the execution
188            // tree.
189            if is_top_level_parent && !args.spawn_workers && !hosted_descriptor_bytes.is_empty() {
190                apply_hosted_descriptors_locally(
191                    &mut execution,
192                    &cloneable_codecs,
193                    &hosted_descriptor_bytes,
194                )
195                .await;
196            }
197            // Mode-consistent HostedRpc semantics for the no-spawn-workers
198            // path: install in-process stubs that route straight to the
199            // parent-held owner cells, so tests see the same `Stub` value
200            // whether or not the runner spawns workers.
201            if is_top_level_parent && !args.spawn_workers && !hosted_rpc_owner_cells.is_empty() {
202                install_local_hosted_rpc_stubs(
203                    &mut execution,
204                    &rpc_factories,
205                    &hosted_rpc_owner_cells,
206                );
207            }
208            // Mirror of `sync::apply_parent_constructed_shared_values_locally`:
209            // in no-spawn-workers mode, install any `Shared`/`PerWorker` dep
210            // values the parent had to construct as transitive inputs to a
211            // Cloneable/Hosted/HostedRpc dep. The in-process test thread's
212            // `materialize_deps` then reuses them instead of re-running the
213            // constructor in the same process.
214            if is_top_level_parent
215                && !args.spawn_workers
216                && !parent_constructed_shared_values.is_empty()
217            {
218                apply_parent_constructed_shared_values_locally(
219                    &mut execution,
220                    &parent_constructed_shared_values,
221                );
222            }
223            if args.spawn_workers {
224                execution.skip_creating_dependencies();
225            }
226
227            // println!("Execution plan: {execution:?}");
228            // println!("Final args: {args:?}");
229            // println!("Has dependencies: {:?}", execution.has_dependencies());
230
231            let count = execution.remaining();
232            let results = Arc::new(Mutex::new(Vec::with_capacity(count)));
233            // Parent-side per-test execution windows aligned 1:1 with
234            // `results`. The host-capture finaliser uses them after all
235            // test_threads finish to attribute spilled host-log records
236            // to the test(s) whose window contains each record.
237            let host_windows: Arc<Mutex<Vec<crate::host_capture::HostWindow>>> =
238                Arc::new(Mutex::new(Vec::with_capacity(count)));
239
240            let start = Instant::now();
241            output.start_suite(&filtered_tests);
242
243            let execution = Arc::new(Mutex::new(execution));
244            let cloneable_wire_bytes = Arc::new(cloneable_wire_bytes);
245            let hosted_descriptor_bytes = Arc::new(hosted_descriptor_bytes);
246            let cloneable_codecs = Arc::new(cloneable_codecs);
247            let rpc_factories = Arc::new(rpc_factories);
248            let hosted_rpc_owner_cells = Arc::new(hosted_rpc_owner_cells);
249            let mut join_set = JoinSet::new();
250            let threads = args.test_threads().get();
251
252            for worker_idx in 0..threads {
253                let execution_clone = execution.clone();
254                let output_clone = output.clone();
255                // Stamp each test-thread's args with the worker index it will
256                // hand to its spawned child via `--worker-index <N>`.
257                let mut args_clone = args.clone();
258                if args_clone.spawn_workers {
259                    args_clone.worker_index = Some(worker_idx);
260                }
261                let results_clone = results.clone();
262                let host_windows_clone = host_windows.clone();
263                let wire_bytes_clone = cloneable_wire_bytes.clone();
264                let hosted_bytes_clone = hosted_descriptor_bytes.clone();
265                let codecs_clone = cloneable_codecs.clone();
266                let rpc_factories_clone = rpc_factories.clone();
267                let hosted_rpc_owner_cells_clone = hosted_rpc_owner_cells.clone();
268                let handle = tokio::runtime::Handle::current();
269                join_set.spawn_blocking(move || {
270                    handle.block_on(test_thread(
271                        args_clone,
272                        execution_clone,
273                        output_clone,
274                        count,
275                        results_clone,
276                        host_windows_clone,
277                        wire_bytes_clone,
278                        hosted_bytes_clone,
279                        codecs_clone,
280                        rpc_factories_clone,
281                        hosted_rpc_owner_cells_clone,
282                        host_capture_epoch,
283                    ))
284                });
285            }
286
287            while let Some(res) = join_set.join_next().await {
288                res.expect("Failed to join task");
289            }
290
291            drop(execution);
292
293            let mut results = results.lock().await;
294            if is_top_level_parent {
295                assert_eq!(
296                    results.len(),
297                    count,
298                    "IPC worker exhaustion stranded tests in the parent execution plan"
299                );
300            }
301            // Drop parent-owned hosted / hosted-rpc owners BEFORE
302            // finalising host capture so any `Drop` impls that
303            // shutdown background threads / subprocesses get a chance
304            // to emit their final lines through the still-active
305            // capture pipe. If we restored fd 1/2 first, those late
306            // lines would either land on the about-to-render
307            // structured output or be swallowed entirely.
308            drop(hosted_rpc_owner_cells);
309            drop(_hosted_owners);
310
311            // Finalise host capture (if any) BEFORE rendering the
312            // suite so the attributed host-log records make it into
313            // the per-test captured-output vecs the formatter walks.
314            if let Some(hc) = host_capture.take() {
315                let epoch_wall = host_capture_epoch_wall.unwrap_or_else(|| hc.epoch_wall());
316                let records = hc.finalize();
317                let windows = host_windows.lock().await;
318                let windows_indexed: Vec<(usize, crate::host_capture::HostWindow)> =
319                    windows.iter().copied().enumerate().collect();
320                crate::host_capture::attribute_records_to_tests(
321                    epoch_wall,
322                    &records,
323                    &windows_indexed,
324                    &mut results,
325                );
326            }
327            output.finished_suite(&all_tests, &results, start.elapsed());
328            exit_code = SuiteResult::exit_code(&results);
329
330            // IPC workers execute exactly one parent-controlled dispatch
331            // session. Suite-level retries belong to the top-level parent;
332            // after Shutdown this subprocess must exit instead of reconnecting
333            // to the same listener for an independent retry attempt.
334            if !is_top_level_parent || exit_code == ExitCode::SUCCESS {
335                break;
336            } else {
337                remaining_retries -= 1;
338            }
339        }
340        exit_code
341    }
342}
343
344#[allow(clippy::too_many_arguments)]
345async fn test_thread(
346    args: Arguments,
347    execution: Arc<Mutex<TestSuiteExecution>>,
348    output: Arc<dyn TestRunnerOutput>,
349    count: usize,
350    results: Arc<Mutex<Vec<(RegisteredTest, TestResult)>>>,
351    host_windows: Arc<Mutex<Vec<crate::host_capture::HostWindow>>>,
352    cloneable_wire_bytes: Arc<Vec<DepWireBytes>>,
353    hosted_descriptor_bytes: Arc<Vec<DepWireBytes>>,
354    cloneable_codecs: Arc<HashMap<String, (CloneableCodec, WorkerReconstructor)>>,
355    rpc_factories: Arc<HashMap<String, RpcFactory>>,
356    hosted_rpc_owner_cells: Arc<HashMap<String, Arc<HostedRpcOwnerCell>>>,
357    host_capture_epoch: Option<std::time::Instant>,
358) {
359    let mut worker = spawn_worker_if_needed(&args).await;
360    // Parent dispatches incoming `HostedRpcCall` frames against the owner
361    // cells materialised in the top-level parent. Workers don't need the owner
362    // cells (they own stubs instead), so they receive an empty map and the
363    // dispatch code path is never reached in subprocesses.
364    if let Some(worker) = worker.as_mut() {
365        worker.set_hosted_rpc_owner_cells(hosted_rpc_owner_cells.clone());
366    }
367    let connection_arc = if let Some(ref name) = args.ipc {
368        let name = ipc_name(name.clone());
369        let stream = Stream::connect(name)
370            .await
371            .expect("Failed to connect to IPC socket");
372        Some(Arc::new(Mutex::new(stream)))
373    } else {
374        None
375    };
376
377    if let Some(worker) = worker.as_mut() {
378        for (dep_id, wire_bytes) in cloneable_wire_bytes.iter() {
379            worker
380                .provide_cloneable(dep_id.clone(), wire_bytes.clone())
381                .await;
382        }
383        // Ship every Hosted dep's descriptor bytes too.
384        for (dep_id, descriptor_bytes) in hosted_descriptor_bytes.iter() {
385            worker
386                .provide_hosted_descriptor(dep_id.clone(), descriptor_bytes.clone())
387                .await;
388        }
389    }
390
391    // Worker subprocess side: build a stub for every HostedRpc dep registered
392    // in this binary using the IPC-backed transport sharing the same socket as
393    // the main IPC loop. Install the stubs in the execution tree so dependency
394    // materialisation skips the parent-only owner constructor.
395    if let Some(connection) = connection_arc.as_ref() {
396        if !rpc_factories.is_empty() {
397            install_worker_subprocess_hosted_rpc_stubs(
398                &execution,
399                &rpc_factories,
400                connection.clone(),
401            )
402            .await;
403        }
404    }
405
406    let mut expected_test = None;
407    let mut owned_worker_exhausted = false;
408
409    'test_loop: loop {
410        let is_ipc_worker = connection_arc.is_some();
411        if crate::ipc::test_loop_should_exit(
412            is_ipc_worker,
413            is_done(&execution).await,
414            owned_worker_exhausted,
415        ) {
416            break;
417        }
418
419        if let Some(connection) = connection_arc.as_ref() {
420            while expected_test.is_none() {
421                let mut conn = connection.lock().await;
422                let command_bytes = read_frame_async(&mut *conn)
423                    .await
424                    .expect("Failed to read IPC command frame");
425                drop(conn);
426                let command: IpcCommand =
427                    deserialize(&command_bytes).expect("Failed to decode IPC command");
428
429                match command {
430                    IpcCommand::RunTest {
431                        name,
432                        crate_name,
433                        module_path,
434                    } => {
435                        expected_test = Some((name, crate_name, module_path));
436                    }
437                    IpcCommand::Shutdown => break 'test_loop,
438                    IpcCommand::ProvideCloneable { dep_id, wire_bytes } => {
439                        // Worker-side reconstruction (see sync.rs::apply_provided_wire_bytes).
440                        apply_provided_wire_bytes(
441                            &execution,
442                            &cloneable_codecs,
443                            &dep_id,
444                            &wire_bytes,
445                            "ProvideCloneable",
446                        )
447                        .await;
448                        let response = IpcResponse::CloneableAccepted { dep_id };
449                        let msg = serialize_to_byte_vec(&response)
450                            .expect("Failed to encode IPC response");
451                        let mut conn = connection.lock().await;
452                        write_frame_async(&mut *conn, &msg)
453                            .await
454                            .expect("Failed to write IPC response frame");
455                    }
456                    IpcCommand::ProvideHostedDescriptor { dep_id, wire_bytes } => {
457                        // Worker-side reconstruction: same shape as
458                        // ProvideCloneable but routed through the registered
459                        // HostedDep worker_fn.
460                        apply_provided_wire_bytes(
461                            &execution,
462                            &cloneable_codecs,
463                            &dep_id,
464                            &wire_bytes,
465                            "ProvideHostedDescriptor",
466                        )
467                        .await;
468                        let response = IpcResponse::HostedDescriptorAccepted { dep_id };
469                        let msg = serialize_to_byte_vec(&response)
470                            .expect("Failed to encode IPC response");
471                        let mut conn = connection.lock().await;
472                        write_frame_async(&mut *conn, &msg)
473                            .await
474                            .expect("Failed to write IPC response frame");
475                    }
476                    IpcCommand::HostedRpcReply { .. } => {
477                        // HR1.2: replies for worker-initiated HostedRpc calls
478                        // are consumed inline by the IPC transport during
479                        // test execution, never by this between-tests
480                        // command loop. Receiving one here means the
481                        // protocol got out of sync; surface that loudly
482                        // rather than dropping the frame.
483                        panic!(
484                            "unexpected `HostedRpcReply` while waiting for the next \
485                             between-tests command in the tokio worker subprocess: a \
486                             stub call must have left a reply on the wire without \
487                             draining it inline"
488                        );
489                    }
490                }
491            }
492        }
493
494        if let Some(next) = pick_next(&execution).await {
495            let skip = if let Some((name, crate_name, module_path)) = &expected_test {
496                next.test.name != *name
497                    || next.test.crate_name != *crate_name
498                    || next.test.module_path != *module_path
499            } else {
500                false
501            };
502
503            if !skip {
504                expected_test = None;
505
506                let ensure_time = get_ensure_time(&args, &next.test);
507
508                // Snapshot the parent's monotonic-clock view of the
509                // test start. The matching end-instant is captured
510                // after `finished_running_test`, and the pair becomes
511                // a `HostWindow` for record attribution. Uses
512                // `std::time::Instant` because the host-capture epoch
513                // is `std::time::Instant` (tokio's `Instant` is a
514                // different type without `From` interop).
515                let window_start = std::time::Instant::now();
516
517                output.start_running_test(&next.test, next.index, count);
518                let result = run_test(
519                    output.clone(),
520                    next.index,
521                    count,
522                    args.nocapture,
523                    args.include_ignored,
524                    ensure_time,
525                    next.deps.clone(),
526                    &next.test,
527                    &mut worker,
528                )
529                .await;
530                owned_worker_exhausted = worker
531                    .as_ref()
532                    .map(|worker| worker.exhausted)
533                    .unwrap_or(false);
534                output.finished_running_test(&next.test, next.index, count, &result);
535                let window_end = std::time::Instant::now();
536
537                if let Some(connection) = connection_arc.as_ref() {
538                    let finish_marker = Uuid::new_v4().to_string();
539                    let finish_marker_line = format!("{finish_marker}\n");
540                    let mut stdout = tokio::io::stdout();
541                    stdout
542                        .write_all(finish_marker_line.as_bytes())
543                        .await
544                        .unwrap();
545                    stdout.flush().await.unwrap();
546
547                    let mut stderr = tokio::io::stderr();
548                    stderr
549                        .write_all(finish_marker_line.as_bytes())
550                        .await
551                        .unwrap();
552                    stderr.flush().await.unwrap();
553
554                    let response = IpcResponse::TestFinished {
555                        result: (&result).into(),
556                        finish_marker,
557                        worker_exhausted: is_done(&execution).await,
558                    };
559                    let msg =
560                        serialize_to_byte_vec(&response).expect("Failed to encode IPC response");
561                    let mut conn = connection.lock().await;
562                    write_frame_async(&mut *conn, &msg)
563                        .await
564                        .expect("Failed to write IPC response frame");
565                }
566
567                // Push the result and its window under the same
568                // critical section so the two vecs stay aligned in the
569                // face of concurrent pushes from sibling test_threads.
570                let window = crate::host_capture::HostWindow::from_instants(
571                    host_capture_epoch,
572                    window_start,
573                    window_end,
574                )
575                .unwrap_or(crate::host_capture::HostWindow {
576                    start: std::time::Duration::ZERO,
577                    end: std::time::Duration::ZERO,
578                });
579                let mut results_guard = results.lock().await;
580                let mut windows_guard = host_windows.lock().await;
581                results_guard.push((next.test.clone(), result));
582                windows_guard.push(window);
583            }
584        }
585    }
586
587    if let Some(worker) = worker {
588        worker.shutdown().await;
589    }
590}
591
592async fn is_done(execution: &Arc<Mutex<TestSuiteExecution>>) -> bool {
593    let execution = execution.lock().await;
594    execution.is_done()
595}
596
597/// Async counterpart to `sync::apply_provided_wire_bytes`. Decodes the wire
598/// bytes into a worker-side dependency value (looked up by the dep's
599/// fully-qualified id `{crate}::{module}::{name}`) and stores it in the
600/// execution tree so the next `materialize_deps` call uses the pre-resolved
601/// value.
602///
603/// `source_command` is the textual name of the IPC command that delivered
604/// the bytes (`"ProvideCloneable"` or `"ProvideHostedDescriptor"`); used
605/// only in panic messages.
606async fn apply_provided_wire_bytes(
607    execution: &Arc<Mutex<TestSuiteExecution>>,
608    wire_codecs: &HashMap<String, (CloneableCodec, WorkerReconstructor)>,
609    dep_id: &str,
610    wire_bytes: &[u8],
611    source_command: &str,
612) {
613    let (codec, worker_fn) = wire_codecs.get(dep_id).unwrap_or_else(|| {
614        panic!("{source_command} referenced unknown wire-shipped dep '{dep_id}'")
615    });
616
617    let wire_payload = (codec.from_wire_bytes)(wire_bytes);
618    let empty_deps: Arc<dyn internal::DependencyView + Send + Sync> =
619        Arc::new(HashMap::<String, Arc<dyn Any + Send + Sync>>::new());
620    let reconstructed = match worker_fn {
621        WorkerReconstructor::Sync(f) => f(wire_payload, empty_deps),
622        WorkerReconstructor::Async(f) => f(wire_payload, empty_deps).await,
623    };
624
625    let mut execution = execution.lock().await;
626    let applied = execution.provide_cloneable_value(dep_id, reconstructed);
627    assert!(
628        applied,
629        "{source_command} for dep '{dep_id}' did not match any registered dep in this worker"
630    );
631}
632
633/// Mode-consistent Cloneable semantics for the no-spawn-workers path on
634/// the tokio runner. Mirrors `sync::apply_cloneable_values_locally`: takes
635/// the parent-constructed Cloneable values and installs them directly
636/// into the parent's `TestSuiteExecution`, so `materialize_deps` reuses
637/// them instead of re-running the user constructor.
638///
639/// For `Cloneable`, the documented round-trip
640/// `from_wire(to_wire(value))` is semantics-preserving, so reusing the
641/// parent value directly is equivalent to round-tripping it through the
642/// wire codec while avoiding the duplicate constructor run that would
643/// otherwise occur on the no-spawn-workers code path. The duplicate run
644/// historically caused user-visible problems (extra observable side
645/// effects under `--nocapture`, and deadlocks when the constructor takes
646/// a runtime-wide lock).
647fn apply_cloneable_values_locally(
648    execution: &mut TestSuiteExecution,
649    cloneable_local_values: &[(String, Arc<dyn Any + Send + Sync>)],
650) {
651    for (dep_id, value) in cloneable_local_values {
652        let applied = execution.provide_cloneable_value(dep_id, value.clone());
653        assert!(
654            applied,
655            "Cloneable dep '{dep_id}' could not be pre-populated locally"
656        );
657    }
658}
659
660/// Tokio counterpart to `sync::apply_parent_constructed_shared_values_locally`.
661/// In no-spawn-workers mode, installs `Shared`/`PerWorker` dep values that
662/// the parent had to construct as transitive inputs to a
663/// Cloneable/Hosted/HostedRpc dep, so the in-process test thread reuses
664/// them instead of re-running the constructor in the same process.
665fn apply_parent_constructed_shared_values_locally(
666    execution: &mut TestSuiteExecution,
667    values: &[(String, Arc<dyn Any + Send + Sync>)],
668) {
669    for (dep_id, value) in values {
670        let applied = execution.provide_materialized_shared_value(dep_id, value.clone());
671        assert!(
672            applied,
673            "Shared/PerWorker dep '{dep_id}' could not be pre-populated locally"
674        );
675    }
676}
677
678/// Mode-consistent Hosted semantics for the no-spawn-workers path on the
679/// tokio runner. Mirrors `sync::apply_hosted_descriptors_locally`: takes
680/// the parent-collected descriptor bytes and reconstructs each Hosted
681/// dep's worker-side handle (via the registered codec + worker_fn)
682/// directly in the parent's `TestSuiteExecution`.
683///
684/// Both `WorkerReconstructor::Sync` (`HostedDep::from_descriptor`) and
685/// `WorkerReconstructor::Async` (`AsyncHostedDep::from_descriptor`) are
686/// supported here so that `async_worker` Hosted deps see the same
687/// worker-side handle whether the runner ended up in spawned-worker mode
688/// or in the no-spawn fallback, matching the documented mode-consistent
689/// Hosted contract in `book/src/advanced_features/dependency_sharing.md`.
690async fn apply_hosted_descriptors_locally(
691    execution: &mut TestSuiteExecution,
692    wire_codecs: &HashMap<String, (CloneableCodec, WorkerReconstructor)>,
693    descriptor_bytes: &[DepWireBytes],
694) {
695    for (dep_id, wire_bytes) in descriptor_bytes {
696        let (codec, worker_fn) = wire_codecs.get(dep_id).unwrap_or_else(|| {
697            panic!("Hosted dep '{dep_id}' missing codec/worker_fn for local handle reconstruction")
698        });
699        let wire_payload = (codec.from_wire_bytes)(wire_bytes);
700        let empty_deps: Arc<dyn internal::DependencyView + Send + Sync> =
701            Arc::new(HashMap::<String, Arc<dyn Any + Send + Sync>>::new());
702        let reconstructed = match worker_fn {
703            WorkerReconstructor::Sync(f) => f(wire_payload, empty_deps),
704            WorkerReconstructor::Async(f) => f(wire_payload, empty_deps).await,
705        };
706        let applied = execution.provide_cloneable_value(dep_id, reconstructed);
707        assert!(
708            applied,
709            "Hosted dep '{dep_id}' could not be pre-populated locally"
710        );
711    }
712}
713
714async fn pick_next(execution: &Arc<Mutex<TestSuiteExecution>>) -> Option<TestExecution> {
715    let mut execution = execution.lock().await;
716    execution.pick_next().await
717}
718
719async fn run_with_flakiness_control<F>(
720    output: Arc<dyn TestRunnerOutput>,
721    test_description: &RegisteredTest,
722    idx: usize,
723    count: usize,
724    test: F,
725) -> Result<Result<(), FailureCause>, Box<dyn Any + Send>>
726where
727    F: Fn(
728            Instant,
729        )
730            -> Pin<Box<dyn Future<Output = Result<Result<(), FailureCause>, Box<dyn Any + Send>>>>>
731        + Send
732        + Sync,
733{
734    match &test_description.props.flakiness_control {
735        FlakinessControl::None => {
736            let start = Instant::now();
737            test(start).await
738        }
739        FlakinessControl::ProveNonFlaky(tries) => {
740            for n in 0..*tries {
741                if n > 0 {
742                    output.repeat_running_test(
743                        test_description,
744                        idx,
745                        count,
746                        n + 1,
747                        *tries,
748                        "to ensure test is not flaky",
749                    );
750                }
751                let start = Instant::now();
752                match test(start).await {
753                    Ok(Ok(())) => {}
754                    Ok(Err(e)) => return Ok(Err(e)),
755                    Err(e) => return Err(e),
756                };
757            }
758            Ok(Ok(()))
759        }
760        FlakinessControl::RetryKnownFlaky(max_retries) => {
761            let mut tries = 1;
762            loop {
763                let start = Instant::now();
764                let result = test(start).await;
765
766                if result.is_err() && tries < *max_retries {
767                    tries += 1;
768                    output.repeat_running_test(
769                        test_description,
770                        idx,
771                        count,
772                        tries,
773                        *max_retries,
774                        "because test is known to be flaky",
775                    );
776                } else {
777                    break result;
778                }
779            }
780        }
781    }
782}
783
784#[allow(clippy::too_many_arguments)]
785async fn run_test(
786    output: Arc<dyn TestRunnerOutput>,
787    idx: usize,
788    count: usize,
789    nocapture: bool,
790    include_ignored: bool,
791    ensure_time: Option<TimeThreshold>,
792    dependency_view: Arc<dyn internal::DependencyView + Send + Sync>,
793    test: &RegisteredTest,
794    worker: &mut Option<Worker>,
795) -> TestResult {
796    if test.props.is_ignored && !include_ignored {
797        TestResult::ignored()
798    } else if let Some(worker) = worker.as_mut() {
799        worker.run_test(nocapture, test).await
800    } else {
801        let start = Instant::now();
802        let test = test.clone();
803        match &test.run {
804            TestFunction::Sync(_) => {
805                let handle = spawn_blocking(move || {
806                    let test = test.clone();
807                    crate::sync::run_sync_test_function(
808                        output,
809                        &test,
810                        idx,
811                        count,
812                        ensure_time,
813                        dependency_view,
814                    )
815                });
816                handle.await.unwrap_or_else(|join_error| {
817                    TestResult::failed(
818                        start.elapsed(),
819                        FailureCause::HarnessError(format!(
820                            "Failed joining test task: {join_error}"
821                        )),
822                    )
823                })
824            }
825            TestFunction::Async(test_fn) => {
826                let timeout = test.props.timeout;
827                let test_fn = test_fn.clone();
828                let detached_panic_policy = test.props.detached_panic_policy.clone();
829                let result = run_with_flakiness_control(output, &test, idx, count, |start| {
830                    let dependency_view = dependency_view.clone();
831                    let test_fn = test_fn.clone();
832                    Box::pin(async move {
833                        let test_id = crate::panic_hook::next_test_id();
834                        crate::panic_hook::set_current_test_id(test_id);
835                        crate::panic_hook::create_detached_collector(test_id);
836                        let result = AssertUnwindSafe(Box::pin(async move {
837                            match timeout {
838                                None => test_fn(dependency_view).await,
839                                Some(duration) => {
840                                    let result =
841                                        tokio::time::timeout(duration, test_fn(dependency_view))
842                                            .await;
843                                    match result {
844                                        Ok(result) => result,
845                                        Err(_) => {
846                                            return Err(FailureCause::HarnessError(
847                                                "Test timed out".to_string(),
848                                            ))
849                                        }
850                                    }
851                                }
852                            }
853                            .into_result()?;
854                            if let Some(ensure_time) = ensure_time {
855                                let elapsed = start.elapsed();
856                                if ensure_time.is_critical(&elapsed) {
857                                    return Err(FailureCause::HarnessError(format!(
858                                        "Test run time exceeds critical threshold: {elapsed:?}"
859                                    )));
860                                }
861                            }
862                            Ok(())
863                        }))
864                        .catch_unwind()
865                        .await;
866                        result
867                    })
868                })
869                .await;
870                let mut test_result =
871                    TestResult::from_result(&test.props.should_panic, start.elapsed(), result);
872                if let Some(test_id) = crate::panic_hook::current_test_id() {
873                    if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
874                        let panics = match collector.lock() {
875                            Ok(p) => p,
876                            Err(poisoned) => poisoned.into_inner(),
877                        };
878                        if !panics.is_empty()
879                            && detached_panic_policy == internal::DetachedPanicPolicy::FailTest
880                            && test_result.is_passed()
881                        {
882                            let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
883                            test_result = TestResult::failed(
884                                start.elapsed(),
885                                FailureCause::Panic(internal::PanicCause {
886                                    message: Some(format!(
887                                        "Detached task(s) panicked:\n{}",
888                                        messages.join("\n---\n")
889                                    )),
890                                    location: panics.first().and_then(|p| p.location.clone()),
891                                    backtrace: panics.first().and_then(|p| p.backtrace.clone()),
892                                }),
893                            );
894                        }
895                    }
896                }
897                crate::panic_hook::clear_current_test_id();
898                test_result
899            }
900            TestFunction::SyncBench(_) => {
901                let handle = spawn_blocking(move || {
902                    let test = test.clone();
903                    crate::sync::run_sync_test_function(
904                        output,
905                        &test,
906                        idx,
907                        count,
908                        ensure_time,
909                        dependency_view,
910                    )
911                });
912                handle.await.unwrap_or_else(|join_error| {
913                    TestResult::failed(
914                        start.elapsed(),
915                        FailureCause::HarnessError(format!(
916                            "Failed joining test task: {join_error}"
917                        )),
918                    )
919                })
920            }
921            TestFunction::AsyncBench(bench_fn) => {
922                let mut bencher = AsyncBencher::new();
923                let test_id = crate::panic_hook::next_test_id();
924                crate::panic_hook::set_current_test_id(test_id);
925                let result = AssertUnwindSafe(async move {
926                    bench_fn(&mut bencher, dependency_view).await;
927                    (
928                        bencher
929                            .summary()
930                            .expect("iter() was not called in bench function"),
931                        bencher.bytes,
932                    )
933                })
934                .catch_unwind()
935                .await;
936                let bytes = result.as_ref().map(|(_, bytes)| *bytes).unwrap_or_default();
937                let test_result = TestResult::from_summary(
938                    &test.props.should_panic,
939                    start.elapsed(),
940                    result.map(|(summary, _)| summary),
941                    bytes,
942                );
943                crate::panic_hook::clear_current_test_id();
944                test_result
945            }
946        }
947    }
948}
949
950struct Worker {
951    _listener: Listener,
952    process: Child,
953    _out_handle: JoinHandle<()>,
954    _err_handle: JoinHandle<()>,
955    out_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
956    err_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
957    capture_enabled: Arc<Mutex<bool>>,
958    connection: Stream,
959    exhausted: bool,
960    /// Parent-held HostedRpc owner cells keyed by fully-qualified dep id. Used
961    /// to dispatch incoming `IpcResponse::HostedRpcCall` frames from the worker
962    /// subprocess back to the right owner.
963    hosted_rpc_owner_cells: Arc<HashMap<String, Arc<HostedRpcOwnerCell>>>,
964}
965
966impl Worker {
967    /// Installs the parent-side map of HostedRpc owner cells so this worker can
968    /// route incoming `IpcResponse::HostedRpcCall` frames to the right
969    /// `HostedRpcOwnerCell` while waiting for a worker subprocess response.
970    fn set_hosted_rpc_owner_cells(&mut self, cells: Arc<HashMap<String, Arc<HostedRpcOwnerCell>>>) {
971        self.hosted_rpc_owner_cells = cells;
972    }
973
974    /// Parent-side dispatcher for a single `IpcResponse::HostedRpcCall`. Looks
975    /// up the owner cell by fully-qualified dep id, runs the dispatch on the
976    /// parent's stored owner, and writes the matching
977    /// `IpcCommand::HostedRpcReply` back to the worker subprocess. Mirrors
978    /// `sync::Worker::handle_hosted_rpc_call`.
979    async fn handle_hosted_rpc_call(
980        &mut self,
981        dump_on_ipc_failure: &DumpOnFailure,
982        request_id: u64,
983        dep_id: String,
984        method_idx: u32,
985        args_bytes: Vec<u8>,
986    ) {
987        let body = match self.hosted_rpc_owner_cells.get(&dep_id) {
988            // Use the async dispatch entry point so an owner that implements
989            // `AsyncHostedRpcDep` directly can `.await` inside its dispatcher
990            // without blocking the tokio runtime. Sync owners reach this
991            // entry point through the blanket bridge and their dispatched
992            // future resolves immediately.
993            Some(cell) => match cell.dispatch_async(method_idx, &args_bytes).await {
994                Ok(result_bytes) => HostedRpcReplyBody::Ok { result_bytes },
995                Err(message) => HostedRpcReplyBody::Err { message },
996            },
997            None => HostedRpcReplyBody::Err {
998                message: format!(
999                    "HostedRpc dispatch: unknown dep id '{dep_id}' in parent owner-cell map"
1000                ),
1001            },
1002        };
1003        let reply = IpcCommand::HostedRpcReply { request_id, body };
1004        let msg = serialize_to_byte_vec(&reply).expect("Failed to encode HostedRpcReply");
1005        dump_on_ipc_failure
1006            .run(write_frame_async(&mut self.connection, &msg).await)
1007            .await;
1008    }
1009
1010    pub async fn run_test(&mut self, nocapture: bool, test: &RegisteredTest) -> TestResult {
1011        let mut capture_enabled = self.capture_enabled.lock().await;
1012        *capture_enabled = test.props.capture_control.requires_capturing(!nocapture);
1013        drop(capture_enabled);
1014
1015        // Send IPC command and wait for IPC response, and in the meantime read from the stdout/stderr channels
1016        let cmd = IpcCommand::RunTest {
1017            name: test.name.clone(),
1018            crate_name: test.crate_name.clone(),
1019            module_path: test.module_path.clone(),
1020        };
1021
1022        let dump_on_ipc_failure = self.dump_on_failure();
1023
1024        let msg = serialize_to_byte_vec(&cmd).expect("Failed to encode IPC command");
1025        dump_on_ipc_failure
1026            .run(write_frame_async(&mut self.connection, &msg).await)
1027            .await;
1028
1029        let response = loop {
1030            let response_bytes = dump_on_ipc_failure
1031                .run(read_frame_async(&mut self.connection).await)
1032                .await;
1033            let response: IpcResponse = dump_on_ipc_failure.run(deserialize(&response_bytes)).await;
1034            match response {
1035                IpcResponse::TestFinished { .. } => break response,
1036                IpcResponse::CloneableAccepted { .. }
1037                | IpcResponse::HostedDescriptorAccepted { .. } => continue,
1038                IpcResponse::HostedRpcCall {
1039                    request_id,
1040                    dep_id,
1041                    method_idx,
1042                    args_bytes,
1043                } => {
1044                    self.handle_hosted_rpc_call(
1045                        &dump_on_ipc_failure,
1046                        request_id,
1047                        dep_id,
1048                        method_idx,
1049                        args_bytes,
1050                    )
1051                    .await;
1052                    continue;
1053                }
1054            }
1055        };
1056
1057        let IpcResponse::TestFinished {
1058            result,
1059            finish_marker,
1060            worker_exhausted,
1061        } = response
1062        else {
1063            unreachable!("loop only breaks on TestFinished")
1064        };
1065        self.exhausted = worker_exhausted;
1066
1067        if test.props.capture_control.requires_capturing(!nocapture) {
1068            let out_lines: Vec<_> =
1069                Self::drain_until(self.out_lines.clone(), finish_marker.clone()).await;
1070            let err_lines: Vec<_> =
1071                Self::drain_until(self.err_lines.clone(), finish_marker.clone()).await;
1072            result.into_test_result(out_lines, err_lines)
1073        } else {
1074            result.into_test_result(Vec::new(), Vec::new())
1075        }
1076    }
1077
1078    /// Ends this worker only after its owning scheduler thread has finished.
1079    /// A worker cannot infer that fact from its private execution plan because
1080    /// its test order may diverge from the shared parent scheduler.
1081    async fn shutdown(mut self) {
1082        let msg = serialize_to_byte_vec(&IpcCommand::Shutdown)
1083            .expect("Failed to encode IPC shutdown command");
1084        let dump_on_ipc_failure = self.dump_on_failure();
1085        dump_on_ipc_failure
1086            .run(write_frame_async(&mut self.connection, &msg).await)
1087            .await;
1088
1089        self.process
1090            .wait()
1091            .await
1092            .expect("Failed to wait for worker process");
1093    }
1094
1095    /// Async counterpart to `sync::Worker::provide_cloneable`. `dep_id` is the
1096    /// dep's fully-qualified id (`{crate}::{module}::{name}`).
1097    async fn provide_cloneable(&mut self, dep_id: String, wire_bytes: Vec<u8>) {
1098        let dump_on_ipc_failure = self.dump_on_failure();
1099        let cmd = IpcCommand::ProvideCloneable {
1100            dep_id: dep_id.clone(),
1101            wire_bytes,
1102        };
1103        let msg = serialize_to_byte_vec(&cmd).expect("Failed to encode IPC command");
1104        dump_on_ipc_failure
1105            .run(write_frame_async(&mut self.connection, &msg).await)
1106            .await;
1107
1108        loop {
1109            let response_bytes = dump_on_ipc_failure
1110                .run(read_frame_async(&mut self.connection).await)
1111                .await;
1112            let response: IpcResponse = dump_on_ipc_failure.run(deserialize(&response_bytes)).await;
1113            match response {
1114                IpcResponse::CloneableAccepted { dep_id: ack_id } => {
1115                    if ack_id == dep_id {
1116                        return;
1117                    }
1118                }
1119                IpcResponse::HostedDescriptorAccepted { .. } => {
1120                    // Out-of-band ack from a previous ProvideHostedDescriptor; ignore.
1121                }
1122                IpcResponse::TestFinished { .. } => {
1123                    // Should not happen before any RunTest.
1124                }
1125                IpcResponse::HostedRpcCall {
1126                    request_id,
1127                    dep_id: rpc_dep_id,
1128                    method_idx,
1129                    args_bytes,
1130                } => {
1131                    // A worker subprocess can issue a HostedRpc call from
1132                    // inside an in-progress test, even while the parent is
1133                    // mid-`ProvideCloneable` for a different dep. Dispatch it
1134                    // so the protocol doesn't desync.
1135                    self.handle_hosted_rpc_call(
1136                        &dump_on_ipc_failure,
1137                        request_id,
1138                        rpc_dep_id,
1139                        method_idx,
1140                        args_bytes,
1141                    )
1142                    .await;
1143                }
1144            }
1145        }
1146    }
1147
1148    /// Async counterpart to `sync::Worker::provide_hosted_descriptor`.
1149    async fn provide_hosted_descriptor(&mut self, dep_id: String, wire_bytes: Vec<u8>) {
1150        let dump_on_ipc_failure = self.dump_on_failure();
1151        let cmd = IpcCommand::ProvideHostedDescriptor {
1152            dep_id: dep_id.clone(),
1153            wire_bytes,
1154        };
1155        let msg = serialize_to_byte_vec(&cmd).expect("Failed to encode IPC command");
1156        dump_on_ipc_failure
1157            .run(write_frame_async(&mut self.connection, &msg).await)
1158            .await;
1159
1160        loop {
1161            let response_bytes = dump_on_ipc_failure
1162                .run(read_frame_async(&mut self.connection).await)
1163                .await;
1164            let response: IpcResponse = dump_on_ipc_failure.run(deserialize(&response_bytes)).await;
1165            match response {
1166                IpcResponse::HostedDescriptorAccepted { dep_id: ack_id } => {
1167                    if ack_id == dep_id {
1168                        return;
1169                    }
1170                }
1171                IpcResponse::CloneableAccepted { .. } => {
1172                    // Out-of-band ack from a previous ProvideCloneable; ignore.
1173                }
1174                IpcResponse::TestFinished { .. } => {
1175                    // Should not happen before any RunTest.
1176                }
1177                IpcResponse::HostedRpcCall {
1178                    request_id,
1179                    dep_id: rpc_dep_id,
1180                    method_idx,
1181                    args_bytes,
1182                } => {
1183                    // See provide_cloneable arm. Dispatch the call inline so
1184                    // the IPC stream stays in sync.
1185                    self.handle_hosted_rpc_call(
1186                        &dump_on_ipc_failure,
1187                        request_id,
1188                        rpc_dep_id,
1189                        method_idx,
1190                        args_bytes,
1191                    )
1192                    .await;
1193                }
1194            }
1195        }
1196    }
1197
1198    fn dump_on_failure(&self) -> DumpOnFailure {
1199        DumpOnFailure {
1200            out_lines: self.out_lines.clone(),
1201            err_lines: self.err_lines.clone(),
1202        }
1203    }
1204
1205    async fn drain_until(
1206        source: Arc<Mutex<VecDeque<CapturedOutput>>>,
1207        finish_marker: String,
1208    ) -> Vec<CapturedOutput> {
1209        let mut result = Vec::new();
1210        loop {
1211            let mut source = source.lock().await;
1212            while let Some(line) = source.pop_front() {
1213                if line.line() == finish_marker {
1214                    return result;
1215                } else {
1216                    result.push(line.clone());
1217                }
1218            }
1219            drop(source);
1220
1221            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1222        }
1223    }
1224}
1225
1226struct DumpOnFailure {
1227    out_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
1228    err_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
1229}
1230
1231impl DumpOnFailure {
1232    pub async fn run<T, E>(&self, result: Result<T, E>) -> T {
1233        match result {
1234            Ok(value) => value,
1235            Err(_error) => {
1236                let out_lines: Vec<_> = self.out_lines.lock().await.drain(..).collect();
1237                let err_lines: Vec<_> = self.err_lines.lock().await.drain(..).collect();
1238                let mut all_lines = [out_lines, err_lines].concat();
1239                all_lines.sort();
1240
1241                // Route the diagnostic through the real terminal even
1242                // when host capture has fd 2 redirected into its pipe.
1243                // We `process::exit(1)` immediately after this and
1244                // never reach `host_capture.finalize()`, so anything we
1245                // wrote into the host pipe would be lost. The capture
1246                // pipe and its reader are abandoned on exit; that is
1247                // acceptable for this fatal-IPC path.
1248                use std::io::Write;
1249                let mut err = crate::host_capture::TerminalStderr;
1250                for line in all_lines {
1251                    let _ = writeln!(err, "{}", line.line());
1252                }
1253                let _ = err.flush();
1254
1255                std::process::exit(1);
1256            }
1257        }
1258    }
1259}
1260
1261async fn spawn_worker_if_needed(args: &Arguments) -> Option<Worker> {
1262    if args.spawn_workers {
1263        let id = Uuid::new_v4();
1264        let name_str = format!("{id}.sock");
1265        let name = name_str
1266            .clone()
1267            .to_ns_name::<GenericNamespaced>()
1268            .expect("Invalid local socket name");
1269        let opts = ListenerOptions::new().name(name.clone());
1270        let listener = opts
1271            .create_tokio()
1272            .expect("Failed to create local socket listener");
1273
1274        let exe = std::env::current_exe().expect("Failed to get current executable path");
1275
1276        let mut args = args.clone();
1277        args.ipc = Some(name_str);
1278        args.spawn_workers = false;
1279        args.logfile = None;
1280        let args = args.to_args();
1281
1282        let mut process = Command::new(exe)
1283            .args(args)
1284            .stdin(Stdio::inherit())
1285            .stderr(Stdio::piped())
1286            .stdout(Stdio::piped())
1287            .spawn()
1288            .expect("Failed to spawn worker process");
1289
1290        let stdout = process.stdout.take().unwrap();
1291        let stderr = process.stderr.take().unwrap();
1292
1293        let out_lines = Arc::new(Mutex::new(VecDeque::new()));
1294        let err_lines = Arc::new(Mutex::new(VecDeque::new()));
1295        let capture_enabled = Arc::new(Mutex::new(true));
1296
1297        let out_lines_clone = out_lines.clone();
1298        let capture_enabled_clone = capture_enabled.clone();
1299        let out_handle = spawn(async move {
1300            let reader = BufReader::new(stdout);
1301            let mut lines = reader.lines();
1302            while let Some(line) = lines
1303                .next_line()
1304                .await
1305                .expect("Failed to read from worker stdout")
1306            {
1307                if *capture_enabled_clone.lock().await {
1308                    out_lines_clone
1309                        .lock()
1310                        .await
1311                        .push_back(CapturedOutput::stdout(line));
1312                } else {
1313                    // `#[never_capture]` pass-through: write to the real
1314                    // terminal even when host capture has redirected
1315                    // fd 1 into its pipe, so the worker line stays
1316                    // uncaptured live output and is not later
1317                    // re-labelled `[host]`.
1318                    use std::io::Write;
1319                    let mut out = crate::host_capture::TerminalStdout;
1320                    let _ = writeln!(out, "{line}");
1321                    let _ = out.flush();
1322                }
1323            }
1324        });
1325
1326        let err_lines_clone = err_lines.clone();
1327        let capture_enabled_clone = capture_enabled.clone();
1328        let err_handle = spawn(async move {
1329            let reader = BufReader::new(stderr);
1330            let mut lines = reader.lines();
1331            while let Some(line) = lines
1332                .next_line()
1333                .await
1334                .expect("Failed to read from worker stderr")
1335            {
1336                if *capture_enabled_clone.lock().await {
1337                    err_lines_clone
1338                        .lock()
1339                        .await
1340                        .push_back(CapturedOutput::stderr(line));
1341                } else {
1342                    // Same as the stdout pass-through above: route the
1343                    // never-captured worker line to the real terminal
1344                    // stderr, not the host capture pipe.
1345                    use std::io::Write;
1346                    let mut err = crate::host_capture::TerminalStderr;
1347                    let _ = writeln!(err, "{line}");
1348                    let _ = err.flush();
1349                }
1350            }
1351        });
1352
1353        let connection = listener
1354            .accept()
1355            .await
1356            .expect("Failed to accept connection");
1357
1358        Some(Worker {
1359            _listener: listener,
1360            process,
1361            _out_handle: out_handle,
1362            _err_handle: err_handle,
1363            out_lines,
1364            err_lines,
1365            connection,
1366            capture_enabled,
1367            exhausted: false,
1368            hosted_rpc_owner_cells: Arc::new(HashMap::new()),
1369        })
1370    } else {
1371        None
1372    }
1373}
1374
1375/// Parent-side `--nocapture` / no-spawn-workers helper. Builds one stub per
1376/// HostedRpc dep using an [`InProcessHostedRpcTransport`] that points at the
1377/// parent-held owner cells, and stashes it in the execution tree so dependency
1378/// materialisation skips the owner-only constructor. Mirrors
1379/// `sync::install_local_hosted_rpc_stubs`.
1380fn install_local_hosted_rpc_stubs(
1381    execution: &mut TestSuiteExecution,
1382    rpc_factories: &HashMap<String, RpcFactory>,
1383    owner_cells: &HashMap<String, Arc<HostedRpcOwnerCell>>,
1384) {
1385    let transport: Arc<dyn HostedRpcTransport> =
1386        Arc::new(InProcessHostedRpcTransport::new(owner_cells.clone()));
1387    for (dep_id, factory) in rpc_factories.iter() {
1388        if !owner_cells.contains_key(dep_id) {
1389            // No owner cell materialised for this dep (e.g. registered
1390            // globally but not pulled into the current filter). Skip so
1391            // we don't install a stub nothing routes.
1392            continue;
1393        }
1394        let channel = HostedRpcChannel::new(dep_id.clone(), transport.clone());
1395        let stub = (factory.build_stub)(channel);
1396        let applied = execution.provide_cloneable_value(dep_id, stub);
1397        if !applied {
1398            // The owner cell can be materialised solely because another
1399            // parent-side Cloneable/Hosted/HostedRpc dependency needs this
1400            // HostedRpc dep as a constructor input. In that case the stub is
1401            // intentionally not present in the worker execution tree; there is
1402            // nothing to pre-populate for no-spawn test execution.
1403            continue;
1404        }
1405    }
1406}
1407
1408/// Worker subprocess helper. Builds one stub per registered HostedRpc dep backed
1409/// by [`IpcHostedRpcTransport`], and installs it in the execution tree. Mirrors
1410/// `sync::install_worker_subprocess_hosted_rpc_stubs` but runs on the tokio
1411/// `Arc<Mutex<Stream>>` connection.
1412async fn install_worker_subprocess_hosted_rpc_stubs(
1413    execution: &Arc<Mutex<TestSuiteExecution>>,
1414    rpc_factories: &HashMap<String, RpcFactory>,
1415    connection_arc: Arc<Mutex<Stream>>,
1416) {
1417    let transport: Arc<dyn HostedRpcTransport> =
1418        Arc::new(IpcHostedRpcTransport::new(connection_arc));
1419    for (dep_id, factory) in rpc_factories.iter() {
1420        let channel = HostedRpcChannel::new(dep_id.clone(), transport.clone());
1421        let stub = (factory.build_stub)(channel);
1422        let mut execution = execution.lock().await;
1423        let applied = execution.provide_cloneable_value(dep_id, stub);
1424        // Not every binary registers a HostedRpc dep that the current
1425        // execution actually uses; if so, just move on.
1426        let _ = applied;
1427    }
1428}
1429
1430/// Worker subprocess `HostedRpcTransport` for the tokio runner.
1431/// Mirrors `sync::IpcHostedRpcTransport` but bridges a sync trait method
1432/// to the async tokio IPC primitives via
1433/// `tokio::task::block_in_place` + `Handle::current().block_on(...)`.
1434///
1435/// The shared `Arc<Mutex<Stream>>` is the same one used by the
1436/// worker subprocess's main IPC loop; the lock guarantees that a stub
1437/// call and the main loop never interleave a half-written frame. Calls
1438/// serialise across all in-flight stubs by acquiring the mutex for the
1439/// full request-then-reply round trip.
1440///
1441/// This relies on the temporal invariant documented on
1442/// [`crate::internal::HostedRpcChannel::call`]: stubs are only invoked
1443/// from inside a running test body, never from `build_stub`, and never
1444/// from detached background work that outlives the test. Under those
1445/// rules the main IPC loop is only reading between tests, so it cannot
1446/// race with a stub call.
1447struct IpcHostedRpcTransport {
1448    connection: Arc<Mutex<Stream>>,
1449    next_request_id: AtomicU64,
1450}
1451
1452impl IpcHostedRpcTransport {
1453    fn new(connection: Arc<Mutex<Stream>>) -> Self {
1454        Self {
1455            connection,
1456            next_request_id: AtomicU64::new(0),
1457        }
1458    }
1459}
1460
1461impl HostedRpcTransport for IpcHostedRpcTransport {
1462    fn call(
1463        &self,
1464        dep_id: &str,
1465        method_idx: u32,
1466        args: Vec<u8>,
1467    ) -> Result<Vec<u8>, HostedRpcError> {
1468        let request_id = self.next_request_id.fetch_add(1, Ordering::SeqCst);
1469        let call = IpcResponse::HostedRpcCall {
1470            request_id,
1471            dep_id: dep_id.to_string(),
1472            method_idx,
1473            args_bytes: args,
1474        };
1475        let msg = serialize_to_byte_vec(&call).map_err(|e| {
1476            HostedRpcError::Transport(format!("encode HostedRpcCall failed: {e:?}"))
1477        })?;
1478
1479        let connection = self.connection.clone();
1480        let handle = tokio::runtime::Handle::current();
1481
1482        // Run the async I/O round-trip from inside this sync trait
1483        // method. `block_in_place` releases this worker thread back to
1484        // the scheduler so the parent's read loop can continue making
1485        // progress on other tasks while we wait for the reply.
1486        tokio::task::block_in_place(move || {
1487            handle.block_on(async move {
1488                let mut conn = connection.lock().await;
1489                write_frame_async(&mut *conn, &msg).await.map_err(|e| {
1490                    HostedRpcError::Transport(format!("write HostedRpcCall failed: {e:?}"))
1491                })?;
1492                let reply_bytes = read_frame_async(&mut *conn).await.map_err(|e| {
1493                    HostedRpcError::Transport(format!("read HostedRpcReply failed: {e:?}"))
1494                })?;
1495                let command: IpcCommand = deserialize(&reply_bytes).map_err(|e| {
1496                    HostedRpcError::Transport(format!("decode HostedRpcReply failed: {e:?}"))
1497                })?;
1498                match command {
1499                    IpcCommand::HostedRpcReply {
1500                        request_id: reply_id,
1501                        body,
1502                    } => {
1503                        if reply_id != request_id {
1504                            return Err(HostedRpcError::Transport(format!(
1505                                "HostedRpcReply request_id mismatch: expected {request_id}, got {reply_id}"
1506                            )));
1507                        }
1508                        match body {
1509                            HostedRpcReplyBody::Ok { result_bytes } => Ok(result_bytes),
1510                            HostedRpcReplyBody::Err { message } => {
1511                                Err(HostedRpcError::Dispatch(message))
1512                            }
1513                        }
1514                    }
1515                    other => Err(HostedRpcError::Transport(format!(
1516                        "unexpected IpcCommand while waiting for HostedRpcReply: {other:?}"
1517                    ))),
1518                }
1519            })
1520        })
1521    }
1522}