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            // Drop parent-owned hosted / hosted-rpc owners BEFORE
295            // finalising host capture so any `Drop` impls that
296            // shutdown background threads / subprocesses get a chance
297            // to emit their final lines through the still-active
298            // capture pipe. If we restored fd 1/2 first, those late
299            // lines would either land on the about-to-render
300            // structured output or be swallowed entirely.
301            drop(hosted_rpc_owner_cells);
302            drop(_hosted_owners);
303
304            // Finalise host capture (if any) BEFORE rendering the
305            // suite so the attributed host-log records make it into
306            // the per-test captured-output vecs the formatter walks.
307            if let Some(hc) = host_capture.take() {
308                let epoch_wall = host_capture_epoch_wall.unwrap_or_else(|| hc.epoch_wall());
309                let records = hc.finalize();
310                let windows = host_windows.lock().await;
311                let windows_indexed: Vec<(usize, crate::host_capture::HostWindow)> =
312                    windows.iter().copied().enumerate().collect();
313                crate::host_capture::attribute_records_to_tests(
314                    epoch_wall,
315                    &records,
316                    &windows_indexed,
317                    &mut results,
318                );
319            }
320            output.finished_suite(&all_tests, &results, start.elapsed());
321            exit_code = SuiteResult::exit_code(&results);
322
323            if exit_code == ExitCode::SUCCESS {
324                break;
325            } else {
326                remaining_retries -= 1;
327            }
328        }
329        exit_code
330    }
331}
332
333#[allow(clippy::too_many_arguments)]
334async fn test_thread(
335    args: Arguments,
336    execution: Arc<Mutex<TestSuiteExecution>>,
337    output: Arc<dyn TestRunnerOutput>,
338    count: usize,
339    results: Arc<Mutex<Vec<(RegisteredTest, TestResult)>>>,
340    host_windows: Arc<Mutex<Vec<crate::host_capture::HostWindow>>>,
341    cloneable_wire_bytes: Arc<Vec<DepWireBytes>>,
342    hosted_descriptor_bytes: Arc<Vec<DepWireBytes>>,
343    cloneable_codecs: Arc<HashMap<String, (CloneableCodec, WorkerReconstructor)>>,
344    rpc_factories: Arc<HashMap<String, RpcFactory>>,
345    hosted_rpc_owner_cells: Arc<HashMap<String, Arc<HostedRpcOwnerCell>>>,
346    host_capture_epoch: Option<std::time::Instant>,
347) {
348    let mut worker = spawn_worker_if_needed(&args).await;
349    // Parent dispatches incoming `HostedRpcCall` frames against the owner
350    // cells materialised in the top-level parent. Workers don't need the owner
351    // cells (they own stubs instead), so they receive an empty map and the
352    // dispatch code path is never reached in subprocesses.
353    if let Some(worker) = worker.as_mut() {
354        worker.set_hosted_rpc_owner_cells(hosted_rpc_owner_cells.clone());
355    }
356    let connection_arc = if let Some(ref name) = args.ipc {
357        let name = ipc_name(name.clone());
358        let stream = Stream::connect(name)
359            .await
360            .expect("Failed to connect to IPC socket");
361        Some(Arc::new(Mutex::new(stream)))
362    } else {
363        None
364    };
365
366    if let Some(worker) = worker.as_mut() {
367        for (dep_id, wire_bytes) in cloneable_wire_bytes.iter() {
368            worker
369                .provide_cloneable(dep_id.clone(), wire_bytes.clone())
370                .await;
371        }
372        // Ship every Hosted dep's descriptor bytes too.
373        for (dep_id, descriptor_bytes) in hosted_descriptor_bytes.iter() {
374            worker
375                .provide_hosted_descriptor(dep_id.clone(), descriptor_bytes.clone())
376                .await;
377        }
378    }
379
380    // Worker subprocess side: build a stub for every HostedRpc dep registered
381    // in this binary using the IPC-backed transport sharing the same socket as
382    // the main IPC loop. Install the stubs in the execution tree so dependency
383    // materialisation skips the parent-only owner constructor.
384    if let Some(connection) = connection_arc.as_ref() {
385        if !rpc_factories.is_empty() {
386            install_worker_subprocess_hosted_rpc_stubs(
387                &execution,
388                &rpc_factories,
389                connection.clone(),
390            )
391            .await;
392        }
393    }
394
395    let mut expected_test = None;
396
397    while !is_done(&execution).await {
398        if let Some(connection) = connection_arc.as_ref() {
399            while expected_test.is_none() {
400                let mut conn = connection.lock().await;
401                let command_bytes = read_frame_async(&mut *conn)
402                    .await
403                    .expect("Failed to read IPC command frame");
404                drop(conn);
405                let command: IpcCommand =
406                    deserialize(&command_bytes).expect("Failed to decode IPC command");
407
408                match command {
409                    IpcCommand::RunTest {
410                        name,
411                        crate_name,
412                        module_path,
413                    } => {
414                        expected_test = Some((name, crate_name, module_path));
415                    }
416                    IpcCommand::ProvideCloneable { dep_id, wire_bytes } => {
417                        // Worker-side reconstruction (see sync.rs::apply_provided_wire_bytes).
418                        apply_provided_wire_bytes(
419                            &execution,
420                            &cloneable_codecs,
421                            &dep_id,
422                            &wire_bytes,
423                            "ProvideCloneable",
424                        )
425                        .await;
426                        let response = IpcResponse::CloneableAccepted { dep_id };
427                        let msg = serialize_to_byte_vec(&response)
428                            .expect("Failed to encode IPC response");
429                        let mut conn = connection.lock().await;
430                        write_frame_async(&mut *conn, &msg)
431                            .await
432                            .expect("Failed to write IPC response frame");
433                    }
434                    IpcCommand::ProvideHostedDescriptor { dep_id, wire_bytes } => {
435                        // Worker-side reconstruction: same shape as
436                        // ProvideCloneable but routed through the registered
437                        // HostedDep worker_fn.
438                        apply_provided_wire_bytes(
439                            &execution,
440                            &cloneable_codecs,
441                            &dep_id,
442                            &wire_bytes,
443                            "ProvideHostedDescriptor",
444                        )
445                        .await;
446                        let response = IpcResponse::HostedDescriptorAccepted { dep_id };
447                        let msg = serialize_to_byte_vec(&response)
448                            .expect("Failed to encode IPC response");
449                        let mut conn = connection.lock().await;
450                        write_frame_async(&mut *conn, &msg)
451                            .await
452                            .expect("Failed to write IPC response frame");
453                    }
454                    IpcCommand::HostedRpcReply { .. } => {
455                        // HR1.2: replies for worker-initiated HostedRpc calls
456                        // are consumed inline by the IPC transport during
457                        // test execution, never by this between-tests
458                        // command loop. Receiving one here means the
459                        // protocol got out of sync; surface that loudly
460                        // rather than dropping the frame.
461                        panic!(
462                            "unexpected `HostedRpcReply` while waiting for the next \
463                             between-tests command in the tokio worker subprocess: a \
464                             stub call must have left a reply on the wire without \
465                             draining it inline"
466                        );
467                    }
468                }
469            }
470        }
471
472        if let Some(next) = pick_next(&execution).await {
473            let skip = if let Some((name, crate_name, module_path)) = &expected_test {
474                next.test.name != *name
475                    || next.test.crate_name != *crate_name
476                    || next.test.module_path != *module_path
477            } else {
478                false
479            };
480
481            if !skip {
482                expected_test = None;
483
484                let ensure_time = get_ensure_time(&args, &next.test);
485
486                // Snapshot the parent's monotonic-clock view of the
487                // test start. The matching end-instant is captured
488                // after `finished_running_test`, and the pair becomes
489                // a `HostWindow` for record attribution. Uses
490                // `std::time::Instant` because the host-capture epoch
491                // is `std::time::Instant` (tokio's `Instant` is a
492                // different type without `From` interop).
493                let window_start = std::time::Instant::now();
494
495                output.start_running_test(&next.test, next.index, count);
496                let result = run_test(
497                    output.clone(),
498                    next.index,
499                    count,
500                    args.nocapture,
501                    args.include_ignored,
502                    ensure_time,
503                    next.deps.clone(),
504                    &next.test,
505                    &mut worker,
506                )
507                .await;
508                output.finished_running_test(&next.test, next.index, count, &result);
509                let window_end = std::time::Instant::now();
510
511                if let Some(connection) = connection_arc.as_ref() {
512                    let finish_marker = Uuid::new_v4().to_string();
513                    let finish_marker_line = format!("{finish_marker}\n");
514                    tokio::io::stdout()
515                        .write_all(finish_marker_line.as_bytes())
516                        .await
517                        .unwrap();
518                    tokio::io::stderr()
519                        .write_all(finish_marker_line.as_bytes())
520                        .await
521                        .unwrap();
522                    tokio::io::stdout().flush().await.unwrap();
523                    tokio::io::stderr().flush().await.unwrap();
524
525                    let response = IpcResponse::TestFinished {
526                        result: (&result).into(),
527                        finish_marker,
528                    };
529                    let msg =
530                        serialize_to_byte_vec(&response).expect("Failed to encode IPC response");
531                    let mut conn = connection.lock().await;
532                    write_frame_async(&mut *conn, &msg)
533                        .await
534                        .expect("Failed to write IPC response frame");
535                }
536
537                // Push the result and its window under the same
538                // critical section so the two vecs stay aligned in the
539                // face of concurrent pushes from sibling test_threads.
540                let window = crate::host_capture::HostWindow::from_instants(
541                    host_capture_epoch,
542                    window_start,
543                    window_end,
544                )
545                .unwrap_or(crate::host_capture::HostWindow {
546                    start: std::time::Duration::ZERO,
547                    end: std::time::Duration::ZERO,
548                });
549                let mut results_guard = results.lock().await;
550                let mut windows_guard = host_windows.lock().await;
551                results_guard.push((next.test.clone(), result));
552                windows_guard.push(window);
553            }
554        }
555    }
556}
557
558async fn is_done(execution: &Arc<Mutex<TestSuiteExecution>>) -> bool {
559    let execution = execution.lock().await;
560    execution.is_done()
561}
562
563/// Async counterpart to `sync::apply_provided_wire_bytes`. Decodes the wire
564/// bytes into a worker-side dependency value (looked up by the dep's
565/// fully-qualified id `{crate}::{module}::{name}`) and stores it in the
566/// execution tree so the next `materialize_deps` call uses the pre-resolved
567/// value.
568///
569/// `source_command` is the textual name of the IPC command that delivered
570/// the bytes (`"ProvideCloneable"` or `"ProvideHostedDescriptor"`); used
571/// only in panic messages.
572async fn apply_provided_wire_bytes(
573    execution: &Arc<Mutex<TestSuiteExecution>>,
574    wire_codecs: &HashMap<String, (CloneableCodec, WorkerReconstructor)>,
575    dep_id: &str,
576    wire_bytes: &[u8],
577    source_command: &str,
578) {
579    let (codec, worker_fn) = wire_codecs.get(dep_id).unwrap_or_else(|| {
580        panic!("{source_command} referenced unknown wire-shipped dep '{dep_id}'")
581    });
582
583    let wire_payload = (codec.from_wire_bytes)(wire_bytes);
584    let empty_deps: Arc<dyn internal::DependencyView + Send + Sync> =
585        Arc::new(HashMap::<String, Arc<dyn Any + Send + Sync>>::new());
586    let reconstructed = match worker_fn {
587        WorkerReconstructor::Sync(f) => f(wire_payload, empty_deps),
588        WorkerReconstructor::Async(f) => f(wire_payload, empty_deps).await,
589    };
590
591    let mut execution = execution.lock().await;
592    let applied = execution.provide_cloneable_value(dep_id, reconstructed);
593    assert!(
594        applied,
595        "{source_command} for dep '{dep_id}' did not match any registered dep in this worker"
596    );
597}
598
599/// Mode-consistent Cloneable semantics for the no-spawn-workers path on
600/// the tokio runner. Mirrors `sync::apply_cloneable_values_locally`: takes
601/// the parent-constructed Cloneable values and installs them directly
602/// into the parent's `TestSuiteExecution`, so `materialize_deps` reuses
603/// them instead of re-running the user constructor.
604///
605/// For `Cloneable`, the documented round-trip
606/// `from_wire(to_wire(value))` is semantics-preserving, so reusing the
607/// parent value directly is equivalent to round-tripping it through the
608/// wire codec while avoiding the duplicate constructor run that would
609/// otherwise occur on the no-spawn-workers code path. The duplicate run
610/// historically caused user-visible problems (extra observable side
611/// effects under `--nocapture`, and deadlocks when the constructor takes
612/// a runtime-wide lock).
613fn apply_cloneable_values_locally(
614    execution: &mut TestSuiteExecution,
615    cloneable_local_values: &[(String, Arc<dyn Any + Send + Sync>)],
616) {
617    for (dep_id, value) in cloneable_local_values {
618        let applied = execution.provide_cloneable_value(dep_id, value.clone());
619        assert!(
620            applied,
621            "Cloneable dep '{dep_id}' could not be pre-populated locally"
622        );
623    }
624}
625
626/// Tokio counterpart to `sync::apply_parent_constructed_shared_values_locally`.
627/// In no-spawn-workers mode, installs `Shared`/`PerWorker` dep values that
628/// the parent had to construct as transitive inputs to a
629/// Cloneable/Hosted/HostedRpc dep, so the in-process test thread reuses
630/// them instead of re-running the constructor in the same process.
631fn apply_parent_constructed_shared_values_locally(
632    execution: &mut TestSuiteExecution,
633    values: &[(String, Arc<dyn Any + Send + Sync>)],
634) {
635    for (dep_id, value) in values {
636        let applied = execution.provide_materialized_shared_value(dep_id, value.clone());
637        assert!(
638            applied,
639            "Shared/PerWorker dep '{dep_id}' could not be pre-populated locally"
640        );
641    }
642}
643
644/// Mode-consistent Hosted semantics for the no-spawn-workers path on the
645/// tokio runner. Mirrors `sync::apply_hosted_descriptors_locally`: takes
646/// the parent-collected descriptor bytes and reconstructs each Hosted
647/// dep's worker-side handle (via the registered codec + worker_fn)
648/// directly in the parent's `TestSuiteExecution`.
649///
650/// Both `WorkerReconstructor::Sync` (`HostedDep::from_descriptor`) and
651/// `WorkerReconstructor::Async` (`AsyncHostedDep::from_descriptor`) are
652/// supported here so that `async_worker` Hosted deps see the same
653/// worker-side handle whether the runner ended up in spawned-worker mode
654/// or in the no-spawn fallback, matching the documented mode-consistent
655/// Hosted contract in `book/src/advanced_features/dependency_sharing.md`.
656async fn apply_hosted_descriptors_locally(
657    execution: &mut TestSuiteExecution,
658    wire_codecs: &HashMap<String, (CloneableCodec, WorkerReconstructor)>,
659    descriptor_bytes: &[DepWireBytes],
660) {
661    for (dep_id, wire_bytes) in descriptor_bytes {
662        let (codec, worker_fn) = wire_codecs.get(dep_id).unwrap_or_else(|| {
663            panic!("Hosted dep '{dep_id}' missing codec/worker_fn for local handle reconstruction")
664        });
665        let wire_payload = (codec.from_wire_bytes)(wire_bytes);
666        let empty_deps: Arc<dyn internal::DependencyView + Send + Sync> =
667            Arc::new(HashMap::<String, Arc<dyn Any + Send + Sync>>::new());
668        let reconstructed = match worker_fn {
669            WorkerReconstructor::Sync(f) => f(wire_payload, empty_deps),
670            WorkerReconstructor::Async(f) => f(wire_payload, empty_deps).await,
671        };
672        let applied = execution.provide_cloneable_value(dep_id, reconstructed);
673        assert!(
674            applied,
675            "Hosted dep '{dep_id}' could not be pre-populated locally"
676        );
677    }
678}
679
680async fn pick_next(execution: &Arc<Mutex<TestSuiteExecution>>) -> Option<TestExecution> {
681    let mut execution = execution.lock().await;
682    execution.pick_next().await
683}
684
685async fn run_with_flakiness_control<F>(
686    output: Arc<dyn TestRunnerOutput>,
687    test_description: &RegisteredTest,
688    idx: usize,
689    count: usize,
690    test: F,
691) -> Result<Result<(), FailureCause>, Box<dyn Any + Send>>
692where
693    F: Fn(
694            Instant,
695        )
696            -> Pin<Box<dyn Future<Output = Result<Result<(), FailureCause>, Box<dyn Any + Send>>>>>
697        + Send
698        + Sync,
699{
700    match &test_description.props.flakiness_control {
701        FlakinessControl::None => {
702            let start = Instant::now();
703            test(start).await
704        }
705        FlakinessControl::ProveNonFlaky(tries) => {
706            for n in 0..*tries {
707                if n > 0 {
708                    output.repeat_running_test(
709                        test_description,
710                        idx,
711                        count,
712                        n + 1,
713                        *tries,
714                        "to ensure test is not flaky",
715                    );
716                }
717                let start = Instant::now();
718                match test(start).await {
719                    Ok(Ok(())) => {}
720                    Ok(Err(e)) => return Ok(Err(e)),
721                    Err(e) => return Err(e),
722                };
723            }
724            Ok(Ok(()))
725        }
726        FlakinessControl::RetryKnownFlaky(max_retries) => {
727            let mut tries = 1;
728            loop {
729                let start = Instant::now();
730                let result = test(start).await;
731
732                if result.is_err() && tries < *max_retries {
733                    tries += 1;
734                    output.repeat_running_test(
735                        test_description,
736                        idx,
737                        count,
738                        tries,
739                        *max_retries,
740                        "because test is known to be flaky",
741                    );
742                } else {
743                    break result;
744                }
745            }
746        }
747    }
748}
749
750#[allow(clippy::too_many_arguments)]
751async fn run_test(
752    output: Arc<dyn TestRunnerOutput>,
753    idx: usize,
754    count: usize,
755    nocapture: bool,
756    include_ignored: bool,
757    ensure_time: Option<TimeThreshold>,
758    dependency_view: Arc<dyn internal::DependencyView + Send + Sync>,
759    test: &RegisteredTest,
760    worker: &mut Option<Worker>,
761) -> TestResult {
762    if test.props.is_ignored && !include_ignored {
763        TestResult::ignored()
764    } else if let Some(worker) = worker.as_mut() {
765        worker.run_test(nocapture, test).await
766    } else {
767        let start = Instant::now();
768        let test = test.clone();
769        match &test.run {
770            TestFunction::Sync(_) => {
771                let handle = spawn_blocking(move || {
772                    let test = test.clone();
773                    crate::sync::run_sync_test_function(
774                        output,
775                        &test,
776                        idx,
777                        count,
778                        ensure_time,
779                        dependency_view,
780                    )
781                });
782                handle.await.unwrap_or_else(|join_error| {
783                    TestResult::failed(
784                        start.elapsed(),
785                        FailureCause::HarnessError(format!(
786                            "Failed joining test task: {join_error}"
787                        )),
788                    )
789                })
790            }
791            TestFunction::Async(test_fn) => {
792                let timeout = test.props.timeout;
793                let test_fn = test_fn.clone();
794                let detached_panic_policy = test.props.detached_panic_policy.clone();
795                let result = run_with_flakiness_control(output, &test, idx, count, |start| {
796                    let dependency_view = dependency_view.clone();
797                    let test_fn = test_fn.clone();
798                    Box::pin(async move {
799                        let test_id = crate::panic_hook::next_test_id();
800                        crate::panic_hook::set_current_test_id(test_id);
801                        crate::panic_hook::create_detached_collector(test_id);
802                        let result = AssertUnwindSafe(Box::pin(async move {
803                            match timeout {
804                                None => test_fn(dependency_view).await,
805                                Some(duration) => {
806                                    let result =
807                                        tokio::time::timeout(duration, test_fn(dependency_view))
808                                            .await;
809                                    match result {
810                                        Ok(result) => result,
811                                        Err(_) => {
812                                            return Err(FailureCause::HarnessError(
813                                                "Test timed out".to_string(),
814                                            ))
815                                        }
816                                    }
817                                }
818                            }
819                            .into_result()?;
820                            if let Some(ensure_time) = ensure_time {
821                                let elapsed = start.elapsed();
822                                if ensure_time.is_critical(&elapsed) {
823                                    return Err(FailureCause::HarnessError(format!(
824                                        "Test run time exceeds critical threshold: {elapsed:?}"
825                                    )));
826                                }
827                            }
828                            Ok(())
829                        }))
830                        .catch_unwind()
831                        .await;
832                        result
833                    })
834                })
835                .await;
836                let mut test_result =
837                    TestResult::from_result(&test.props.should_panic, start.elapsed(), result);
838                if let Some(test_id) = crate::panic_hook::current_test_id() {
839                    if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
840                        let panics = match collector.lock() {
841                            Ok(p) => p,
842                            Err(poisoned) => poisoned.into_inner(),
843                        };
844                        if !panics.is_empty()
845                            && detached_panic_policy == internal::DetachedPanicPolicy::FailTest
846                            && test_result.is_passed()
847                        {
848                            let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
849                            test_result = TestResult::failed(
850                                start.elapsed(),
851                                FailureCause::Panic(internal::PanicCause {
852                                    message: Some(format!(
853                                        "Detached task(s) panicked:\n{}",
854                                        messages.join("\n---\n")
855                                    )),
856                                    location: panics.first().and_then(|p| p.location.clone()),
857                                    backtrace: panics.first().and_then(|p| p.backtrace.clone()),
858                                }),
859                            );
860                        }
861                    }
862                }
863                crate::panic_hook::clear_current_test_id();
864                test_result
865            }
866            TestFunction::SyncBench(_) => {
867                let handle = spawn_blocking(move || {
868                    let test = test.clone();
869                    crate::sync::run_sync_test_function(
870                        output,
871                        &test,
872                        idx,
873                        count,
874                        ensure_time,
875                        dependency_view,
876                    )
877                });
878                handle.await.unwrap_or_else(|join_error| {
879                    TestResult::failed(
880                        start.elapsed(),
881                        FailureCause::HarnessError(format!(
882                            "Failed joining test task: {join_error}"
883                        )),
884                    )
885                })
886            }
887            TestFunction::AsyncBench(bench_fn) => {
888                let mut bencher = AsyncBencher::new();
889                let test_id = crate::panic_hook::next_test_id();
890                crate::panic_hook::set_current_test_id(test_id);
891                let result = AssertUnwindSafe(async move {
892                    bench_fn(&mut bencher, dependency_view).await;
893                    (
894                        bencher
895                            .summary()
896                            .expect("iter() was not called in bench function"),
897                        bencher.bytes,
898                    )
899                })
900                .catch_unwind()
901                .await;
902                let bytes = result.as_ref().map(|(_, bytes)| *bytes).unwrap_or_default();
903                let test_result = TestResult::from_summary(
904                    &test.props.should_panic,
905                    start.elapsed(),
906                    result.map(|(summary, _)| summary),
907                    bytes,
908                );
909                crate::panic_hook::clear_current_test_id();
910                test_result
911            }
912        }
913    }
914}
915
916struct Worker {
917    _listener: Listener,
918    _process: Child,
919    _out_handle: JoinHandle<()>,
920    _err_handle: JoinHandle<()>,
921    out_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
922    err_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
923    capture_enabled: Arc<Mutex<bool>>,
924    connection: Stream,
925    /// Parent-held HostedRpc owner cells keyed by fully-qualified dep id. Used
926    /// to dispatch incoming `IpcResponse::HostedRpcCall` frames from the worker
927    /// subprocess back to the right owner.
928    hosted_rpc_owner_cells: Arc<HashMap<String, Arc<HostedRpcOwnerCell>>>,
929}
930
931impl Worker {
932    /// Installs the parent-side map of HostedRpc owner cells so this worker can
933    /// route incoming `IpcResponse::HostedRpcCall` frames to the right
934    /// `HostedRpcOwnerCell` while waiting for a worker subprocess response.
935    fn set_hosted_rpc_owner_cells(&mut self, cells: Arc<HashMap<String, Arc<HostedRpcOwnerCell>>>) {
936        self.hosted_rpc_owner_cells = cells;
937    }
938
939    /// Parent-side dispatcher for a single `IpcResponse::HostedRpcCall`. Looks
940    /// up the owner cell by fully-qualified dep id, runs the dispatch on the
941    /// parent's stored owner, and writes the matching
942    /// `IpcCommand::HostedRpcReply` back to the worker subprocess. Mirrors
943    /// `sync::Worker::handle_hosted_rpc_call`.
944    async fn handle_hosted_rpc_call(
945        &mut self,
946        dump_on_ipc_failure: &DumpOnFailure,
947        request_id: u64,
948        dep_id: String,
949        method_idx: u32,
950        args_bytes: Vec<u8>,
951    ) {
952        let body = match self.hosted_rpc_owner_cells.get(&dep_id) {
953            // Use the async dispatch entry point so an owner that implements
954            // `AsyncHostedRpcDep` directly can `.await` inside its dispatcher
955            // without blocking the tokio runtime. Sync owners reach this
956            // entry point through the blanket bridge and their dispatched
957            // future resolves immediately.
958            Some(cell) => match cell.dispatch_async(method_idx, &args_bytes).await {
959                Ok(result_bytes) => HostedRpcReplyBody::Ok { result_bytes },
960                Err(message) => HostedRpcReplyBody::Err { message },
961            },
962            None => HostedRpcReplyBody::Err {
963                message: format!(
964                    "HostedRpc dispatch: unknown dep id '{dep_id}' in parent owner-cell map"
965                ),
966            },
967        };
968        let reply = IpcCommand::HostedRpcReply { request_id, body };
969        let msg = serialize_to_byte_vec(&reply).expect("Failed to encode HostedRpcReply");
970        dump_on_ipc_failure
971            .run(write_frame_async(&mut self.connection, &msg).await)
972            .await;
973    }
974
975    pub async fn run_test(&mut self, nocapture: bool, test: &RegisteredTest) -> TestResult {
976        let mut capture_enabled = self.capture_enabled.lock().await;
977        *capture_enabled = test.props.capture_control.requires_capturing(!nocapture);
978        drop(capture_enabled);
979
980        // Send IPC command and wait for IPC response, and in the meantime read from the stdout/stderr channels
981        let cmd = IpcCommand::RunTest {
982            name: test.name.clone(),
983            crate_name: test.crate_name.clone(),
984            module_path: test.module_path.clone(),
985        };
986
987        let dump_on_ipc_failure = self.dump_on_failure();
988
989        let msg = serialize_to_byte_vec(&cmd).expect("Failed to encode IPC command");
990        dump_on_ipc_failure
991            .run(write_frame_async(&mut self.connection, &msg).await)
992            .await;
993
994        let response = loop {
995            let response_bytes = dump_on_ipc_failure
996                .run(read_frame_async(&mut self.connection).await)
997                .await;
998            let response: IpcResponse = dump_on_ipc_failure.run(deserialize(&response_bytes)).await;
999            match response {
1000                IpcResponse::TestFinished { .. } => break response,
1001                IpcResponse::CloneableAccepted { .. }
1002                | IpcResponse::HostedDescriptorAccepted { .. } => continue,
1003                IpcResponse::HostedRpcCall {
1004                    request_id,
1005                    dep_id,
1006                    method_idx,
1007                    args_bytes,
1008                } => {
1009                    self.handle_hosted_rpc_call(
1010                        &dump_on_ipc_failure,
1011                        request_id,
1012                        dep_id,
1013                        method_idx,
1014                        args_bytes,
1015                    )
1016                    .await;
1017                    continue;
1018                }
1019            }
1020        };
1021
1022        let IpcResponse::TestFinished {
1023            result,
1024            finish_marker,
1025        } = response
1026        else {
1027            unreachable!("loop only breaks on TestFinished")
1028        };
1029
1030        if test.props.capture_control.requires_capturing(!nocapture) {
1031            let out_lines: Vec<_> =
1032                Self::drain_until(self.out_lines.clone(), finish_marker.clone()).await;
1033            let err_lines: Vec<_> =
1034                Self::drain_until(self.err_lines.clone(), finish_marker.clone()).await;
1035            result.into_test_result(out_lines, err_lines)
1036        } else {
1037            result.into_test_result(Vec::new(), Vec::new())
1038        }
1039    }
1040
1041    /// Async counterpart to `sync::Worker::provide_cloneable`. `dep_id` is the
1042    /// dep's fully-qualified id (`{crate}::{module}::{name}`).
1043    async fn provide_cloneable(&mut self, dep_id: String, wire_bytes: Vec<u8>) {
1044        let dump_on_ipc_failure = self.dump_on_failure();
1045        let cmd = IpcCommand::ProvideCloneable {
1046            dep_id: dep_id.clone(),
1047            wire_bytes,
1048        };
1049        let msg = serialize_to_byte_vec(&cmd).expect("Failed to encode IPC command");
1050        dump_on_ipc_failure
1051            .run(write_frame_async(&mut self.connection, &msg).await)
1052            .await;
1053
1054        loop {
1055            let response_bytes = dump_on_ipc_failure
1056                .run(read_frame_async(&mut self.connection).await)
1057                .await;
1058            let response: IpcResponse = dump_on_ipc_failure.run(deserialize(&response_bytes)).await;
1059            match response {
1060                IpcResponse::CloneableAccepted { dep_id: ack_id } => {
1061                    if ack_id == dep_id {
1062                        return;
1063                    }
1064                }
1065                IpcResponse::HostedDescriptorAccepted { .. } => {
1066                    // Out-of-band ack from a previous ProvideHostedDescriptor; ignore.
1067                }
1068                IpcResponse::TestFinished { .. } => {
1069                    // Should not happen before any RunTest.
1070                }
1071                IpcResponse::HostedRpcCall {
1072                    request_id,
1073                    dep_id: rpc_dep_id,
1074                    method_idx,
1075                    args_bytes,
1076                } => {
1077                    // A worker subprocess can issue a HostedRpc call from
1078                    // inside an in-progress test, even while the parent is
1079                    // mid-`ProvideCloneable` for a different dep. Dispatch it
1080                    // so the protocol doesn't desync.
1081                    self.handle_hosted_rpc_call(
1082                        &dump_on_ipc_failure,
1083                        request_id,
1084                        rpc_dep_id,
1085                        method_idx,
1086                        args_bytes,
1087                    )
1088                    .await;
1089                }
1090            }
1091        }
1092    }
1093
1094    /// Async counterpart to `sync::Worker::provide_hosted_descriptor`.
1095    async fn provide_hosted_descriptor(&mut self, dep_id: String, wire_bytes: Vec<u8>) {
1096        let dump_on_ipc_failure = self.dump_on_failure();
1097        let cmd = IpcCommand::ProvideHostedDescriptor {
1098            dep_id: dep_id.clone(),
1099            wire_bytes,
1100        };
1101        let msg = serialize_to_byte_vec(&cmd).expect("Failed to encode IPC command");
1102        dump_on_ipc_failure
1103            .run(write_frame_async(&mut self.connection, &msg).await)
1104            .await;
1105
1106        loop {
1107            let response_bytes = dump_on_ipc_failure
1108                .run(read_frame_async(&mut self.connection).await)
1109                .await;
1110            let response: IpcResponse = dump_on_ipc_failure.run(deserialize(&response_bytes)).await;
1111            match response {
1112                IpcResponse::HostedDescriptorAccepted { dep_id: ack_id } => {
1113                    if ack_id == dep_id {
1114                        return;
1115                    }
1116                }
1117                IpcResponse::CloneableAccepted { .. } => {
1118                    // Out-of-band ack from a previous ProvideCloneable; ignore.
1119                }
1120                IpcResponse::TestFinished { .. } => {
1121                    // Should not happen before any RunTest.
1122                }
1123                IpcResponse::HostedRpcCall {
1124                    request_id,
1125                    dep_id: rpc_dep_id,
1126                    method_idx,
1127                    args_bytes,
1128                } => {
1129                    // See provide_cloneable arm. Dispatch the call inline so
1130                    // the IPC stream stays in sync.
1131                    self.handle_hosted_rpc_call(
1132                        &dump_on_ipc_failure,
1133                        request_id,
1134                        rpc_dep_id,
1135                        method_idx,
1136                        args_bytes,
1137                    )
1138                    .await;
1139                }
1140            }
1141        }
1142    }
1143
1144    fn dump_on_failure(&self) -> DumpOnFailure {
1145        DumpOnFailure {
1146            out_lines: self.out_lines.clone(),
1147            err_lines: self.err_lines.clone(),
1148        }
1149    }
1150
1151    async fn drain_until(
1152        source: Arc<Mutex<VecDeque<CapturedOutput>>>,
1153        finish_marker: String,
1154    ) -> Vec<CapturedOutput> {
1155        let mut result = Vec::new();
1156        loop {
1157            let mut source = source.lock().await;
1158            while let Some(line) = source.pop_front() {
1159                if line.line() == finish_marker {
1160                    return result;
1161                } else {
1162                    result.push(line.clone());
1163                }
1164            }
1165            drop(source);
1166
1167            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1168        }
1169    }
1170}
1171
1172struct DumpOnFailure {
1173    out_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
1174    err_lines: Arc<Mutex<VecDeque<CapturedOutput>>>,
1175}
1176
1177impl DumpOnFailure {
1178    pub async fn run<T, E>(&self, result: Result<T, E>) -> T {
1179        match result {
1180            Ok(value) => value,
1181            Err(_error) => {
1182                let out_lines: Vec<_> = self.out_lines.lock().await.drain(..).collect();
1183                let err_lines: Vec<_> = self.err_lines.lock().await.drain(..).collect();
1184                let mut all_lines = [out_lines, err_lines].concat();
1185                all_lines.sort();
1186
1187                // Route the diagnostic through the real terminal even
1188                // when host capture has fd 2 redirected into its pipe.
1189                // We `process::exit(1)` immediately after this and
1190                // never reach `host_capture.finalize()`, so anything we
1191                // wrote into the host pipe would be lost. The capture
1192                // pipe and its reader are abandoned on exit; that is
1193                // acceptable for this fatal-IPC path.
1194                use std::io::Write;
1195                let mut err = crate::host_capture::TerminalStderr;
1196                for line in all_lines {
1197                    let _ = writeln!(err, "{}", line.line());
1198                }
1199                let _ = err.flush();
1200
1201                std::process::exit(1);
1202            }
1203        }
1204    }
1205}
1206
1207async fn spawn_worker_if_needed(args: &Arguments) -> Option<Worker> {
1208    if args.spawn_workers {
1209        let id = Uuid::new_v4();
1210        let name_str = format!("{id}.sock");
1211        let name = name_str
1212            .clone()
1213            .to_ns_name::<GenericNamespaced>()
1214            .expect("Invalid local socket name");
1215        let opts = ListenerOptions::new().name(name.clone());
1216        let listener = opts
1217            .create_tokio()
1218            .expect("Failed to create local socket listener");
1219
1220        let exe = std::env::current_exe().expect("Failed to get current executable path");
1221
1222        let mut args = args.clone();
1223        args.ipc = Some(name_str);
1224        args.spawn_workers = false;
1225        args.logfile = None;
1226        let args = args.to_args();
1227
1228        let mut process = Command::new(exe)
1229            .args(args)
1230            .stdin(Stdio::inherit())
1231            .stderr(Stdio::piped())
1232            .stdout(Stdio::piped())
1233            .spawn()
1234            .expect("Failed to spawn worker process");
1235
1236        let stdout = process.stdout.take().unwrap();
1237        let stderr = process.stderr.take().unwrap();
1238
1239        let out_lines = Arc::new(Mutex::new(VecDeque::new()));
1240        let err_lines = Arc::new(Mutex::new(VecDeque::new()));
1241        let capture_enabled = Arc::new(Mutex::new(true));
1242
1243        let out_lines_clone = out_lines.clone();
1244        let capture_enabled_clone = capture_enabled.clone();
1245        let out_handle = spawn(async move {
1246            let reader = BufReader::new(stdout);
1247            let mut lines = reader.lines();
1248            while let Some(line) = lines
1249                .next_line()
1250                .await
1251                .expect("Failed to read from worker stdout")
1252            {
1253                if *capture_enabled_clone.lock().await {
1254                    out_lines_clone
1255                        .lock()
1256                        .await
1257                        .push_back(CapturedOutput::stdout(line));
1258                } else {
1259                    // `#[never_capture]` pass-through: write to the real
1260                    // terminal even when host capture has redirected
1261                    // fd 1 into its pipe, so the worker line stays
1262                    // uncaptured live output and is not later
1263                    // re-labelled `[host]`.
1264                    use std::io::Write;
1265                    let mut out = crate::host_capture::TerminalStdout;
1266                    let _ = writeln!(out, "{line}");
1267                    let _ = out.flush();
1268                }
1269            }
1270        });
1271
1272        let err_lines_clone = err_lines.clone();
1273        let capture_enabled_clone = capture_enabled.clone();
1274        let err_handle = spawn(async move {
1275            let reader = BufReader::new(stderr);
1276            let mut lines = reader.lines();
1277            while let Some(line) = lines
1278                .next_line()
1279                .await
1280                .expect("Failed to read from worker stderr")
1281            {
1282                if *capture_enabled_clone.lock().await {
1283                    err_lines_clone
1284                        .lock()
1285                        .await
1286                        .push_back(CapturedOutput::stderr(line));
1287                } else {
1288                    // Same as the stdout pass-through above: route the
1289                    // never-captured worker line to the real terminal
1290                    // stderr, not the host capture pipe.
1291                    use std::io::Write;
1292                    let mut err = crate::host_capture::TerminalStderr;
1293                    let _ = writeln!(err, "{line}");
1294                    let _ = err.flush();
1295                }
1296            }
1297        });
1298
1299        let connection = listener
1300            .accept()
1301            .await
1302            .expect("Failed to accept connection");
1303
1304        Some(Worker {
1305            _listener: listener,
1306            _process: process,
1307            _out_handle: out_handle,
1308            _err_handle: err_handle,
1309            out_lines,
1310            err_lines,
1311            connection,
1312            capture_enabled,
1313            hosted_rpc_owner_cells: Arc::new(HashMap::new()),
1314        })
1315    } else {
1316        None
1317    }
1318}
1319
1320/// Parent-side `--nocapture` / no-spawn-workers helper. Builds one stub per
1321/// HostedRpc dep using an [`InProcessHostedRpcTransport`] that points at the
1322/// parent-held owner cells, and stashes it in the execution tree so dependency
1323/// materialisation skips the owner-only constructor. Mirrors
1324/// `sync::install_local_hosted_rpc_stubs`.
1325fn install_local_hosted_rpc_stubs(
1326    execution: &mut TestSuiteExecution,
1327    rpc_factories: &HashMap<String, RpcFactory>,
1328    owner_cells: &HashMap<String, Arc<HostedRpcOwnerCell>>,
1329) {
1330    let transport: Arc<dyn HostedRpcTransport> =
1331        Arc::new(InProcessHostedRpcTransport::new(owner_cells.clone()));
1332    for (dep_id, factory) in rpc_factories.iter() {
1333        if !owner_cells.contains_key(dep_id) {
1334            // No owner cell materialised for this dep (e.g. registered
1335            // globally but not pulled into the current filter). Skip so
1336            // we don't install a stub nothing routes.
1337            continue;
1338        }
1339        let channel = HostedRpcChannel::new(dep_id.clone(), transport.clone());
1340        let stub = (factory.build_stub)(channel);
1341        let applied = execution.provide_cloneable_value(dep_id, stub);
1342        if !applied {
1343            // The owner cell can be materialised solely because another
1344            // parent-side Cloneable/Hosted/HostedRpc dependency needs this
1345            // HostedRpc dep as a constructor input. In that case the stub is
1346            // intentionally not present in the worker execution tree; there is
1347            // nothing to pre-populate for no-spawn test execution.
1348            continue;
1349        }
1350    }
1351}
1352
1353/// Worker subprocess helper. Builds one stub per registered HostedRpc dep backed
1354/// by [`IpcHostedRpcTransport`], and installs it in the execution tree. Mirrors
1355/// `sync::install_worker_subprocess_hosted_rpc_stubs` but runs on the tokio
1356/// `Arc<Mutex<Stream>>` connection.
1357async fn install_worker_subprocess_hosted_rpc_stubs(
1358    execution: &Arc<Mutex<TestSuiteExecution>>,
1359    rpc_factories: &HashMap<String, RpcFactory>,
1360    connection_arc: Arc<Mutex<Stream>>,
1361) {
1362    let transport: Arc<dyn HostedRpcTransport> =
1363        Arc::new(IpcHostedRpcTransport::new(connection_arc));
1364    for (dep_id, factory) in rpc_factories.iter() {
1365        let channel = HostedRpcChannel::new(dep_id.clone(), transport.clone());
1366        let stub = (factory.build_stub)(channel);
1367        let mut execution = execution.lock().await;
1368        let applied = execution.provide_cloneable_value(dep_id, stub);
1369        // Not every binary registers a HostedRpc dep that the current
1370        // execution actually uses; if so, just move on.
1371        let _ = applied;
1372    }
1373}
1374
1375/// Worker subprocess `HostedRpcTransport` for the tokio runner.
1376/// Mirrors `sync::IpcHostedRpcTransport` but bridges a sync trait method
1377/// to the async tokio IPC primitives via
1378/// `tokio::task::block_in_place` + `Handle::current().block_on(...)`.
1379///
1380/// The shared `Arc<Mutex<Stream>>` is the same one used by the
1381/// worker subprocess's main IPC loop; the lock guarantees that a stub
1382/// call and the main loop never interleave a half-written frame. Calls
1383/// serialise across all in-flight stubs by acquiring the mutex for the
1384/// full request-then-reply round trip.
1385///
1386/// This relies on the temporal invariant documented on
1387/// [`crate::internal::HostedRpcChannel::call`]: stubs are only invoked
1388/// from inside a running test body, never from `build_stub`, and never
1389/// from detached background work that outlives the test. Under those
1390/// rules the main IPC loop is only reading between tests, so it cannot
1391/// race with a stub call.
1392struct IpcHostedRpcTransport {
1393    connection: Arc<Mutex<Stream>>,
1394    next_request_id: AtomicU64,
1395}
1396
1397impl IpcHostedRpcTransport {
1398    fn new(connection: Arc<Mutex<Stream>>) -> Self {
1399        Self {
1400            connection,
1401            next_request_id: AtomicU64::new(0),
1402        }
1403    }
1404}
1405
1406impl HostedRpcTransport for IpcHostedRpcTransport {
1407    fn call(
1408        &self,
1409        dep_id: &str,
1410        method_idx: u32,
1411        args: Vec<u8>,
1412    ) -> Result<Vec<u8>, HostedRpcError> {
1413        let request_id = self.next_request_id.fetch_add(1, Ordering::SeqCst);
1414        let call = IpcResponse::HostedRpcCall {
1415            request_id,
1416            dep_id: dep_id.to_string(),
1417            method_idx,
1418            args_bytes: args,
1419        };
1420        let msg = serialize_to_byte_vec(&call).map_err(|e| {
1421            HostedRpcError::Transport(format!("encode HostedRpcCall failed: {e:?}"))
1422        })?;
1423
1424        let connection = self.connection.clone();
1425        let handle = tokio::runtime::Handle::current();
1426
1427        // Run the async I/O round-trip from inside this sync trait
1428        // method. `block_in_place` releases this worker thread back to
1429        // the scheduler so the parent's read loop can continue making
1430        // progress on other tasks while we wait for the reply.
1431        tokio::task::block_in_place(move || {
1432            handle.block_on(async move {
1433                let mut conn = connection.lock().await;
1434                write_frame_async(&mut *conn, &msg).await.map_err(|e| {
1435                    HostedRpcError::Transport(format!("write HostedRpcCall failed: {e:?}"))
1436                })?;
1437                let reply_bytes = read_frame_async(&mut *conn).await.map_err(|e| {
1438                    HostedRpcError::Transport(format!("read HostedRpcReply failed: {e:?}"))
1439                })?;
1440                let command: IpcCommand = deserialize(&reply_bytes).map_err(|e| {
1441                    HostedRpcError::Transport(format!("decode HostedRpcReply failed: {e:?}"))
1442                })?;
1443                match command {
1444                    IpcCommand::HostedRpcReply {
1445                        request_id: reply_id,
1446                        body,
1447                    } => {
1448                        if reply_id != request_id {
1449                            return Err(HostedRpcError::Transport(format!(
1450                                "HostedRpcReply request_id mismatch: expected {request_id}, got {reply_id}"
1451                            )));
1452                        }
1453                        match body {
1454                            HostedRpcReplyBody::Ok { result_bytes } => Ok(result_bytes),
1455                            HostedRpcReplyBody::Err { message } => {
1456                                Err(HostedRpcError::Dispatch(message))
1457                            }
1458                        }
1459                    }
1460                    other => Err(HostedRpcError::Transport(format!(
1461                        "unexpected IpcCommand while waiting for HostedRpcReply: {other:?}"
1462                    ))),
1463                }
1464            })
1465        })
1466    }
1467}