Skip to main content

test_r_core/
internal.rs

1use crate::args::{Arguments, TimeThreshold};
2use crate::bench::Bencher;
3use crate::stats::Summary;
4use std::any::{Any, TypeId};
5use std::backtrace::Backtrace;
6use std::cmp::{max, Ordering};
7use std::collections::HashMap;
8use std::fmt::{Debug, Display, Formatter};
9use std::future::Future;
10use std::hash::Hash;
11use std::pin::Pin;
12use std::process::ExitCode;
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, SystemTime};
15
16#[derive(Clone)]
17#[allow(clippy::type_complexity)]
18pub enum TestFunction {
19    Sync(
20        Arc<
21            dyn Fn(Arc<dyn DependencyView + Send + Sync>) -> Box<dyn TestReturnValue>
22                + Send
23                + Sync
24                + 'static,
25        >,
26    ),
27    SyncBench(
28        Arc<dyn Fn(&mut Bencher, Arc<dyn DependencyView + Send + Sync>) + Send + Sync + 'static>,
29    ),
30    #[cfg(feature = "tokio")]
31    Async(
32        Arc<
33            dyn (Fn(
34                    Arc<dyn DependencyView + Send + Sync>,
35                ) -> Pin<Box<dyn Future<Output = Box<dyn TestReturnValue>>>>)
36                + Send
37                + Sync
38                + 'static,
39        >,
40    ),
41    #[cfg(feature = "tokio")]
42    AsyncBench(
43        Arc<
44            dyn for<'a> Fn(
45                    &'a mut crate::bench::AsyncBencher,
46                    Arc<dyn DependencyView + Send + Sync>,
47                ) -> Pin<Box<dyn Future<Output = ()> + 'a>>
48                + Send
49                + Sync
50                + 'static,
51        >,
52    ),
53}
54
55impl TestFunction {
56    #[cfg(not(feature = "tokio"))]
57    pub fn is_bench(&self) -> bool {
58        matches!(self, TestFunction::SyncBench(_))
59    }
60
61    #[cfg(feature = "tokio")]
62    pub fn is_bench(&self) -> bool {
63        matches!(
64            self,
65            TestFunction::SyncBench(_) | TestFunction::AsyncBench(_)
66        )
67    }
68}
69
70pub trait TestReturnValue {
71    fn into_result(self: Box<Self>) -> Result<(), FailureCause>;
72}
73
74impl TestReturnValue for () {
75    fn into_result(self: Box<Self>) -> Result<(), FailureCause> {
76        Ok(())
77    }
78}
79
80impl<T, E: Display + Debug + Send + Sync + 'static> TestReturnValue for Result<T, E> {
81    fn into_result(self: Box<Self>) -> Result<(), FailureCause> {
82        match *self {
83            Ok(_) => Ok(()),
84            Err(e) => Err(FailureCause::from_error(e)),
85        }
86    }
87}
88
89#[derive(Clone)]
90pub enum FailureCause {
91    /// Test returned Err(e) where E: Display + Debug — stores both representations
92    /// and the original error value for later downcasting
93    ReturnedError {
94        display: String,
95        debug: String,
96        prefer_debug: bool,
97        error: Arc<dyn Any + Send + Sync>,
98    },
99    /// Test returned Err(String) — stored as raw string without formatting
100    ReturnedMessage(String),
101    /// Test panicked
102    Panic(PanicCause),
103    /// Framework error (join failure, timeout, IPC deserialization, etc.)
104    HarnessError(String),
105}
106
107#[derive(Debug, Clone)]
108pub struct PanicCause {
109    pub message: Option<String>,
110    pub location: Option<PanicLocation>,
111    pub backtrace: Option<Arc<Backtrace>>,
112}
113
114#[derive(Debug, Clone)]
115pub struct PanicLocation {
116    pub file: String,
117    pub line: u32,
118    pub column: u32,
119}
120
121impl std::fmt::Debug for FailureCause {
122    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
123        match self {
124            FailureCause::ReturnedError { display, .. } => {
125                f.debug_tuple("ReturnedError").field(display).finish()
126            }
127            FailureCause::ReturnedMessage(s) => f.debug_tuple("ReturnedMessage").field(s).finish(),
128            FailureCause::Panic(p) => f.debug_tuple("Panic").field(p).finish(),
129            FailureCause::HarnessError(s) => f.debug_tuple("HarnessError").field(s).finish(),
130        }
131    }
132}
133
134impl FailureCause {
135    pub fn from_error<E: Display + Debug + Send + Sync + 'static>(e: E) -> Self {
136        if TypeId::of::<E>() == TypeId::of::<String>() {
137            let any: Box<dyn Any + Send + Sync> = Box::new(e);
138            return FailureCause::ReturnedMessage(*any.downcast::<String>().unwrap());
139        }
140
141        let mut _prefer_debug = false;
142        #[cfg(feature = "anyhow")]
143        {
144            _prefer_debug = TypeId::of::<E>() == TypeId::of::<anyhow::Error>();
145        }
146
147        FailureCause::ReturnedError {
148            display: format!("{e:#}"),
149            debug: format!("{e:?}"),
150            prefer_debug: _prefer_debug,
151            error: Arc::new(e),
152        }
153    }
154
155    pub fn render(&self) -> String {
156        match self {
157            FailureCause::ReturnedError {
158                display,
159                debug,
160                prefer_debug,
161                ..
162            } => {
163                if *prefer_debug {
164                    debug.clone()
165                } else {
166                    display.clone()
167                }
168            }
169            FailureCause::ReturnedMessage(s) => s.clone(),
170            FailureCause::Panic(p) => p.render(),
171            FailureCause::HarnessError(s) => s.clone(),
172        }
173    }
174
175    /// Get the message string for ShouldPanic matching (without backtrace)
176    pub fn panic_message(&self) -> Option<&str> {
177        match self {
178            FailureCause::Panic(p) => p.message.as_deref(),
179            _ => None,
180        }
181    }
182}
183
184impl PanicCause {
185    pub fn render(&self) -> String {
186        let mut out = self.message.clone().unwrap_or_default();
187        if let Some(loc) = &self.location {
188            out.push_str(&format!("\n  at {}:{}:{}", loc.file, loc.line, loc.column));
189        }
190        if let Some(bt) = &self.backtrace {
191            let bt_str = format!("{bt}");
192            if !bt_str.is_empty() && bt_str != "disabled backtrace" {
193                out.push_str(&format!("\n\nStack backtrace:\n{bt}"));
194            }
195        }
196        out
197    }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum ShouldPanic {
202    No,
203    Yes,
204    WithMessage(String),
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum TestType {
209    UnitTest,
210    IntegrationTest,
211}
212
213impl TestType {
214    pub fn from_path(path: &str) -> Self {
215        if path.contains("/src/") {
216            TestType::UnitTest
217        } else {
218            TestType::IntegrationTest
219        }
220    }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum FlakinessControl {
225    None,
226    ProveNonFlaky(usize),
227    RetryKnownFlaky(usize),
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub enum DetachedPanicPolicy {
232    FailTest,
233    Ignore,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub enum CaptureControl {
238    Default,
239    AlwaysCapture,
240    NeverCapture,
241}
242
243impl CaptureControl {
244    pub fn requires_capturing(&self, default: bool) -> bool {
245        match self {
246            CaptureControl::Default => default,
247            CaptureControl::AlwaysCapture => true,
248            CaptureControl::NeverCapture => false,
249        }
250    }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub enum ReportTimeControl {
255    Default,
256    Enabled,
257    Disabled,
258}
259
260#[derive(Clone)]
261pub struct TestProperties {
262    pub should_panic: ShouldPanic,
263    pub test_type: TestType,
264    pub timeout: Option<Duration>,
265    pub flakiness_control: FlakinessControl,
266    pub capture_control: CaptureControl,
267    pub report_time_control: ReportTimeControl,
268    pub ensure_time_control: ReportTimeControl,
269    pub tags: Vec<String>,
270    pub is_ignored: bool,
271    pub detached_panic_policy: DetachedPanicPolicy,
272}
273
274impl TestProperties {
275    pub fn unit_test() -> Self {
276        TestProperties {
277            test_type: TestType::UnitTest,
278            ..Default::default()
279        }
280    }
281
282    pub fn integration_test() -> Self {
283        TestProperties {
284            test_type: TestType::IntegrationTest,
285            ..Default::default()
286        }
287    }
288}
289
290impl Default for TestProperties {
291    fn default() -> Self {
292        Self {
293            should_panic: ShouldPanic::No,
294            test_type: TestType::UnitTest,
295            timeout: None,
296            flakiness_control: FlakinessControl::None,
297            capture_control: CaptureControl::Default,
298            report_time_control: ReportTimeControl::Default,
299            ensure_time_control: ReportTimeControl::Default,
300            tags: Vec::new(),
301            is_ignored: false,
302            detached_panic_policy: DetachedPanicPolicy::FailTest,
303        }
304    }
305}
306
307#[derive(Clone)]
308pub struct RegisteredTest {
309    pub name: String,
310    pub crate_name: String,
311    pub module_path: String,
312    pub run: TestFunction,
313    pub props: TestProperties,
314    pub dependencies: Option<Vec<String>>,
315}
316
317impl RegisteredTest {
318    pub fn filterable_name(&self) -> String {
319        if !self.module_path.is_empty() {
320            format!("{}::{}", self.module_path, self.name)
321        } else {
322            self.name.clone()
323        }
324    }
325
326    pub fn fully_qualified_name(&self) -> String {
327        [&self.crate_name, &self.module_path, &self.name]
328            .into_iter()
329            .filter(|s| !s.is_empty())
330            .cloned()
331            .collect::<Vec<String>>()
332            .join("::")
333    }
334
335    pub fn crate_and_module(&self) -> String {
336        [&self.crate_name, &self.module_path]
337            .into_iter()
338            .filter(|s| !s.is_empty())
339            .cloned()
340            .collect::<Vec<String>>()
341            .join("::")
342    }
343}
344
345impl Debug for RegisteredTest {
346    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
347        f.debug_struct("RegisteredTest")
348            .field("name", &self.name)
349            .field("crate_name", &self.crate_name)
350            .field("module_path", &self.module_path)
351            .finish()
352    }
353}
354
355pub static REGISTERED_TESTS: Mutex<Vec<RegisteredTest>> = Mutex::new(Vec::new());
356
357#[derive(Clone)]
358#[allow(clippy::type_complexity)]
359pub enum DependencyConstructor {
360    Sync(
361        Arc<
362            dyn (Fn(Arc<dyn DependencyView + Send + Sync>) -> Arc<dyn Any + Send + Sync + 'static>)
363                + Send
364                + Sync
365                + 'static,
366        >,
367    ),
368    Async(
369        Arc<
370            dyn (Fn(
371                    Arc<dyn DependencyView + Send + Sync>,
372                ) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
373                + Send
374                + Sync
375                + 'static,
376        >,
377    ),
378}
379
380/// User-facing trait that opts a dependency value into the `Cloneable`
381/// sharing strategy. The parent calls [`to_wire`](CloneableDep::to_wire) once
382/// and ships the bytes to each worker via IPC. Each worker calls
383/// [`from_wire`](CloneableDep::from_wire) to reconstruct a local value.
384///
385/// The on-the-wire encoding is entirely up to the implementor: `serde_json`,
386/// `bincode`, `postcard`, a hand-rolled binary format, an on-disk file path,
387/// etc. The bytes are treated as opaque by the runner.
388///
389/// The simple `Self`-returning `from_wire` covers Cloneable deps that need no
390/// other worker-local context. If reconstruction needs worker-local state (for
391/// example, a per-worker engine), model that state as a separate dependency and
392/// combine the two from the test or a higher-level helper.
393pub trait CloneableDep: Sized + Send + Sync + 'static {
394    /// Serialise this value into wire bytes for transmission to workers.
395    fn to_wire(&self) -> Vec<u8>;
396
397    /// Reconstruct a value from wire bytes received from the parent.
398    fn from_wire(bytes: &[u8]) -> Self;
399}
400
401/// User-facing trait that opts a dependency value into the `Hosted`
402/// sharing strategy. Like [`CloneableDep`], but the owner instance lives in
403/// the **parent test runner process** for the entire suite. The parent runs
404/// the constructor once per Hosted dep and keeps the value alive until every
405/// worker has finished — useful for singleton services like an in-process
406/// TCP listener, a Docker container, an env-based test environment, or any
407/// long-running runtime that must not be duplicated across worker processes.
408///
409/// The parent calls [`descriptor`](HostedDep::descriptor) on its owner once
410/// and forwards the resulting bytes to every worker over IPC. Each worker
411/// reconstructs a local handle via
412/// [`from_descriptor`](HostedDep::from_descriptor) — the handle typically
413/// connects to the live owner held by the parent (e.g. opens a TCP
414/// connection to the address carried in the descriptor).
415///
416/// For descriptor-based Hosted deps, the owner and worker handle share the same
417/// type `Self`. The implementation is responsible for stashing owner-only
418/// state (sockets, background threads, etc.) in fields that the worker side
419/// won't touch.
420pub trait HostedDep: Sized + Send + Sync + 'static {
421    /// Owner-side: produce the descriptor bytes that workers will use to
422    /// reconstruct a connected handle.
423    fn descriptor(&self) -> Vec<u8>;
424
425    /// Worker-side: reconstruct a handle from descriptor bytes received from
426    /// the parent.
427    fn from_descriptor(bytes: &[u8]) -> Self;
428}
429
430/// Async counterpart of [`HostedDep`]. Implement this when worker-side
431/// reconstruction needs to `.await` (e.g. opening async network
432/// clients, doing async filesystem work, calling
433/// `Provided*::new(...).await` constructors).
434///
435/// No opt-in flag is required: the helper functions emitted by
436/// `#[test_dep(scope = Hosted)]` auto-select the async path under the `tokio`
437/// runtime, so simply implementing `AsyncHostedDep` is enough.
438///
439/// ```ignore
440/// #[test_dep(scope = Hosted)]
441/// async fn dependencies() -> EnvBasedTestDependencies { /* … */ }
442///
443/// impl test_r::core::AsyncHostedDep for EnvBasedTestDependencies {
444///     fn descriptor(&self) -> Vec<u8> { /* … */ }
445///
446///     async fn from_descriptor(bytes: &[u8]) -> Self { /* … */ }
447/// }
448/// ```
449///
450/// `AsyncHostedDep` is the tokio-only async counterpart of
451/// [`HostedDep`]. Under the `tokio` test runtime the Hosted helper
452/// functions emitted by `#[test_dep(scope = Hosted)]` always go
453/// through `AsyncHostedDep`, regardless of whether the user wrote
454/// `impl HostedDep` (covered by the blanket bridge below) or
455/// `impl AsyncHostedDep` directly. Under the sync runtime, the same
456/// helpers stay on `HostedDep`: a `scope = Hosted` registration that
457/// uses an async-only `AsyncHostedDep` type therefore fails to
458/// compile in sync builds. (Writing the impl on its own still compiles fine;
459/// only the `#[test_dep(scope = Hosted)]` registration fails.)
460///
461/// `descriptor()` stays synchronous and is called on the parent owner
462/// value, exactly as in [`HostedDep`]; the only difference is that
463/// `from_descriptor` returns a `Future` so worker-side reconstruction
464/// can await.
465///
466/// The legacy `async_worker` attribute is **deprecated** and ignored
467/// (it now only triggers a compile-time deprecation warning at the
468/// dep's registration site); remove it from any new code.
469pub trait AsyncHostedDep: Sized + Send + Sync + 'static {
470    /// Owner-side: produce the descriptor bytes that workers will use to
471    /// reconstruct a connected handle. Called from the parent runner
472    /// process exactly once per Hosted dep, just like
473    /// [`HostedDep::descriptor`].
474    fn descriptor(&self) -> Vec<u8>;
475
476    /// Worker-side: asynchronously reconstruct a handle from descriptor
477    /// bytes received from the parent.
478    fn from_descriptor(bytes: &[u8]) -> impl std::future::Future<Output = Self> + Send;
479}
480
481/// Blanket bridge: every [`HostedDep`] is automatically also an
482/// [`AsyncHostedDep`]. The bridged `from_descriptor` returns
483/// [`std::future::ready`], so the bridge itself adds only an immediately-ready
484/// future and the runtime can await all Hosted reconstruction uniformly.
485///
486/// This lets the test-r runtime drive **every** Hosted descriptor
487/// reconstruction through one async path under the `tokio` runtime, regardless
488/// of whether the dep's own implementation is sync (`impl HostedDep for ...`)
489/// or async (`impl AsyncHostedDep for ...`). The `async_worker` macro flag is
490/// unnecessary — the implementor picks sync vs async purely at the trait-impl
491/// call site.
492///
493/// **Cost:** the bridge itself is negligible (`ready(x).await` is an
494/// immediately-ready future); when the runtime later routes every
495/// Hosted reconstruction through the async path there is still a
496/// small async-wrapper overhead per worker-startup-per-dep (polling
497/// the outer future, and possibly one boxed-future allocation from
498/// `WorkerReconstructor::Async`), but no extra user-level async work.
499///
500/// **Coherence note:** on stable Rust, with this blanket impl in place
501/// a concrete type cannot manually implement both `HostedDep` and
502/// `AsyncHostedDep` — rustc rejects that as conflicting
503/// implementations. That compile-time error is the intended signal
504/// that one of the two manual impls is redundant and should be
505/// removed.
506///
507/// **Source-compat note:** if downstream code imports both
508/// `HostedDep` and `AsyncHostedDep` into the same scope and calls
509/// trait methods by method syntax (e.g.
510/// `MyType::from_descriptor(bytes)` or `dep.descriptor()`), the call
511/// can become ambiguous now that one type satisfies both traits.
512/// Resolve with UFCS — `<MyType as HostedDep>::from_descriptor(bytes)`.
513impl<T: HostedDep> AsyncHostedDep for T {
514    fn descriptor(&self) -> Vec<u8> {
515        <T as HostedDep>::descriptor(self)
516    }
517
518    fn from_descriptor(bytes: &[u8]) -> impl std::future::Future<Output = Self> + Send {
519        std::future::ready(<T as HostedDep>::from_descriptor(bytes))
520    }
521}
522
523#[cfg(test)]
524mod hosted_dep_blanket_bridge_tests {
525    use super::{AsyncHostedDep, HostedDep};
526    // Need `Future` in scope to call `.poll(...)` on the pinned future
527    // returned by the blanket-bridged `from_descriptor`.
528    use std::future::Future;
529
530    /// Test fixture: a Hosted dep that only implements the sync `HostedDep`
531    /// trait. The blanket bridge must also expose it through the async API.
532    #[derive(Debug, PartialEq, Eq)]
533    struct SyncOnlyDep {
534        bytes: Vec<u8>,
535    }
536
537    impl HostedDep for SyncOnlyDep {
538        fn descriptor(&self) -> Vec<u8> {
539            self.bytes.clone()
540        }
541
542        fn from_descriptor(bytes: &[u8]) -> Self {
543            Self {
544                bytes: bytes.to_vec(),
545            }
546        }
547    }
548
549    /// Compile-time pin that the blanket `impl<T: HostedDep> AsyncHostedDep
550    /// for T` covers a sync-only `HostedDep`. If a future change ever
551    /// drops or narrows the blanket, this test fires because the function
552    /// signature won't compile: it requires `T: AsyncHostedDep` and we
553    /// pass a `SyncOnlyDep` (which only implements `HostedDep`).
554    fn requires_async_hosted_dep<T: AsyncHostedDep>(_t: &T) {}
555
556    #[test]
557    fn blanket_impl_exposes_sync_hosted_dep_via_async_api() {
558        let dep = SyncOnlyDep {
559            bytes: vec![1, 2, 3, 4],
560        };
561
562        // 1. Compile-time witness: the bound `T: AsyncHostedDep` resolves
563        //    for `SyncOnlyDep` because of the blanket bridge.
564        requires_async_hosted_dep(&dep);
565
566        // 2. Owner-side: `descriptor()` reachable through both traits and
567        //    returns the same bytes.
568        assert_eq!(
569            <SyncOnlyDep as HostedDep>::descriptor(&dep),
570            vec![1, 2, 3, 4]
571        );
572        assert_eq!(
573            <SyncOnlyDep as AsyncHostedDep>::descriptor(&dep),
574            vec![1, 2, 3, 4]
575        );
576
577        // 3. Worker-side: `from_descriptor(...)` reachable through the
578        //    async API; the returned future resolves synchronously to the
579        //    same value the sync API would produce. Driven without an
580        //    executor by polling the future once (since it is
581        //    `std::future::Ready`, the first poll completes).
582        let fut = <SyncOnlyDep as AsyncHostedDep>::from_descriptor(&[7, 8, 9]);
583        let mut fut = Box::pin(fut);
584        let waker = futures_test_helpers::noop_waker();
585        let mut cx = std::task::Context::from_waker(&waker);
586        match fut.as_mut().poll(&mut cx) {
587            std::task::Poll::Ready(value) => {
588                assert_eq!(
589                    value,
590                    SyncOnlyDep {
591                        bytes: vec![7, 8, 9]
592                    },
593                    "blanket-bridged from_descriptor must yield the same value the sync impl produces"
594                );
595            }
596            std::task::Poll::Pending => panic!(
597                "blanket-bridged from_descriptor must be immediately ready (std::future::ready)"
598            ),
599        }
600    }
601
602    /// Minimal no-op waker so the test doesn't pull in tokio just to
603    /// poll a `std::future::Ready`.
604    mod futures_test_helpers {
605        use std::task::{RawWaker, RawWakerVTable, Waker};
606
607        unsafe fn clone(p: *const ()) -> RawWaker {
608            RawWaker::new(p, &VTABLE)
609        }
610        unsafe fn wake(_: *const ()) {}
611        unsafe fn wake_by_ref(_: *const ()) {}
612        unsafe fn drop(_: *const ()) {}
613
614        static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
615
616        pub fn noop_waker() -> Waker {
617            // SAFETY: vtable functions are no-ops and never touch the
618            // null data pointer.
619            unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
620        }
621    }
622}
623
624/// User-facing trait that opts a dependency value into the `HostedRpc` sharing
625/// strategy. Like [`HostedDep`], the owner lives in the
626/// **parent test runner process** for the entire suite; unlike `Hosted`,
627/// workers do NOT see the owner type — they see a separate `Stub` type
628/// that calls back into the parent over the existing IPC socket through a
629/// small generated method-dispatch table.
630///
631/// The implementor provides:
632/// - `type Stub`: the worker-side handle type tests actually parameterise on.
633/// - [`dispatch`](HostedRpcDep::dispatch): owner-side method dispatcher.
634///   Receives a stable `method_idx` plus serialized argument bytes and
635///   returns serialized result bytes (or a textual error).
636/// - [`build_stub`](HostedRpcDep::build_stub): worker-side constructor.
637///   Wraps the supplied [`HostedRpcChannel`] into a `Self::Stub` that
638///   serialises calls and forwards them to the parent's dispatcher.
639///
640/// Calls to a HostedRpc dep are **serialised** on the parent side (owner is held
641/// behind a single `Mutex`). Even logical `&self` methods do not run
642/// concurrently, matching how most singleton service handles already behave
643/// internally.
644pub trait HostedRpcDep: Send + Sync + 'static {
645    /// The worker-side handle type that tests parameterise on. Typically
646    /// a small struct that holds a [`HostedRpcChannel`] and implements
647    /// a user-defined trait by routing each method through the channel.
648    type Stub: Send + Sync + 'static;
649
650    /// Owner-side: handle one method call. `method_idx` is a stable per-method
651    /// index assigned by the implementor (usually generated by the
652    /// `#[hosted_rpc]` macro; for a manual stub, the implementor picks the
653    /// indices). `args` is the worker-supplied serialized payload. Return
654    /// `Ok(bytes)` on success or `Err(message)` on failure — the message is
655    /// surfaced to the calling worker as [`HostedRpcError::Dispatch`].
656    fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String>;
657
658    /// Worker-side: build a `Self::Stub` over the channel that connects
659    /// back to the parent's owner. Called once per worker subprocess at
660    /// startup, before any test body runs.
661    ///
662    /// **Contract — `build_stub` must be cheap and side-effect free.**
663    /// The runtime constructs one stub per registered HostedRpc dep at
664    /// worker startup, *before* the worker has even received its first
665    /// [`crate::ipc::IpcCommand::RunTest`]. In particular this means:
666    ///
667    /// - **Do NOT call `channel.call(...)` from `build_stub`** — there is
668    ///   no test in flight yet, so the parent's command loop may legally
669    ///   send a `RunTest` while the stub is blocked waiting for a reply,
670    ///   and the IPC framing will desync.
671    /// - **Do NOT block, do I/O, or do expensive work** — the stub is
672    ///   built unconditionally for every registered HostedRpc dep, even
673    ///   if the test filter doesn't pull it into the suite.
674    /// - Stash the `channel` and any small caches on `Self::Stub`; defer
675    ///   all RPC to actual method calls inside test bodies.
676    fn build_stub(channel: HostedRpcChannel) -> Self::Stub;
677}
678
679/// Dyn-safe entry point used by the parent runtime to dispatch incoming
680/// [`crate::ipc::IpcResponse::HostedRpcCall`] frames to a type-erased owner
681/// value. Auto-implemented for every [`HostedRpcDep`].
682pub trait HostedRpcDispatcher: Send + Sync {
683    fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String>;
684}
685
686impl<T: HostedRpcDep> HostedRpcDispatcher for T {
687    fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
688        <T as HostedRpcDep>::dispatch(self, method_idx, args)
689    }
690}
691
692/// Async counterpart of [`HostedRpcDep`]. Implement this when the owner-side
693/// method dispatcher needs to `.await` (e.g. controlling subprocesses, holding
694/// `tokio::sync` locks, calling other async APIs).
695///
696/// No opt-in flag is required: under the `tokio` test runtime the runtime
697/// always routes HostedRpc dispatch through `AsyncHostedRpcDep`, regardless
698/// of whether the user wrote `impl HostedRpcDep` (covered by the blanket
699/// bridge below) or `impl AsyncHostedRpcDep` directly. The `#[hosted_rpc]`
700/// macro mirrors this transparency: if any trait method is `async fn`, the
701/// generated dispatcher is async, otherwise it stays sync.
702///
703/// `build_stub` stays synchronous and the worker-side stub still calls a
704/// synchronous [`HostedRpcChannel::call`] in its method bodies, exactly as
705/// in [`HostedRpcDep`]; the only difference is that owner-side `dispatch`
706/// returns a `Future` so it can await.
707pub trait AsyncHostedRpcDep: Send + Sync + 'static {
708    /// The worker-side handle type that tests parameterise on. Same shape
709    /// as [`HostedRpcDep::Stub`].
710    type Stub: Send + Sync + 'static;
711
712    /// Owner-side: handle one method call asynchronously. `method_idx` is the
713    /// per-method index assigned by the implementor / `#[hosted_rpc]` macro;
714    /// `args` is the worker-supplied serialized payload.
715    fn dispatch<'a>(
716        &'a mut self,
717        method_idx: u32,
718        args: &'a [u8],
719    ) -> impl Future<Output = Result<Vec<u8>, String>> + Send + 'a;
720
721    /// Worker-side: build a `Self::Stub` over the channel that connects
722    /// back to the parent's owner. Identical contract to
723    /// [`HostedRpcDep::build_stub`].
724    fn build_stub(channel: HostedRpcChannel) -> Self::Stub;
725}
726
727/// Blanket bridge: every [`HostedRpcDep`] is automatically also an
728/// [`AsyncHostedRpcDep`]. The bridged `dispatch` returns
729/// [`std::future::ready`] so the bridge itself adds only an
730/// immediately-ready future and the tokio runtime can await all HostedRpc
731/// dispatch uniformly.
732///
733/// This lets the test-r runtime drive **every** HostedRpc dispatch through
734/// one async path under the `tokio` runtime, regardless of whether the
735/// owner's own implementation is sync (`impl HostedRpcDep for ...`) or
736/// async (`impl AsyncHostedRpcDep for ...`). No annotation flag is needed
737/// on `#[hosted_rpc]` or `#[test_dep]`; the implementor picks sync vs
738/// async purely at the trait-impl call site.
739///
740/// **Coherence note:** on stable Rust, with this blanket impl in place
741/// a concrete owner type cannot manually implement both `HostedRpcDep` and
742/// `AsyncHostedRpcDep` — rustc rejects that as conflicting implementations.
743/// That compile-time error is the intended signal that one of the two manual
744/// impls is redundant and should be removed.
745///
746/// **Source-compat note:** if downstream code imports both
747/// `HostedRpcDep` and `AsyncHostedRpcDep` into the same scope and calls
748/// trait methods by method syntax (e.g. `MyOwner::build_stub(channel)`
749/// or `owner.dispatch(idx, args)`), the call can become ambiguous now
750/// that one type satisfies both traits. Resolve with UFCS —
751/// `<MyOwner as HostedRpcDep>::build_stub(channel)`.
752impl<T: HostedRpcDep> AsyncHostedRpcDep for T {
753    type Stub = <T as HostedRpcDep>::Stub;
754
755    fn dispatch<'a>(
756        &'a mut self,
757        method_idx: u32,
758        args: &'a [u8],
759    ) -> impl Future<Output = Result<Vec<u8>, String>> + Send + 'a {
760        std::future::ready(<T as HostedRpcDep>::dispatch(self, method_idx, args))
761    }
762
763    fn build_stub(channel: HostedRpcChannel) -> Self::Stub {
764        <T as HostedRpcDep>::build_stub(channel)
765    }
766}
767
768/// Object-safe sibling of [`AsyncHostedRpcDep`] used by the parent's
769/// async owner cell. Auto-implemented for every [`AsyncHostedRpcDep`].
770pub trait AsyncHostedRpcDispatcher: Send + Sync {
771    fn dispatch<'a>(
772        &'a mut self,
773        method_idx: u32,
774        args: &'a [u8],
775    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>;
776}
777
778impl<T: AsyncHostedRpcDep> AsyncHostedRpcDispatcher for T {
779    fn dispatch<'a>(
780        &'a mut self,
781        method_idx: u32,
782        args: &'a [u8],
783    ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>> {
784        Box::pin(<T as AsyncHostedRpcDep>::dispatch(self, method_idx, args))
785    }
786}
787
788#[cfg(test)]
789mod hosted_rpc_blanket_bridge_tests {
790    use super::{AsyncHostedRpcDep, HostedRpcChannel, HostedRpcDep};
791
792    /// Sync-only HostedRpc owner fixture: implements [`HostedRpcDep`].
793    /// The blanket bridge must also expose it as [`AsyncHostedRpcDep`].
794    struct SyncOnlyOwner {
795        next: u64,
796    }
797
798    /// Worker-side stub stand-in — the bridge tests do not exercise
799    /// channel-side dispatch, so the stub just stashes the channel.
800    pub struct SyncOnlyStub {
801        _channel: HostedRpcChannel,
802    }
803
804    impl HostedRpcDep for SyncOnlyOwner {
805        type Stub = SyncOnlyStub;
806
807        fn dispatch(&mut self, method_idx: u32, _args: &[u8]) -> Result<Vec<u8>, String> {
808            if method_idx == 0 {
809                self.next += 1;
810                Ok(self.next.to_be_bytes().to_vec())
811            } else {
812                Err(format!("SyncOnlyOwner: unknown method_idx {method_idx}"))
813            }
814        }
815
816        fn build_stub(channel: HostedRpcChannel) -> Self::Stub {
817            SyncOnlyStub { _channel: channel }
818        }
819    }
820
821    /// Compile-time pin that the blanket `impl<T: HostedRpcDep>
822    /// AsyncHostedRpcDep for T` covers a sync-only `HostedRpcDep`. If a
823    /// future change ever drops or narrows the blanket, this function's
824    /// signature stops compiling: it requires `T: AsyncHostedRpcDep` and
825    /// we hand it a `SyncOnlyOwner` (which only implements `HostedRpcDep`).
826    fn requires_async_hosted_rpc_dep<T: AsyncHostedRpcDep>(_t: &T) {}
827
828    #[test]
829    fn blanket_impl_exposes_sync_hosted_rpc_dep_via_async_api() {
830        let owner = SyncOnlyOwner { next: 0 };
831        // 1. Compile-time witness: the bound `T: AsyncHostedRpcDep`
832        //    resolves for `SyncOnlyOwner` because of the blanket bridge.
833        requires_async_hosted_rpc_dep(&owner);
834    }
835
836    /// Driving the bridge async dispatch end-to-end requires a tokio
837    /// runtime, so the runtime-only assertion is cfg-gated. The sync
838    /// build keeps the compile-time witness above; this test extends
839    /// it by actually polling the bridged future and checking the
840    /// dispatched bytes match what the sync dispatcher would produce.
841    #[cfg(feature = "tokio")]
842    #[test]
843    fn bridged_async_dispatch_round_trips_sync_owner_bytes() {
844        let mut owner = SyncOnlyOwner { next: 0 };
845        let rt = ::tokio::runtime::Builder::new_multi_thread()
846            .enable_all()
847            .build()
848            .expect("build tokio runtime");
849        let bytes = rt
850            .block_on(<SyncOnlyOwner as AsyncHostedRpcDep>::dispatch(
851                &mut owner,
852                0,
853                &[],
854            ))
855            .expect("bridged dispatch must succeed");
856        assert_eq!(
857            bytes,
858            1u64.to_be_bytes().to_vec(),
859            "bridged async dispatch must yield the same bytes the sync impl produces"
860        );
861    }
862}
863
864/// Type-erased, parent-owned cell that holds the owner value behind a
865/// `Mutex` and exposes a `&self` dispatch entry point. Constructed by the
866/// macro-generated registration code on the parent (the `DependencyConstructor`
867/// for a `HostedRpc` dep returns one of these wrapped in `Arc<dyn Any>`)
868/// and kept alive in `_hosted_owners` for the suite's lifetime.
869///
870/// Two internal variants:
871/// - `Sync` holds a [`HostedRpcDep`] dispatcher and supports the legacy
872///   synchronous dispatch path. The sync runtime uses this exclusively.
873/// - `Async` holds an [`AsyncHostedRpcDep`] dispatcher behind a
874///   [`tokio::sync::Mutex`] so awaits inside the dispatcher don't
875///   block other tokio tasks waiting for the lock. The tokio runtime
876///   constructs this variant for HostedRpc registrations.
877pub struct HostedRpcOwnerCell {
878    inner: HostedRpcOwnerCellInner,
879}
880
881enum HostedRpcOwnerCellInner {
882    Sync(Mutex<Box<dyn HostedRpcDispatcher>>),
883    #[cfg(feature = "tokio")]
884    Async(AsyncOwnerCell),
885}
886
887#[cfg(feature = "tokio")]
888struct AsyncOwnerCell {
889    /// Mirrors the sync `Mutex` poisoning semantics: `tokio::sync::Mutex`
890    /// itself does not poison, so we track poisoning out-of-band via this
891    /// flag. Once a dispatched call panics, every subsequent dispatch
892    /// short-circuits with the stable `"hosted rpc owner poisoned"` error.
893    poisoned: std::sync::atomic::AtomicBool,
894    inner: tokio::sync::Mutex<Box<dyn AsyncHostedRpcDispatcher>>,
895}
896
897impl HostedRpcOwnerCell {
898    /// Wrap a synchronous owner value into a `HostedRpcOwnerCell`. The owner
899    /// type must implement [`HostedRpcDep`]. This is the back-compat
900    /// constructor used by the sync runtime and by any manual hand-written
901    /// fixture; the runtime never blocks on async dispatch through cells
902    /// built this way.
903    pub fn from_owner<T: HostedRpcDep>(owner: T) -> Self {
904        Self {
905            inner: HostedRpcOwnerCellInner::Sync(Mutex::new(
906                Box::new(owner) as Box<dyn HostedRpcDispatcher>
907            )),
908        }
909    }
910
911    /// Wrap an owner value that exposes an async dispatcher into a
912    /// `HostedRpcOwnerCell`. Accepts any [`AsyncHostedRpcDep`] — including
913    /// every [`HostedRpcDep`] via the blanket bridge — so the tokio runtime
914    /// can route both sync and async owners through one async dispatch path.
915    #[cfg(feature = "tokio")]
916    pub fn from_async_owner<T: AsyncHostedRpcDep>(owner: T) -> Self {
917        Self {
918            inner: HostedRpcOwnerCellInner::Async(AsyncOwnerCell {
919                poisoned: std::sync::atomic::AtomicBool::new(false),
920                inner: tokio::sync::Mutex::new(Box::new(owner) as Box<dyn AsyncHostedRpcDispatcher>),
921            }),
922        }
923    }
924
925    /// Construct a synchronous `HostedRpcOwnerCell` that dispatches
926    /// against `&T` borrowed from a shared `Arc<T>`, rather than
927    /// consuming `T` outright. Used exclusively by the
928    /// `#[test_dep(scope = Hosted, worker = both(Trait))]` lowering so
929    /// the parent-side dep map can hand the same `Arc<T>` to
930    /// downstream consumers that take `&T` while the RPC view keeps
931    /// dispatching to the same owner instance.
932    ///
933    /// The supplied `dispatch` closure is typically a thin wrapper
934    /// around the `#[hosted_rpc]`-generated
935    /// `dispatch_<snake>_shared(&T, method_idx, args)` helper.
936    ///
937    /// Calls remain serialized by the cell's internal `Mutex`, matching
938    /// the existing [`Self::from_owner`] semantics.
939    pub fn from_shared_owner_sync<T, F>(owner: Arc<T>, dispatch: F) -> Self
940    where
941        T: Send + Sync + 'static,
942        F: Fn(&T, u32, &[u8]) -> Result<Vec<u8>, String> + Send + Sync + 'static,
943    {
944        struct SharedDispatcher<T, F>
945        where
946            T: Send + Sync + 'static,
947            F: Fn(&T, u32, &[u8]) -> Result<Vec<u8>, String> + Send + Sync + 'static,
948        {
949            owner: Arc<T>,
950            dispatch: F,
951        }
952
953        impl<T, F> HostedRpcDispatcher for SharedDispatcher<T, F>
954        where
955            T: Send + Sync + 'static,
956            F: Fn(&T, u32, &[u8]) -> Result<Vec<u8>, String> + Send + Sync + 'static,
957        {
958            fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
959                (self.dispatch)(&self.owner, method_idx, args)
960            }
961        }
962
963        let dispatcher: Box<dyn HostedRpcDispatcher> =
964            Box::new(SharedDispatcher { owner, dispatch });
965        Self {
966            inner: HostedRpcOwnerCellInner::Sync(Mutex::new(dispatcher)),
967        }
968    }
969
970    /// Async counterpart of [`Self::from_shared_owner_sync`] for the
971    /// tokio runtime: dispatch against `&T` via an async closure that
972    /// returns a boxed future. Used by the `worker = both(Trait)`
973    /// lowering when an async owner constructor is in play, or when
974    /// the trait declared `async fn` methods.
975    ///
976    /// Calls remain serialized by the async cell's `tokio::sync::Mutex`,
977    /// matching the existing [`Self::from_async_owner`] semantics.
978    #[cfg(feature = "tokio")]
979    pub fn from_shared_owner_async<T, F>(owner: Arc<T>, dispatch: F) -> Self
980    where
981        T: Send + Sync + 'static,
982        F: for<'a> Fn(
983                &'a T,
984                u32,
985                &'a [u8],
986            )
987                -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>
988            + Send
989            + Sync
990            + 'static,
991    {
992        struct SharedAsyncDispatcher<T, F>
993        where
994            T: Send + Sync + 'static,
995            F: for<'a> Fn(
996                    &'a T,
997                    u32,
998                    &'a [u8],
999                )
1000                    -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>
1001                + Send
1002                + Sync
1003                + 'static,
1004        {
1005            owner: Arc<T>,
1006            dispatch: F,
1007        }
1008
1009        impl<T, F> AsyncHostedRpcDispatcher for SharedAsyncDispatcher<T, F>
1010        where
1011            T: Send + Sync + 'static,
1012            F: for<'a> Fn(
1013                    &'a T,
1014                    u32,
1015                    &'a [u8],
1016                )
1017                    -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>
1018                + Send
1019                + Sync
1020                + 'static,
1021        {
1022            fn dispatch<'a>(
1023                &'a mut self,
1024                method_idx: u32,
1025                args: &'a [u8],
1026            ) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>> {
1027                (self.dispatch)(&self.owner, method_idx, args)
1028            }
1029        }
1030
1031        let dispatcher: Box<dyn AsyncHostedRpcDispatcher> =
1032            Box::new(SharedAsyncDispatcher { owner, dispatch });
1033        Self {
1034            inner: HostedRpcOwnerCellInner::Async(AsyncOwnerCell {
1035                poisoned: std::sync::atomic::AtomicBool::new(false),
1036                inner: tokio::sync::Mutex::new(dispatcher),
1037            }),
1038        }
1039    }
1040
1041    /// Dispatch one method call synchronously. Catches owner panics and
1042    /// turns them into `Err("hosted rpc owner panicked: …")` so the
1043    /// dispatcher loop never dies. The lock is acquired *inside* the
1044    /// `catch_unwind` closure on purpose: when the owner panics, the
1045    /// `MutexGuard` drops during the unwind, which poisons the mutex.
1046    /// Every subsequent `dispatch` call then short-circuits with the
1047    /// stable `"hosted rpc owner poisoned"` error and does NOT retry the
1048    /// (possibly half-mutated) owner.
1049    ///
1050    /// For cells constructed via [`Self::from_async_owner`], synchronous
1051    /// dispatch is unsupported and the call returns
1052    /// `Err("hosted rpc owner cell uses the async dispatch path; use dispatch_async or dispatch_blocking")`.
1053    /// Note that under the tokio feature a plain `HostedRpcDep` owner may
1054    /// also end up in the async cell variant via the blanket bridge, so
1055    /// this branch is not strictly limited to user-authored async owners.
1056    /// The sync runtime never builds async cells, so this branch only
1057    /// fires in misuse cases.
1058    pub fn dispatch(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
1059        match &self.inner {
1060            HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
1061            #[cfg(feature = "tokio")]
1062            HostedRpcOwnerCellInner::Async(_) => Err(
1063                "hosted rpc owner cell uses the async dispatch path; use dispatch_async or dispatch_blocking"
1064                    .to_string(),
1065            ),
1066        }
1067    }
1068
1069    /// Async dispatch entry point used by the tokio runtime's parent-side
1070    /// HostedRpc loop and by the in-process transport's `block_on` bridge.
1071    /// Works for both `Sync` and `Async` cell variants:
1072    ///
1073    /// - `Sync` variant: invokes the synchronous dispatcher inline (no
1074    ///   `await` actually happens).
1075    /// - `Async` variant: awaits the user's async dispatcher with panic
1076    ///   capture so an `await`-side panic poisons the cell.
1077    #[cfg(feature = "tokio")]
1078    pub async fn dispatch_async(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
1079        match &self.inner {
1080            HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
1081            HostedRpcOwnerCellInner::Async(cell) => {
1082                async_dispatch_inner(cell, method_idx, args).await
1083            }
1084        }
1085    }
1086
1087    /// Synchronous bridge to [`Self::dispatch_async`] for sync call sites
1088    /// (such as [`InProcessHostedRpcTransport::call`]) that need to feed
1089    /// an async owner cell. Drives the future with
1090    /// [`tokio::task::block_in_place`] + [`tokio::runtime::Handle::block_on`]
1091    /// when an async cell is present, and falls back to the regular sync
1092    /// dispatch otherwise.
1093    ///
1094    /// `Sync` cells short-circuit through the regular sync path with no
1095    /// runtime requirement. `Async` cells require a running multi-thread
1096    /// Tokio runtime, matching the IPC transport's existing requirement.
1097    #[cfg(feature = "tokio")]
1098    pub fn dispatch_blocking(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
1099        match &self.inner {
1100            HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
1101            HostedRpcOwnerCellInner::Async(cell) => {
1102                let handle = tokio::runtime::Handle::try_current().map_err(|_| {
1103                    "hosted rpc owner is async-only and no Tokio runtime is active at the dispatch site"
1104                        .to_string()
1105                })?;
1106                // `block_in_place` panics on a `current_thread` runtime
1107                // even though `Handle::try_current()` succeeded. Probe
1108                // the runtime flavor and return a clean error instead
1109                // of hitting the panic — the API contract is
1110                // `Result<_, String>`.
1111                if !matches!(
1112                    handle.runtime_flavor(),
1113                    tokio::runtime::RuntimeFlavor::MultiThread
1114                ) {
1115                    return Err(
1116                        "hosted rpc owner is async-only and the current Tokio runtime is not multi-threaded"
1117                            .to_string(),
1118                    );
1119                }
1120                tokio::task::block_in_place(|| {
1121                    handle.block_on(async_dispatch_inner(cell, method_idx, args))
1122                })
1123            }
1124        }
1125    }
1126}
1127
1128fn sync_dispatch_inner(
1129    mtx: &Mutex<Box<dyn HostedRpcDispatcher>>,
1130    method_idx: u32,
1131    args: &[u8],
1132) -> Result<Vec<u8>, String> {
1133    // The lock acquire lives inside the catch_unwind closure on
1134    // purpose. If we acquired the lock outside and the user dispatch
1135    // panicked, the panic would be caught before the MutexGuard had a
1136    // chance to drop during unwinding, leaving the mutex healthy — and
1137    // we want it poisoned so that subsequent calls see a deterministic
1138    // "owner is dead" error rather than re-entering a half-mutated
1139    // owner value.
1140    let dispatch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1141        let mut guard = match mtx.lock() {
1142            Ok(g) => g,
1143            Err(_) => return Err("hosted rpc owner poisoned".to_string()),
1144        };
1145        guard.dispatch(method_idx, args)
1146    }));
1147    panic_payload_to_err(dispatch_result)
1148}
1149
1150#[cfg(feature = "tokio")]
1151async fn async_dispatch_inner(
1152    cell: &AsyncOwnerCell,
1153    method_idx: u32,
1154    args: &[u8],
1155) -> Result<Vec<u8>, String> {
1156    use futures::FutureExt;
1157    use std::sync::atomic::Ordering;
1158
1159    // Fast-path check: avoids acquiring the async mutex when the owner
1160    // has already been poisoned by an earlier panic.
1161    if cell.poisoned.load(Ordering::SeqCst) {
1162        return Err("hosted rpc owner poisoned".to_string());
1163    }
1164    let mut guard = cell.inner.lock().await;
1165    // Re-check inside the lock: a second dispatch can park on
1166    // `lock().await` *before* the first dispatch panics. Without this
1167    // re-check the second waiter would acquire the lock and re-enter
1168    // the (possibly half-mutated) owner because the poison flag is
1169    // only stored after the panicking task drops its guard. This
1170    // mirrors the std::sync::Mutex poisoning semantics the sync cell
1171    // gets for free.
1172    if cell.poisoned.load(Ordering::SeqCst) {
1173        return Err("hosted rpc owner poisoned".to_string());
1174    }
1175    let fut = std::panic::AssertUnwindSafe(async {
1176        AsyncHostedRpcDispatcher::dispatch(&mut **guard, method_idx, args).await
1177    });
1178    let outcome = fut.catch_unwind().await;
1179    match outcome {
1180        Ok(r) => {
1181            drop(guard);
1182            r
1183        }
1184        Err(payload) => {
1185            // Set the poison flag *while still holding the guard* so any
1186            // waiter that subsequently acquires the mutex sees the flag
1187            // on its in-lock re-check above and short-circuits without
1188            // re-entering the owner.
1189            cell.poisoned.store(true, Ordering::SeqCst);
1190            drop(guard);
1191            let msg = panic_payload_to_string(&payload);
1192            Err(format!("hosted rpc owner panicked: {msg}"))
1193        }
1194    }
1195}
1196
1197fn panic_payload_to_err(
1198    dispatch_result: Result<Result<Vec<u8>, String>, Box<dyn Any + Send>>,
1199) -> Result<Vec<u8>, String> {
1200    match dispatch_result {
1201        Ok(r) => r,
1202        Err(payload) => {
1203            let msg = panic_payload_to_string(&payload);
1204            Err(format!("hosted rpc owner panicked: {msg}"))
1205        }
1206    }
1207}
1208
1209fn panic_payload_to_string(payload: &Box<dyn Any + Send>) -> String {
1210    if let Some(s) = payload.downcast_ref::<&str>() {
1211        (*s).to_string()
1212    } else if let Some(s) = payload.downcast_ref::<String>() {
1213        s.clone()
1214    } else {
1215        "<non-string panic payload>".to_string()
1216    }
1217}
1218
1219/// Support type for `#[test_dep(scope = Hosted, worker = both(T))]`.
1220///
1221/// One macro-emitted `worker = both(T)` registration is lowered into
1222/// **two** `RegisteredDependency` entries that both point at the same
1223/// parent-side owner — one for the descriptor (Hosted) view, one for
1224/// the RPC stub (HostedRpc) view. To keep the owner unique under
1225/// either view, both registrations route through a single
1226/// `HostedBothShared` cell created by the macro-emitted weak cache:
1227///
1228/// - the **descriptor view** asks for the cached descriptor bytes
1229///   (`HostedDep::descriptor` / `AsyncHostedDep::descriptor` is only
1230///   called once, on the first construction);
1231/// - the **RPC view** asks for the inner [`HostedRpcOwnerCell`], so
1232///   the parent-side dispatcher sees the same owner the descriptor
1233///   was derived from;
1234/// - the **parent-side owner getter** (used by downstream dep
1235///   constructors that take `&Owner`) downcasts to [`HostedBothShared`]
1236///   and pulls out [`Self::owner_arc`], a type-erased `Arc<T>` of the
1237///   very same owner the cell holds.
1238///
1239/// This is intentionally *not* a public end-user type; only the
1240/// macro-support helpers in [`crate::__test_r_make_hosted_both_shared`]
1241/// and friends construct one.
1242pub struct HostedBothShared {
1243    descriptor_bytes: Vec<u8>,
1244    /// Type-erased `Arc<T>` of the owner value. The RPC cell holds
1245    /// the same `Arc<T>` (cloned) and dispatches against `&T` via the
1246    /// `#[hosted_rpc]`-generated `&self` dispatcher helper, so parent
1247    /// consumers and the RPC view observe one and the same owner
1248    /// instance.
1249    owner: Arc<dyn Any + Send + Sync>,
1250    rpc_cell: Arc<HostedRpcOwnerCell>,
1251}
1252
1253impl HostedBothShared {
1254    /// Wrap a pre-computed descriptor + type-erased owner handle + RPC
1255    /// owner cell for the `both` dep variant. The macro acquire helper
1256    /// is the canonical construction site.
1257    pub fn new(
1258        descriptor_bytes: Vec<u8>,
1259        owner: Arc<dyn Any + Send + Sync>,
1260        rpc_cell: Arc<HostedRpcOwnerCell>,
1261    ) -> Self {
1262        Self {
1263            descriptor_bytes,
1264            owner,
1265            rpc_cell,
1266        }
1267    }
1268
1269    /// Borrow the cached descriptor bytes (computed once, on first
1270    /// construction).
1271    pub fn descriptor_bytes(&self) -> &[u8] {
1272        &self.descriptor_bytes
1273    }
1274
1275    /// Cheap clone of the inner RPC owner cell `Arc`. The
1276    /// HostedRpc-view registration's `RpcFactory::owner_into_cell`
1277    /// hands this back to the runtime.
1278    pub fn rpc_cell(&self) -> Arc<HostedRpcOwnerCell> {
1279        self.rpc_cell.clone()
1280    }
1281
1282    /// Downcast the type-erased owner handle back to `Arc<T>`. Used by
1283    /// the macro-generated owner getter so parent-side consumers that
1284    /// take `&T` can reach the singleton owner the RPC cell is holding
1285    /// behind a shared dispatcher.
1286    pub fn owner_arc<T>(&self) -> Arc<T>
1287    where
1288        T: Send + Sync + 'static,
1289    {
1290        Arc::clone(&self.owner)
1291            .downcast::<T>()
1292            .expect("HostedBothShared owner type mismatch")
1293    }
1294}
1295
1296/// Error returned by [`HostedRpcChannel::call`] when an RPC fails.
1297#[derive(Debug, Clone)]
1298pub enum HostedRpcError {
1299    /// The owner-side dispatcher returned an error string (unknown method,
1300    /// codec error, panic in the user method, …).
1301    Dispatch(String),
1302    /// The IPC transport itself failed (worker disconnected, framing error,
1303    /// runtime not in spawn-workers mode, …).
1304    Transport(String),
1305}
1306
1307impl std::fmt::Display for HostedRpcError {
1308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1309        match self {
1310            HostedRpcError::Dispatch(s) => write!(f, "hosted rpc dispatch error: {s}"),
1311            HostedRpcError::Transport(s) => write!(f, "hosted rpc transport error: {s}"),
1312        }
1313    }
1314}
1315
1316impl std::error::Error for HostedRpcError {}
1317
1318/// Trait implemented by the per-runner transport that workers use to send
1319/// RPCs to the parent's owner. The runtime provides a concrete IPC
1320/// implementation for the spawn-workers case and a direct in-process
1321/// implementation for `--nocapture` / single-process mode.
1322pub trait HostedRpcTransport: Send + Sync {
1323    /// Send one call and block until the reply arrives. `dep_id` is the
1324    /// dep's fully-qualified id (`{crate}::{module}::{name}`) used by the
1325    /// parent to route the call to the right owner.
1326    fn call(&self, dep_id: &str, method_idx: u32, args: Vec<u8>)
1327        -> Result<Vec<u8>, HostedRpcError>;
1328}
1329
1330/// Per-dep channel handed to [`HostedRpcDep::build_stub`] on the worker side.
1331///
1332/// The stub holds this channel and calls [`HostedRpcChannel::call`] from
1333/// each of its method bodies; the channel takes care of dep-id routing,
1334/// serialization framing, and waiting for the parent's reply.
1335pub struct HostedRpcChannel {
1336    dep_id: String,
1337    transport: Arc<dyn HostedRpcTransport>,
1338}
1339
1340impl HostedRpcChannel {
1341    /// Construct a channel that targets the dep identified by
1342    /// `dep_id` (a fully-qualified id) and uses the supplied transport.
1343    pub fn new(dep_id: String, transport: Arc<dyn HostedRpcTransport>) -> Self {
1344        Self { dep_id, transport }
1345    }
1346
1347    /// The fully-qualified dep id this channel routes to. Stubs almost never
1348    /// need this directly, but it's exposed for diagnostics and tests.
1349    pub fn dep_id(&self) -> &str {
1350        &self.dep_id
1351    }
1352
1353    /// Send one method call and block until the parent replies. `args` are
1354    /// already-serialized bytes; the stub method body owns the choice of
1355    /// codec.
1356    ///
1357    /// **Temporal invariant — only call this while a test body is actually
1358    /// running.** The transport assumes one
1359    /// HostedRpc request/reply pair per worker subprocess is in flight
1360    /// at a time *and* that the worker's main IPC command loop is idle
1361    /// (it only reads `Provide*` / `RunTest` between tests). Specifically:
1362    ///
1363    /// - **Do NOT call from `HostedRpcDep::build_stub`** — see that
1364    ///   method's docs for why.
1365    /// - **Do NOT call from background threads or detached tasks that
1366    ///   outlive the test body** — once the test returns the worker
1367    ///   sends `TestFinished` and the parent's next message will be a
1368    ///   `Provide*` / `RunTest`, which the transport's read side would
1369    ///   then misinterpret as a reply.
1370    /// - **Do NOT call from `Drop` / destructor-style cleanup or any
1371    ///   teardown hook that may fire after the test body has returned** —
1372    ///   that is just another form of "outside the test body" and has the
1373    ///   same IPC-framing-desync risk as a detached background thread.
1374    /// - Stub calls from inside the test body — directly or transitively
1375    ///   from helpers the test body awaits/blocks on — are the supported
1376    ///   shape.
1377    pub fn call(&self, method_idx: u32, args: Vec<u8>) -> Result<Vec<u8>, HostedRpcError> {
1378        self.transport.call(&self.dep_id, method_idx, args)
1379    }
1380}
1381
1382impl Clone for HostedRpcChannel {
1383    fn clone(&self) -> Self {
1384        Self {
1385            dep_id: self.dep_id.clone(),
1386            transport: self.transport.clone(),
1387        }
1388    }
1389}
1390
1391/// In-process transport used in `--nocapture` / single-process mode: the
1392/// stub calls the owner-side [`HostedRpcOwnerCell`] directly without
1393/// touching any IPC stream.
1394pub struct InProcessHostedRpcTransport {
1395    cells: HashMap<String, Arc<HostedRpcOwnerCell>>,
1396}
1397
1398impl InProcessHostedRpcTransport {
1399    pub fn new(cells: HashMap<String, Arc<HostedRpcOwnerCell>>) -> Self {
1400        Self { cells }
1401    }
1402}
1403
1404impl HostedRpcTransport for InProcessHostedRpcTransport {
1405    fn call(
1406        &self,
1407        dep_id: &str,
1408        method_idx: u32,
1409        args: Vec<u8>,
1410    ) -> Result<Vec<u8>, HostedRpcError> {
1411        let cell = self.cells.get(dep_id).ok_or_else(|| {
1412            HostedRpcError::Transport(format!("in-process HostedRpc: unknown dep id '{dep_id}'"))
1413        })?;
1414        // Under the tokio feature, route through `dispatch_blocking` so async
1415        // owners (and bridged sync owners stored in `Async` cells) are driven
1416        // by the surrounding multi-thread tokio runtime. Without the tokio
1417        // feature only sync cells exist, so the plain sync `dispatch` is fine.
1418        #[cfg(feature = "tokio")]
1419        let result = cell.dispatch_blocking(method_idx, &args);
1420        #[cfg(not(feature = "tokio"))]
1421        let result = cell.dispatch(method_idx, &args);
1422        result.map_err(HostedRpcError::Dispatch)
1423    }
1424}
1425
1426/// Factory pair stored on a `HostedRpc` [`RegisteredDependency`]. The macro
1427/// emits a `RpcFactory` per registered HostedRpc dep so the runtime can
1428/// (a) wrap the constructor's output into a parent dispatcher cell, and
1429/// (b) build a worker-side stub from a channel.
1430#[derive(Clone)]
1431#[allow(clippy::type_complexity)]
1432pub struct RpcFactory {
1433    /// Downcast the constructor's `Arc<dyn Any>` to the concrete
1434    /// `HostedRpcOwnerCell` for this dep.
1435    pub owner_into_cell: Arc<
1436        dyn (Fn(Arc<dyn Any + Send + Sync>) -> Arc<HostedRpcOwnerCell>) + Send + Sync + 'static,
1437    >,
1438    /// Build a worker-side stub (typed as the dep's `Stub` associated type)
1439    /// from the supplied channel, boxed as `Arc<dyn Any>`.
1440    pub build_stub:
1441        Arc<dyn (Fn(HostedRpcChannel) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
1442}
1443
1444/// Sharing strategy declared on a `#[test_dep]`. Controls how the dependency
1445/// interacts with output capturing and parallel test execution.
1446///
1447/// See `book/src/design/sharing-strategy.md` for the full description.
1448#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
1449pub enum DepScope {
1450    /// Today's behaviour: a single materialized instance shared by every test.
1451    /// Forces single-threaded execution when output capturing is on, because
1452    /// the `Arc<dyn Any>` cannot cross the parent/worker process boundary.
1453    #[default]
1454    Shared,
1455    /// Each worker child materializes its own instance independently. Tests
1456    /// inside one worker share the instance.
1457    PerWorker,
1458    /// Parent runs the constructor once and produces wire bytes; each worker
1459    /// reconstructs a local instance from those bytes via the registered
1460    /// worker reconstructor (`worker_fn`).
1461    Cloneable,
1462    /// Owner runs once in the **parent** test runner process and stays alive
1463    /// for the entire suite. The parent produces a descriptor (via
1464    /// [`HostedDep::descriptor`]) and ships those descriptor bytes to every
1465    /// worker. Each worker reconstructs a handle via
1466    /// [`HostedDep::from_descriptor`]. The owner is held in the parent
1467    /// process so singleton services (TCP listeners, Docker containers,
1468    /// gRPC server clients, env-based runtimes) are not duplicated per
1469    /// worker.
1470    Hosted,
1471    /// Like [`Self::Hosted`], but the owner stays in the parent AND workers
1472    /// talk back to it via the runtime's built-in RPC layer instead of reaching
1473    /// out via their own transport. The dep implementor provides a
1474    /// [`HostedRpcDep`] impl (sync owners) — or, under the `tokio` feature,
1475    /// an [`AsyncHostedRpcDep`] impl (async owners) — on the owner type
1476    /// with a stub type, a method dispatch function, and a stub builder.
1477    HostedRpc,
1478}
1479
1480impl DepScope {
1481    /// Returns `true` for scopes that materialize in the parent process and
1482    /// therefore force single-threaded fallback when capturing is on.
1483    pub fn requires_single_thread_when_capturing(&self) -> bool {
1484        matches!(self, DepScope::Shared)
1485    }
1486
1487    /// Returns `true` for scopes the parent should still materialize even
1488    /// when it is otherwise delegating dependency construction to workers
1489    /// (i.e. `skip_creating_dependencies` is set). `Cloneable` deps need the
1490    /// parent to compute the wire form; `Hosted` / `HostedRpc` deps need
1491    /// the parent to hold the owner alive for the whole suite.
1492    pub fn parent_must_materialize_under_spawn_workers(&self) -> bool {
1493        matches!(
1494            self,
1495            DepScope::Cloneable | DepScope::Hosted | DepScope::HostedRpc
1496        )
1497    }
1498}
1499
1500/// Function pointer-equivalent used by the worker side of a `Cloneable`
1501/// dependency. Receives the deserialized wire payload (boxed as `Any` for
1502/// type erasure) plus the current dependency view, and produces the
1503/// reconstructed worker-side value.
1504#[derive(Clone)]
1505#[allow(clippy::type_complexity)]
1506pub enum WorkerReconstructor {
1507    Sync(
1508        Arc<
1509            dyn (Fn(
1510                    Arc<dyn Any + Send + Sync>,
1511                    Arc<dyn DependencyView + Send + Sync>,
1512                ) -> Arc<dyn Any + Send + Sync + 'static>)
1513                + Send
1514                + Sync
1515                + 'static,
1516        >,
1517    ),
1518    Async(
1519        Arc<
1520            dyn (Fn(
1521                    Arc<dyn Any + Send + Sync>,
1522                    Arc<dyn DependencyView + Send + Sync>,
1523                ) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
1524                + Send
1525                + Sync
1526                + 'static,
1527        >,
1528    ),
1529}
1530
1531/// Function-pointer wrappers used by Cloneable deps to convert the
1532/// constructed value into wire bytes on the parent, and to deserialize those
1533/// bytes into a typed value on the worker.
1534#[derive(Clone)]
1535#[allow(clippy::type_complexity)]
1536pub struct CloneableCodec {
1537    /// Parent-side: `to_wire`. Receives the dependency value as `Arc<dyn Any>`,
1538    /// returns the encoded wire bytes.
1539    pub to_wire: Arc<dyn (Fn(Arc<dyn Any + Send + Sync>) -> Vec<u8>) + Send + Sync + 'static>,
1540    /// Worker-side: deserialize wire bytes into the boxed `Wire` payload that
1541    /// is then fed to the [`WorkerReconstructor`].
1542    pub from_wire_bytes: Arc<dyn (Fn(&[u8]) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
1543}
1544
1545#[derive(Clone)]
1546pub struct RegisteredDependency {
1547    pub name: String, // TODO: Should we use TypeId here?
1548    pub crate_name: String,
1549    pub module_path: String,
1550    pub constructor: DependencyConstructor,
1551    pub dependencies: Vec<String>,
1552    /// Sharing strategy declared on the constructor. Defaults to
1553    /// [`DepScope::Shared`] for backward compatibility.
1554    pub scope: DepScope,
1555    /// Worker-side reconstructor for `Cloneable` and `Hosted` deps
1556    /// (`None` otherwise). For `Cloneable` the wire payload IS the dep value;
1557    /// for `Hosted` the wire payload is the descriptor passed to
1558    /// [`HostedDep::from_descriptor`](crate::internal::HostedDep::from_descriptor).
1559    pub worker_fn: Option<WorkerReconstructor>,
1560    /// Wire-bytes codec for `Cloneable` deps (`None` otherwise). The codec
1561    /// shape is shared with [`Self::hosted_codec`] but the runtime dispatches
1562    /// on whichever field is populated.
1563    pub cloneable_codec: Option<CloneableCodec>,
1564    /// Descriptor-bytes codec for `Hosted` deps (`None` otherwise). Same
1565    /// shape as [`Self::cloneable_codec`]; the codec encodes the value
1566    /// returned by [`HostedDep::descriptor`](crate::internal::HostedDep::descriptor)
1567    /// into wire bytes on the parent (where the owner lives), and decodes
1568    /// those bytes in the worker before they are passed to the registered
1569    /// worker reconstructor.
1570    pub hosted_codec: Option<CloneableCodec>,
1571    /// Factories for `HostedRpc` deps (`None` otherwise). The parent uses
1572    /// [`RpcFactory::owner_into_cell`] to extract the `HostedRpcOwnerCell`
1573    /// returned by the constructor; the worker uses [`RpcFactory::build_stub`]
1574    /// to construct its `Stub` from a fresh [`HostedRpcChannel`].
1575    pub rpc_factory: Option<RpcFactory>,
1576    /// Planner-only sibling dep names that must be retained together
1577    /// with this dep during pruning. Unlike `dependencies`, companions
1578    /// are **not** real dependency edges — no constructor argument is
1579    /// derived from a companion, and no topological ordering is
1580    /// implied. The pruner simply treats companions as mutually
1581    /// reachable: if any companion in a group is in the keep-set, the
1582    /// whole group is retained.
1583    ///
1584    /// Currently set by the `#[test_dep(scope = Hosted, worker = both(T))]`
1585    /// macro lowering, which registers two paired dep entries (the
1586    /// Hosted owner view and the HostedRpc stub view) backed by the
1587    /// same parent-side `Arc<HostedBothShared>` cache. The async
1588    /// flavour of that lowering has a sync resolver on the stub side
1589    /// that assumes the Hosted side has already populated the shared
1590    /// cache; if pruning ever dropped the Hosted half because the
1591    /// selected tests only parameterised on the stub view, that
1592    /// resolver would panic. Pairing the two as companions guarantees
1593    /// the Hosted half is retained whenever either half is needed.
1594    pub companions: Vec<String>,
1595}
1596
1597impl RegisteredDependency {
1598    /// Construct a `Shared` (legacy / default-scope) dependency. Preserves the
1599    /// pre-scopes constructor signature so downstream code that built
1600    /// `RegisteredDependency` directly keeps compiling.
1601    pub fn new_shared(
1602        name: String,
1603        crate_name: String,
1604        module_path: String,
1605        constructor: DependencyConstructor,
1606        dependencies: Vec<String>,
1607    ) -> Self {
1608        Self {
1609            name,
1610            crate_name,
1611            module_path,
1612            constructor,
1613            dependencies,
1614            scope: DepScope::Shared,
1615            worker_fn: None,
1616            cloneable_codec: None,
1617            hosted_codec: None,
1618            rpc_factory: None,
1619            companions: Vec::new(),
1620        }
1621    }
1622}
1623
1624impl Debug for RegisteredDependency {
1625    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1626        f.debug_struct("RegisteredDependency")
1627            .field("name", &self.name)
1628            .field("crate_name", &self.crate_name)
1629            .field("module_path", &self.module_path)
1630            .finish()
1631    }
1632}
1633
1634impl PartialEq for RegisteredDependency {
1635    fn eq(&self, other: &Self) -> bool {
1636        self.name == other.name
1637    }
1638}
1639
1640impl Eq for RegisteredDependency {}
1641
1642impl Hash for RegisteredDependency {
1643    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1644        self.name.hash(state);
1645    }
1646}
1647
1648impl RegisteredDependency {
1649    pub fn crate_and_module(&self) -> String {
1650        [&self.crate_name, &self.module_path]
1651            .into_iter()
1652            .filter(|s| !s.is_empty())
1653            .cloned()
1654            .collect::<Vec<String>>()
1655            .join("::")
1656    }
1657
1658    /// Fully-qualified identifier used for cross-process bookkeeping of
1659    /// Cloneable dependencies. The shape is `{crate_name}::{module_path}::{name}`
1660    /// with empty segments dropped, so two deps with the same `name` registered
1661    /// in different modules get distinct identifiers.
1662    pub fn qualified_id(&self) -> String {
1663        [&self.crate_name, &self.module_path, &self.name]
1664            .into_iter()
1665            .filter(|s| !s.is_empty())
1666            .cloned()
1667            .collect::<Vec<String>>()
1668            .join("::")
1669    }
1670}
1671
1672pub static REGISTERED_DEPENDENCY_CONSTRUCTORS: Mutex<Vec<RegisteredDependency>> =
1673    Mutex::new(Vec::new());
1674
1675#[derive(Debug, Clone)]
1676pub enum RegisteredTestSuiteProperty {
1677    Sequential {
1678        name: String,
1679        crate_name: String,
1680        module_path: String,
1681    },
1682    Tag {
1683        name: String,
1684        crate_name: String,
1685        module_path: String,
1686        tag: String,
1687    },
1688    Timeout {
1689        name: String,
1690        crate_name: String,
1691        module_path: String,
1692        timeout: Duration,
1693    },
1694}
1695
1696impl RegisteredTestSuiteProperty {
1697    pub fn crate_name(&self) -> &String {
1698        match self {
1699            RegisteredTestSuiteProperty::Sequential { crate_name, .. } => crate_name,
1700            RegisteredTestSuiteProperty::Tag { crate_name, .. } => crate_name,
1701            RegisteredTestSuiteProperty::Timeout { crate_name, .. } => crate_name,
1702        }
1703    }
1704
1705    pub fn module_path(&self) -> &String {
1706        match self {
1707            RegisteredTestSuiteProperty::Sequential { module_path, .. } => module_path,
1708            RegisteredTestSuiteProperty::Tag { module_path, .. } => module_path,
1709            RegisteredTestSuiteProperty::Timeout { module_path, .. } => module_path,
1710        }
1711    }
1712
1713    pub fn name(&self) -> &String {
1714        match self {
1715            RegisteredTestSuiteProperty::Sequential { name, .. } => name,
1716            RegisteredTestSuiteProperty::Tag { name, .. } => name,
1717            RegisteredTestSuiteProperty::Timeout { name, .. } => name,
1718        }
1719    }
1720
1721    pub fn crate_and_module(&self) -> String {
1722        [self.crate_name(), self.module_path(), self.name()]
1723            .into_iter()
1724            .filter(|s| !s.is_empty())
1725            .cloned()
1726            .collect::<Vec<String>>()
1727            .join("::")
1728    }
1729}
1730
1731pub static REGISTERED_TESTSUITE_PROPS: Mutex<Vec<RegisteredTestSuiteProperty>> =
1732    Mutex::new(Vec::new());
1733
1734#[derive(Clone)]
1735#[allow(clippy::type_complexity)]
1736pub enum TestGeneratorFunction {
1737    Sync(Arc<dyn Fn() -> Vec<GeneratedTest> + Send + Sync + 'static>),
1738    Async(
1739        Arc<
1740            dyn (Fn() -> Pin<Box<dyn Future<Output = Vec<GeneratedTest>> + Send>>)
1741                + Send
1742                + Sync
1743                + 'static,
1744        >,
1745    ),
1746}
1747
1748pub struct DynamicTestRegistration {
1749    tests: Vec<GeneratedTest>,
1750}
1751
1752impl Default for DynamicTestRegistration {
1753    fn default() -> Self {
1754        Self::new()
1755    }
1756}
1757
1758impl DynamicTestRegistration {
1759    pub fn new() -> Self {
1760        Self { tests: Vec::new() }
1761    }
1762
1763    pub fn to_vec(self) -> Vec<GeneratedTest> {
1764        self.tests
1765    }
1766
1767    pub fn add_sync_test<R: TestReturnValue + 'static>(
1768        &mut self,
1769        name: impl AsRef<str>,
1770        props: TestProperties,
1771        dependencies: Option<Vec<String>>,
1772        run: impl Fn(Arc<dyn DependencyView + Send + Sync>) -> R + Send + Sync + Clone + 'static,
1773    ) {
1774        self.tests.push(GeneratedTest {
1775            name: name.as_ref().to_string(),
1776            run: TestFunction::Sync(Arc::new(move |deps| {
1777                Box::new(run(deps)) as Box<dyn TestReturnValue>
1778            })),
1779            props,
1780            dependencies,
1781        });
1782    }
1783
1784    #[cfg(feature = "tokio")]
1785    pub fn add_async_test<R: TestReturnValue + 'static>(
1786        &mut self,
1787        name: impl AsRef<str>,
1788        props: TestProperties,
1789        dependencies: Option<Vec<String>>,
1790        run: impl (Fn(Arc<dyn DependencyView + Send + Sync>) -> Pin<Box<dyn Future<Output = R> + Send>>)
1791            + Send
1792            + Sync
1793            + Clone
1794            + 'static,
1795    ) {
1796        self.tests.push(GeneratedTest {
1797            name: name.as_ref().to_string(),
1798            run: TestFunction::Async(Arc::new(move |deps| {
1799                let run = run.clone();
1800                Box::pin(async move {
1801                    let r = run(deps).await;
1802                    Box::new(r) as Box<dyn TestReturnValue>
1803                })
1804            })),
1805            props,
1806            dependencies,
1807        });
1808    }
1809}
1810
1811#[derive(Clone)]
1812pub struct GeneratedTest {
1813    pub name: String,
1814    pub run: TestFunction,
1815    pub props: TestProperties,
1816    pub dependencies: Option<Vec<String>>,
1817}
1818
1819#[derive(Clone)]
1820pub struct RegisteredTestGenerator {
1821    pub name: String,
1822    pub crate_name: String,
1823    pub module_path: String,
1824    pub run: TestGeneratorFunction,
1825    pub is_ignored: bool,
1826}
1827
1828impl RegisteredTestGenerator {
1829    pub fn crate_and_module(&self) -> String {
1830        [&self.crate_name, &self.module_path]
1831            .into_iter()
1832            .filter(|s| !s.is_empty())
1833            .cloned()
1834            .collect::<Vec<String>>()
1835            .join("::")
1836    }
1837}
1838
1839pub static REGISTERED_TEST_GENERATORS: Mutex<Vec<RegisteredTestGenerator>> = Mutex::new(Vec::new());
1840
1841pub(crate) fn filter_test(test: &RegisteredTest, filter: &str, exact: bool) -> bool {
1842    if let Some(tag_list) = filter.strip_prefix(":tag:") {
1843        if tag_list.is_empty() {
1844            // Filtering for tags with NO TAGS
1845            test.props.tags.is_empty()
1846        } else {
1847            let or_tags = tag_list.split('|').collect::<Vec<&str>>();
1848            let mut result = false;
1849            for or_tag in or_tags {
1850                let and_tags = or_tag.split('&').collect::<Vec<&str>>();
1851                let mut and_result = true;
1852                for and_tag in and_tags {
1853                    if !test.props.tags.contains(&and_tag.to_string()) {
1854                        and_result = false;
1855                        break;
1856                    }
1857                }
1858                if and_result {
1859                    result = true;
1860                    break;
1861                }
1862            }
1863            result
1864        }
1865    } else if exact {
1866        test.filterable_name() == filter
1867    } else {
1868        test.filterable_name().contains(filter)
1869    }
1870}
1871
1872pub(crate) fn apply_suite_props_to_tests(
1873    tests: &[RegisteredTest],
1874    props: &[RegisteredTestSuiteProperty],
1875) -> Vec<RegisteredTest> {
1876    let props_with_prefix = props
1877        .iter()
1878        .map(|prop| (prop.crate_and_module(), prop))
1879        .collect::<Vec<_>>();
1880
1881    let mut result = Vec::new();
1882    for test in tests {
1883        let mut test = test.clone();
1884        for (prefix, prop) in &props_with_prefix {
1885            if test.crate_and_module().starts_with(prefix) {
1886                match prop {
1887                    RegisteredTestSuiteProperty::Tag { tag, .. } => {
1888                        test.props.tags.push(tag.clone());
1889                    }
1890                    RegisteredTestSuiteProperty::Timeout { timeout, .. } => {
1891                        if test.props.timeout.is_none() {
1892                            test.props.timeout = Some(*timeout);
1893                        }
1894                    }
1895                    RegisteredTestSuiteProperty::Sequential { .. } => {
1896                        // handled in TestSuiteExecution
1897                    }
1898                }
1899            }
1900        }
1901        result.push(test);
1902    }
1903    result
1904}
1905
1906pub(crate) fn filter_registered_tests(
1907    args: &Arguments,
1908    registered_tests: &[RegisteredTest],
1909) -> Vec<RegisteredTest> {
1910    registered_tests
1911        .iter()
1912        .filter(|registered_test| {
1913            !args
1914                .skip
1915                .iter()
1916                .any(|skip| filter_test(registered_test, skip, args.exact))
1917        })
1918        .filter(|registered_test| {
1919            args.filter.is_empty()
1920                || args
1921                    .filter
1922                    .iter()
1923                    .any(|filter| filter_test(registered_test, filter, args.exact))
1924        })
1925        .filter(|registered_tests| {
1926            (args.bench && registered_tests.run.is_bench())
1927                || (args.test && !registered_tests.run.is_bench())
1928                || (!args.bench && !args.test)
1929        })
1930        .filter(|registered_test| {
1931            !args.exclude_should_panic || registered_test.props.should_panic == ShouldPanic::No
1932        })
1933        .cloned()
1934        .collect::<Vec<_>>()
1935}
1936
1937fn add_generated_tests(
1938    target: &mut Vec<RegisteredTest>,
1939    generator: &RegisteredTestGenerator,
1940    generated: Vec<GeneratedTest>,
1941) {
1942    target.extend(generated.into_iter().map(|mut test| {
1943        test.props.is_ignored |= generator.is_ignored;
1944        RegisteredTest {
1945            name: format!("{}::{}", generator.name, test.name),
1946            crate_name: generator.crate_name.clone(),
1947            module_path: generator.module_path.clone(),
1948            run: test.run,
1949            props: test.props,
1950            dependencies: test.dependencies,
1951        }
1952    }));
1953}
1954
1955#[cfg(feature = "tokio")]
1956pub(crate) async fn generate_tests(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
1957    let mut result = Vec::new();
1958    for generator in generators {
1959        match &generator.run {
1960            TestGeneratorFunction::Sync(generator_fn) => {
1961                let tests = generator_fn();
1962                add_generated_tests(&mut result, generator, tests);
1963            }
1964            TestGeneratorFunction::Async(generator_fn) => {
1965                let tests = generator_fn().await;
1966                add_generated_tests(&mut result, generator, tests);
1967            }
1968        }
1969    }
1970    result
1971}
1972
1973pub(crate) fn generate_tests_sync(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
1974    let mut result = Vec::new();
1975    for generator in generators {
1976        match &generator.run {
1977            TestGeneratorFunction::Sync(generator_fn) => {
1978                let tests = generator_fn();
1979                add_generated_tests(&mut result, generator, tests);
1980            }
1981            TestGeneratorFunction::Async(_) => {
1982                panic!("Async test generators are not supported in sync mode")
1983            }
1984        }
1985    }
1986    result
1987}
1988
1989pub(crate) fn get_ensure_time(args: &Arguments, test: &RegisteredTest) -> Option<TimeThreshold> {
1990    let should_ensure_time = match test.props.ensure_time_control {
1991        ReportTimeControl::Default => args.ensure_time,
1992        ReportTimeControl::Enabled => true,
1993        ReportTimeControl::Disabled => false,
1994    };
1995    if should_ensure_time {
1996        match test.props.test_type {
1997            TestType::UnitTest => Some(args.unit_test_threshold()),
1998            TestType::IntegrationTest => Some(args.integration_test_threshold()),
1999        }
2000    } else {
2001        None
2002    }
2003}
2004
2005#[derive(Clone)]
2006pub enum TestResult {
2007    Passed {
2008        captured: Vec<CapturedOutput>,
2009        exec_time: Duration,
2010    },
2011    Benchmarked {
2012        captured: Vec<CapturedOutput>,
2013        exec_time: Duration,
2014        ns_iter_summ: Summary,
2015        mb_s: usize,
2016    },
2017    Failed {
2018        cause: FailureCause,
2019        captured: Vec<CapturedOutput>,
2020        exec_time: Duration,
2021    },
2022    Ignored {
2023        captured: Vec<CapturedOutput>,
2024    },
2025}
2026
2027impl TestResult {
2028    pub fn passed(exec_time: Duration) -> Self {
2029        TestResult::Passed {
2030            captured: Vec::new(),
2031            exec_time,
2032        }
2033    }
2034
2035    pub fn benchmarked(exec_time: Duration, ns_iter_summ: Summary, mb_s: usize) -> Self {
2036        TestResult::Benchmarked {
2037            captured: Vec::new(),
2038            exec_time,
2039            ns_iter_summ,
2040            mb_s,
2041        }
2042    }
2043
2044    pub fn failed(exec_time: Duration, cause: FailureCause) -> Self {
2045        TestResult::Failed {
2046            cause,
2047            captured: Vec::new(),
2048            exec_time,
2049        }
2050    }
2051
2052    pub fn ignored() -> Self {
2053        TestResult::Ignored {
2054            captured: Vec::new(),
2055        }
2056    }
2057
2058    pub(crate) fn is_passed(&self) -> bool {
2059        matches!(self, TestResult::Passed { .. })
2060    }
2061
2062    pub(crate) fn is_benchmarked(&self) -> bool {
2063        matches!(self, TestResult::Benchmarked { .. })
2064    }
2065
2066    pub(crate) fn is_failed(&self) -> bool {
2067        matches!(self, TestResult::Failed { .. })
2068    }
2069
2070    pub(crate) fn is_ignored(&self) -> bool {
2071        matches!(self, TestResult::Ignored { .. })
2072    }
2073
2074    pub(crate) fn captured_output(&self) -> &Vec<CapturedOutput> {
2075        match self {
2076            TestResult::Passed { captured, .. } => captured,
2077            TestResult::Failed { captured, .. } => captured,
2078            TestResult::Ignored { captured, .. } => captured,
2079            TestResult::Benchmarked { captured, .. } => captured,
2080        }
2081    }
2082
2083    pub(crate) fn stats(&self) -> Option<&Summary> {
2084        match self {
2085            TestResult::Benchmarked { ns_iter_summ, .. } => Some(ns_iter_summ),
2086            _ => None,
2087        }
2088    }
2089
2090    pub(crate) fn set_captured_output(&mut self, captured: Vec<CapturedOutput>) {
2091        match self {
2092            TestResult::Passed {
2093                captured: captured_ref,
2094                ..
2095            } => *captured_ref = captured,
2096            TestResult::Failed {
2097                captured: captured_ref,
2098                ..
2099            } => *captured_ref = captured,
2100            TestResult::Ignored {
2101                captured: captured_ref,
2102            } => *captured_ref = captured,
2103            TestResult::Benchmarked {
2104                captured: captured_ref,
2105                ..
2106            } => *captured_ref = captured,
2107        }
2108    }
2109
2110    pub(crate) fn from_result<A>(
2111        should_panic: &ShouldPanic,
2112        elapsed: Duration,
2113        result: Result<Result<A, FailureCause>, Box<dyn Any + Send>>,
2114    ) -> Self {
2115        match result {
2116            Ok(Ok(_)) => {
2117                if should_panic == &ShouldPanic::No {
2118                    TestResult::passed(elapsed)
2119                } else {
2120                    TestResult::failed(
2121                        elapsed,
2122                        FailureCause::HarnessError("Test did not panic as expected".to_string()),
2123                    )
2124                }
2125            }
2126            Ok(Err(cause)) => TestResult::failed(elapsed, cause),
2127            Err(panic) => TestResult::from_panic(should_panic, elapsed, panic),
2128        }
2129    }
2130
2131    pub(crate) fn from_summary(
2132        should_panic: &ShouldPanic,
2133        elapsed: Duration,
2134        result: Result<Summary, Box<dyn Any + Send>>,
2135        bytes: u64,
2136    ) -> Self {
2137        match result {
2138            Ok(summary) => {
2139                let ns_iter = max(summary.median as u64, 1);
2140                let mb_s = bytes * 1000 / ns_iter;
2141                TestResult::benchmarked(elapsed, summary, mb_s as usize)
2142            }
2143            Err(panic) => Self::from_panic(should_panic, elapsed, panic),
2144        }
2145    }
2146
2147    fn from_panic(
2148        should_panic: &ShouldPanic,
2149        elapsed: Duration,
2150        panic: Box<dyn Any + Send>,
2151    ) -> Self {
2152        let captured = crate::panic_hook::take_current_panic_capture();
2153
2154        let panic_cause = if let Some(cause) = captured {
2155            cause
2156        } else {
2157            let message = panic
2158                .downcast_ref::<String>()
2159                .cloned()
2160                .or(panic.downcast_ref::<&str>().map(|s| s.to_string()));
2161            PanicCause {
2162                message,
2163                location: None,
2164                backtrace: None,
2165            }
2166        };
2167
2168        match should_panic {
2169            ShouldPanic::WithMessage(expected) => match &panic_cause.message {
2170                Some(message) if message.contains(expected) => TestResult::passed(elapsed),
2171                _ => TestResult::failed(
2172                    elapsed,
2173                    FailureCause::Panic(PanicCause {
2174                        message: Some(format!(
2175                            "Test panicked with unexpected message: {}",
2176                            panic_cause.message.as_deref().unwrap_or_default()
2177                        )),
2178                        location: None,
2179                        backtrace: None,
2180                    }),
2181                ),
2182            },
2183            ShouldPanic::Yes => TestResult::passed(elapsed),
2184            ShouldPanic::No => TestResult::failed(elapsed, FailureCause::Panic(panic_cause)),
2185        }
2186    }
2187
2188    pub(crate) fn failure_message(&self) -> Option<String> {
2189        self.failure_cause().map(|c| c.render())
2190    }
2191
2192    pub fn failure_cause(&self) -> Option<&FailureCause> {
2193        match self {
2194            TestResult::Failed { cause, .. } => Some(cause),
2195            _ => None,
2196        }
2197    }
2198}
2199
2200pub struct SuiteResult {
2201    pub passed: usize,
2202    pub failed: usize,
2203    pub ignored: usize,
2204    pub measured: usize,
2205    pub filtered_out: usize,
2206    pub exec_time: Duration,
2207}
2208
2209impl SuiteResult {
2210    pub fn from_test_results(
2211        registered_tests: &[RegisteredTest],
2212        results: &[(RegisteredTest, TestResult)],
2213        exec_time: Duration,
2214    ) -> Self {
2215        let passed = results
2216            .iter()
2217            .filter(|(_, result)| result.is_passed())
2218            .count();
2219        let measured = results
2220            .iter()
2221            .filter(|(_, result)| result.is_benchmarked())
2222            .count();
2223        let failed = results
2224            .iter()
2225            .filter(|(_, result)| result.is_failed())
2226            .count();
2227        let ignored = results
2228            .iter()
2229            .filter(|(_, result)| result.is_ignored())
2230            .count();
2231        let filtered_out = registered_tests.len() - results.len();
2232
2233        Self {
2234            passed,
2235            failed,
2236            ignored,
2237            measured,
2238            filtered_out,
2239            exec_time,
2240        }
2241    }
2242
2243    pub fn exit_code(results: &[(RegisteredTest, TestResult)]) -> ExitCode {
2244        if results.iter().any(|(_, result)| result.is_failed()) {
2245            ExitCode::from(101)
2246        } else {
2247            ExitCode::SUCCESS
2248        }
2249    }
2250}
2251
2252pub trait DependencyView: Debug {
2253    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>>;
2254}
2255
2256impl DependencyView for Arc<dyn DependencyView + Send + Sync> {
2257    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2258        self.as_ref().get(name)
2259    }
2260}
2261
2262#[derive(Debug, Clone, Eq, PartialEq)]
2263pub enum CapturedOutput {
2264    Stdout {
2265        timestamp: SystemTime,
2266        line: String,
2267    },
2268    Stderr {
2269        timestamp: SystemTime,
2270        line: String,
2271    },
2272    /// Host-side output captured in the parent process during this
2273    /// test's execution window. Attribution is overlap-based and
2274    /// best-effort: a line that lands on the parent's redirected
2275    /// stdout/stderr pipe between the parent observing this test
2276    /// start and finish is attributed to it. Sources include
2277    /// `HostedRpc` owner dispatch methods, owner constructors that
2278    /// are still emitting after returning, and any background
2279    /// threads / tasks / subprocesses they spawn.
2280    ///
2281    /// When tests run in parallel a single host-side line may be
2282    /// attributed to multiple tests whose windows overlap.
2283    Host {
2284        timestamp: SystemTime,
2285        line: String,
2286    },
2287}
2288
2289impl CapturedOutput {
2290    pub fn stdout(line: String) -> Self {
2291        CapturedOutput::Stdout {
2292            timestamp: SystemTime::now(),
2293            line,
2294        }
2295    }
2296
2297    pub fn stderr(line: String) -> Self {
2298        CapturedOutput::Stderr {
2299            timestamp: SystemTime::now(),
2300            line,
2301        }
2302    }
2303
2304    /// Constructs a `Host`-tagged capture. Used by the parent's host
2305    /// capture finaliser to inject overlap-attributed host log lines
2306    /// into each test's captured output vec before the formatter
2307    /// renders the suite.
2308    pub fn host(timestamp: SystemTime, line: String) -> Self {
2309        CapturedOutput::Host { timestamp, line }
2310    }
2311
2312    pub fn timestamp(&self) -> SystemTime {
2313        match self {
2314            CapturedOutput::Stdout { timestamp, .. } => *timestamp,
2315            CapturedOutput::Stderr { timestamp, .. } => *timestamp,
2316            CapturedOutput::Host { timestamp, .. } => *timestamp,
2317        }
2318    }
2319
2320    pub fn line(&self) -> &str {
2321        match self {
2322            CapturedOutput::Stdout { line, .. } => line,
2323            CapturedOutput::Stderr { line, .. } => line,
2324            CapturedOutput::Host { line, .. } => line,
2325        }
2326    }
2327}
2328
2329impl PartialOrd for CapturedOutput {
2330    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2331        Some(self.cmp(other))
2332    }
2333}
2334
2335impl Ord for CapturedOutput {
2336    fn cmp(&self, other: &Self) -> Ordering {
2337        self.timestamp().cmp(&other.timestamp())
2338    }
2339}
2340
2341#[cfg(test)]
2342mod error_reporting_tests {
2343    use super::*;
2344    use std::panic::{catch_unwind, AssertUnwindSafe};
2345    use std::time::Duration;
2346
2347    fn simulate_runner(
2348        test_fn: impl FnOnce() -> Box<dyn TestReturnValue> + std::panic::UnwindSafe,
2349    ) -> TestResult {
2350        crate::panic_hook::install_panic_hook();
2351        let test_id = crate::panic_hook::next_test_id();
2352        crate::panic_hook::set_current_test_id(test_id);
2353        let result = catch_unwind(AssertUnwindSafe(move || {
2354            let ret = test_fn();
2355            ret.into_result()?;
2356            Ok(())
2357        }));
2358        let test_result =
2359            TestResult::from_result(&ShouldPanic::No, Duration::from_millis(1), result);
2360        crate::panic_hook::clear_current_test_id();
2361        test_result
2362    }
2363
2364    #[test]
2365    fn panic_with_assert_eq() {
2366        let result = simulate_runner(|| {
2367            assert_eq!(1, 2);
2368            Box::new(())
2369        });
2370        assert!(result.is_failed());
2371        let msg = result.failure_message().unwrap();
2372        println!("=== panic assert_eq failure message ===\n{msg}\n===");
2373        assert!(
2374            msg.contains("assertion `left == right` failed"),
2375            "Expected assertion message, got: {msg}"
2376        );
2377        assert!(
2378            msg.contains("at "),
2379            "Expected location info in message, got: {msg}"
2380        );
2381    }
2382
2383    #[test]
2384    fn string_error() {
2385        let result = simulate_runner(|| {
2386            let r: Result<(), String> = Err("something went wrong".to_string());
2387            Box::new(r)
2388        });
2389        assert!(result.is_failed());
2390        let msg = result.failure_message().unwrap();
2391        println!("=== string error failure message ===\n{msg}\n===");
2392        assert_eq!(msg, "something went wrong");
2393    }
2394
2395    #[test]
2396    fn anyhow_error() {
2397        let result = simulate_runner(|| {
2398            let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2399            let err = anyhow::anyhow!(inner).context("operation failed");
2400            let r: Result<(), anyhow::Error> = Err(err);
2401            Box::new(r)
2402        });
2403        assert!(result.is_failed());
2404        let msg = result.failure_message().unwrap();
2405        println!("=== anyhow error failure message ===\n{msg}\n===");
2406        assert!(
2407            msg.contains("operation failed"),
2408            "Expected 'operation failed', got: {msg}"
2409        );
2410        assert!(
2411            msg.contains("file not found"),
2412            "Expected 'file not found', got: {msg}"
2413        );
2414    }
2415
2416    #[test]
2417    fn std_io_error() {
2418        let result = simulate_runner(|| {
2419            let r: Result<(), std::io::Error> = Err(std::io::Error::new(
2420                std::io::ErrorKind::NotFound,
2421                "file not found",
2422            ));
2423            Box::new(r)
2424        });
2425        assert!(result.is_failed());
2426        let msg = result.failure_message().unwrap();
2427        println!("=== std io error failure message ===\n{msg}\n===");
2428        // Should use Display (not Debug), so no "Custom { kind: NotFound, ... }"
2429        assert_eq!(msg, "file not found");
2430    }
2431
2432    #[test]
2433    fn panic_with_location_info() {
2434        let result = simulate_runner(|| {
2435            panic!("test panic with location");
2436            #[allow(unreachable_code)]
2437            Box::new(())
2438        });
2439        assert!(result.is_failed());
2440        let cause = result.failure_cause().unwrap();
2441        match cause {
2442            FailureCause::Panic(p) => {
2443                assert!(p.location.is_some(), "Expected location info");
2444                let loc = p.location.as_ref().unwrap();
2445                assert!(
2446                    loc.file.contains("internal.rs"),
2447                    "Expected file to contain internal.rs, got: {}",
2448                    loc.file
2449                );
2450                assert!(loc.line > 0, "Expected non-zero line number");
2451            }
2452            other => panic!("Expected Panic cause, got: {other:?}"),
2453        }
2454    }
2455
2456    #[test]
2457    fn panic_render_includes_location() {
2458        let result = simulate_runner(|| {
2459            panic!("location test");
2460            #[allow(unreachable_code)]
2461            Box::new(())
2462        });
2463        let msg = result.failure_message().unwrap();
2464        assert!(
2465            msg.contains("location test"),
2466            "Expected panic message, got: {msg}"
2467        );
2468        assert!(
2469            msg.contains("\n  at "),
2470            "Expected location line in render, got: {msg}"
2471        );
2472    }
2473
2474    #[test]
2475    fn should_panic_with_message_matching() {
2476        crate::panic_hook::install_panic_hook();
2477        let test_id = crate::panic_hook::next_test_id();
2478        crate::panic_hook::set_current_test_id(test_id);
2479        let result = catch_unwind(AssertUnwindSafe(|| {
2480            panic!("expected panic message");
2481        }));
2482        let test_result = TestResult::from_result(
2483            &ShouldPanic::WithMessage("expected panic".to_string()),
2484            Duration::from_millis(1),
2485            result.map(|_| Ok(())),
2486        );
2487        crate::panic_hook::clear_current_test_id();
2488        assert!(
2489            test_result.is_passed(),
2490            "Expected test to pass with matching panic message"
2491        );
2492    }
2493
2494    #[test]
2495    fn should_panic_with_wrong_message() {
2496        crate::panic_hook::install_panic_hook();
2497        let test_id = crate::panic_hook::next_test_id();
2498        crate::panic_hook::set_current_test_id(test_id);
2499        let result = catch_unwind(AssertUnwindSafe(|| {
2500            panic!("actual panic message");
2501        }));
2502        let test_result = TestResult::from_result(
2503            &ShouldPanic::WithMessage("completely different".to_string()),
2504            Duration::from_millis(1),
2505            result.map(|_| Ok(())),
2506        );
2507        crate::panic_hook::clear_current_test_id();
2508        assert!(
2509            test_result.is_failed(),
2510            "Expected test to fail with wrong panic message"
2511        );
2512        let msg = test_result.failure_message().unwrap();
2513        assert!(
2514            msg.contains("unexpected message"),
2515            "Expected 'unexpected message' in: {msg}"
2516        );
2517    }
2518
2519    #[test]
2520    fn pretty_assertions_diff() {
2521        let result = simulate_runner(|| {
2522            pretty_assertions::assert_eq!("hello world\nfoo\nbar\n", "hello world\nbaz\nbar\n");
2523            Box::new(())
2524        });
2525        assert!(result.is_failed());
2526        let cause = result.failure_cause().unwrap();
2527
2528        // Should be a Panic variant (assert_eq! panics)
2529        let panic_cause = match cause {
2530            FailureCause::Panic(p) => p,
2531            other => panic!("Expected Panic cause, got: {other:?}"),
2532        };
2533
2534        // The panic message should contain the colorful diff from pretty_assertions
2535        let message = panic_cause.message.as_deref().unwrap();
2536        println!("=== pretty_assertions failure message ===\n{message}\n===");
2537        assert!(
2538            message.contains("foo") && message.contains("baz"),
2539            "Expected diff with 'foo' and 'baz', got: {message}"
2540        );
2541
2542        // Location should be captured
2543        assert!(panic_cause.location.is_some(), "Expected location info");
2544
2545        // The rendered output should NOT contain backtrace noise when RUST_BACKTRACE is unset
2546        let rendered = cause.render();
2547        println!("=== pretty_assertions rendered ===\n{rendered}\n===");
2548        assert!(
2549            !rendered.contains("stack backtrace") && !rendered.contains("Stack backtrace"),
2550            "Expected no backtrace noise in rendered output, got: {rendered}"
2551        );
2552        // Should contain location
2553        assert!(
2554            rendered.contains("\n  at "),
2555            "Expected location in rendered output, got: {rendered}"
2556        );
2557    }
2558
2559    #[test]
2560    fn detached_thread_panic_detected() {
2561        crate::panic_hook::install_panic_hook();
2562        let test_id = crate::panic_hook::next_test_id();
2563        crate::panic_hook::set_current_test_id(test_id);
2564        crate::panic_hook::create_detached_collector(test_id);
2565
2566        let result = catch_unwind(AssertUnwindSafe(|| {
2567            let handle = crate::spawn::spawn_thread(|| {
2568                panic!("background thread panic");
2569            });
2570            let _ = handle.join();
2571        }));
2572
2573        let mut test_result = TestResult::from_result(
2574            &ShouldPanic::No,
2575            Duration::from_millis(1),
2576            result.map(|_| Ok(())),
2577        );
2578
2579        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
2580            let panics = match collector.lock() {
2581                Ok(p) => p,
2582                Err(poisoned) => poisoned.into_inner(),
2583            };
2584            if !panics.is_empty() && test_result.is_passed() {
2585                let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
2586                test_result = TestResult::failed(
2587                    Duration::from_millis(1),
2588                    FailureCause::Panic(PanicCause {
2589                        message: Some(format!(
2590                            "Detached task(s) panicked:\n{}",
2591                            messages.join("\n---\n")
2592                        )),
2593                        location: panics.first().and_then(|p| p.location.clone()),
2594                        backtrace: panics.first().and_then(|p| p.backtrace.clone()),
2595                    }),
2596                );
2597            }
2598        }
2599
2600        crate::panic_hook::clear_current_test_id();
2601
2602        assert!(
2603            test_result.is_failed(),
2604            "Expected test to fail due to detached panic"
2605        );
2606        let msg = test_result.failure_message().unwrap();
2607        assert!(
2608            msg.contains("Detached task(s) panicked"),
2609            "Expected detached panic message, got: {msg}"
2610        );
2611        assert!(
2612            msg.contains("background thread panic"),
2613            "Expected original panic message, got: {msg}"
2614        );
2615    }
2616
2617    #[test]
2618    fn detached_thread_panic_ignored_with_policy() {
2619        crate::panic_hook::install_panic_hook();
2620        let test_id = crate::panic_hook::next_test_id();
2621        crate::panic_hook::set_current_test_id(test_id);
2622        crate::panic_hook::create_detached_collector(test_id);
2623
2624        let result = catch_unwind(AssertUnwindSafe(|| {
2625            let handle = crate::spawn::spawn_thread(|| {
2626                panic!("ignored thread panic");
2627            });
2628            let _ = handle.join();
2629        }));
2630
2631        let test_result = TestResult::from_result(
2632            &ShouldPanic::No,
2633            Duration::from_millis(1),
2634            result.map(|_| Ok(())),
2635        );
2636
2637        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
2638            let panics = match collector.lock() {
2639                Ok(p) => p,
2640                Err(poisoned) => poisoned.into_inner(),
2641            };
2642            // Verify panics were captured but Ignore policy does not fail the test
2643            assert!(
2644                !panics.is_empty(),
2645                "Expected panics in collector even with Ignore policy"
2646            );
2647        }
2648
2649        crate::panic_hook::clear_current_test_id();
2650
2651        assert!(
2652            test_result.is_passed(),
2653            "Expected test to pass with Ignore policy"
2654        );
2655    }
2656
2657    #[cfg(feature = "tokio")]
2658    #[test]
2659    fn detached_task_panic_detected() {
2660        let rt = tokio::runtime::Runtime::new().unwrap();
2661        rt.block_on(async {
2662            crate::panic_hook::install_panic_hook();
2663            let test_id = crate::panic_hook::next_test_id();
2664            crate::panic_hook::set_current_test_id(test_id);
2665            crate::panic_hook::create_detached_collector(test_id);
2666
2667            let handle = crate::spawn::spawn(async {
2668                panic!("detached task panic");
2669            });
2670            let _ = handle.await;
2671
2672            let collector = crate::panic_hook::take_detached_collector(test_id).unwrap();
2673            let panics = collector.lock().unwrap();
2674
2675            assert_eq!(panics.len(), 1);
2676            assert!(
2677                panics[0]
2678                    .message
2679                    .as_ref()
2680                    .unwrap()
2681                    .contains("detached task panic"),
2682                "Expected panic message, got: {:?}",
2683                panics[0].message
2684            );
2685
2686            crate::panic_hook::clear_current_test_id();
2687        });
2688    }
2689
2690    #[test]
2691    fn failure_cause_variants() {
2692        // ReturnedMessage
2693        let cause = FailureCause::ReturnedMessage("simple message".to_string());
2694        assert_eq!(cause.render(), "simple message");
2695        assert!(cause.panic_message().is_none());
2696
2697        // ReturnedError (prefer display)
2698        let cause = FailureCause::ReturnedError {
2699            display: "display text".to_string(),
2700            debug: "debug text".to_string(),
2701            prefer_debug: false,
2702            error: Arc::new("display text".to_string()),
2703        };
2704        assert_eq!(cause.render(), "display text");
2705
2706        // ReturnedError (prefer debug, e.g. anyhow)
2707        let cause = FailureCause::ReturnedError {
2708            display: "display text".to_string(),
2709            debug: "debug text".to_string(),
2710            prefer_debug: true,
2711            error: Arc::new("debug text".to_string()),
2712        };
2713        assert_eq!(cause.render(), "debug text");
2714
2715        // HarnessError
2716        let cause = FailureCause::HarnessError("harness error".to_string());
2717        assert_eq!(cause.render(), "harness error");
2718
2719        // Panic with message
2720        let cause = FailureCause::Panic(PanicCause {
2721            message: Some("panic msg".to_string()),
2722            location: None,
2723            backtrace: None,
2724        });
2725        assert_eq!(cause.render(), "panic msg");
2726        assert_eq!(cause.panic_message(), Some("panic msg"));
2727    }
2728}
2729
2730#[cfg(test)]
2731mod filter_tests {
2732    use super::*;
2733
2734    fn make_test(name: &str, module_path: &str) -> RegisteredTest {
2735        RegisteredTest {
2736            name: name.to_string(),
2737            crate_name: "mycrate".to_string(),
2738            module_path: module_path.to_string(),
2739            run: TestFunction::Sync(Arc::new(|_| Box::new(()))),
2740            props: TestProperties::default(),
2741            dependencies: None,
2742        }
2743    }
2744
2745    fn make_tagged_test(name: &str, module_path: &str, tags: Vec<&str>) -> RegisteredTest {
2746        let mut test = make_test(name, module_path);
2747        test.props.tags = tags.into_iter().map(String::from).collect();
2748        test
2749    }
2750
2751    fn make_args(filters: Vec<&str>, skip: Vec<&str>, exact: bool) -> Arguments {
2752        Arguments {
2753            filter: filters.into_iter().map(String::from).collect(),
2754            skip: skip.into_iter().map(String::from).collect(),
2755            exact,
2756            ..Default::default()
2757        }
2758    }
2759
2760    fn filtered_names(args: &Arguments, tests: &[RegisteredTest]) -> Vec<String> {
2761        filter_registered_tests(args, tests)
2762            .into_iter()
2763            .map(|t| t.filterable_name())
2764            .collect()
2765    }
2766
2767    // --- filter_test unit tests ---
2768
2769    #[test]
2770    fn filter_test_substring_match() {
2771        let test = make_test("hello_world", "mod1");
2772        assert!(filter_test(&test, "hello", false));
2773        assert!(filter_test(&test, "world", false));
2774        assert!(filter_test(&test, "mod1::hello", false));
2775        assert!(!filter_test(&test, "nonexistent", false));
2776    }
2777
2778    #[test]
2779    fn filter_test_exact_match() {
2780        let test = make_test("hello_world", "mod1");
2781        assert!(filter_test(&test, "mod1::hello_world", true));
2782        assert!(!filter_test(&test, "hello_world", true));
2783        assert!(!filter_test(&test, "hello", true));
2784    }
2785
2786    #[test]
2787    fn filter_test_tag_match() {
2788        let test = make_tagged_test("t1", "mod1", vec!["fast", "unit"]);
2789        assert!(filter_test(&test, ":tag:fast", false));
2790        assert!(filter_test(&test, ":tag:unit", false));
2791        assert!(!filter_test(&test, ":tag:slow", false));
2792    }
2793
2794    #[test]
2795    fn filter_test_tag_empty_matches_untagged() {
2796        let untagged = make_test("t1", "mod1");
2797        let tagged = make_tagged_test("t2", "mod1", vec!["fast"]);
2798        assert!(filter_test(&untagged, ":tag:", false));
2799        assert!(!filter_test(&tagged, ":tag:", false));
2800    }
2801
2802    // --- filter_registered_tests: multiple include filters (OR semantics) ---
2803
2804    #[test]
2805    fn no_filters_includes_all() {
2806        let tests = vec![make_test("a", "m"), make_test("b", "m")];
2807        let args = make_args(vec![], vec![], false);
2808        assert_eq!(filtered_names(&args, &tests), vec!["m::a", "m::b"]);
2809    }
2810
2811    #[test]
2812    fn single_filter_substring() {
2813        let tests = vec![
2814            make_test("alpha", "m"),
2815            make_test("beta", "m"),
2816            make_test("alphabet", "m"),
2817        ];
2818        let args = make_args(vec!["alpha"], vec![], false);
2819        assert_eq!(
2820            filtered_names(&args, &tests),
2821            vec!["m::alpha", "m::alphabet"]
2822        );
2823    }
2824
2825    #[test]
2826    fn multiple_filters_or_semantics() {
2827        let tests = vec![
2828            make_test("alpha", "m"),
2829            make_test("beta", "m"),
2830            make_test("gamma", "m"),
2831        ];
2832        let args = make_args(vec!["alpha", "gamma"], vec![], false);
2833        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::gamma"]);
2834    }
2835
2836    #[test]
2837    fn multiple_filters_exact() {
2838        let tests = vec![
2839            make_test("alpha", "m"),
2840            make_test("alphabet", "m"),
2841            make_test("beta", "m"),
2842        ];
2843        let args = make_args(vec!["m::alpha", "m::beta"], vec![], true);
2844        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::beta"]);
2845    }
2846
2847    // --- skip behavior ---
2848
2849    #[test]
2850    fn skip_substring_match() {
2851        let tests = vec![
2852            make_test("fast_test", "m"),
2853            make_test("slow_test", "m"),
2854            make_test("slower_test", "m"),
2855        ];
2856        let args = make_args(vec![], vec!["slow"], false);
2857        assert_eq!(filtered_names(&args, &tests), vec!["m::fast_test"]);
2858    }
2859
2860    #[test]
2861    fn skip_exact_match() {
2862        let tests = vec![make_test("slow_test", "m"), make_test("slower_test", "m")];
2863        let args = make_args(vec![], vec!["m::slow_test"], true);
2864        assert_eq!(filtered_names(&args, &tests), vec!["m::slower_test"]);
2865    }
2866
2867    #[test]
2868    fn skip_with_tag() {
2869        let tests = vec![
2870            make_tagged_test("t1", "m", vec!["slow"]),
2871            make_tagged_test("t2", "m", vec!["fast"]),
2872            make_test("t3", "m"),
2873        ];
2874        let args = make_args(vec![], vec![":tag:slow"], false);
2875        assert_eq!(filtered_names(&args, &tests), vec!["m::t2", "m::t3"]);
2876    }
2877
2878    // --- combined include + skip ---
2879
2880    #[test]
2881    fn include_and_skip_combined() {
2882        let tests = vec![
2883            make_test("alpha_fast", "m"),
2884            make_test("alpha_slow", "m"),
2885            make_test("beta_fast", "m"),
2886        ];
2887        // Include anything with "alpha", but skip anything with "slow"
2888        let args = make_args(vec!["alpha"], vec!["slow"], false);
2889        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha_fast"]);
2890    }
2891
2892    #[test]
2893    fn skip_wins_over_include() {
2894        let tests = vec![make_test("target", "m")];
2895        // Both include and skip match the same test — skip should win
2896        let args = make_args(vec!["target"], vec!["target"], false);
2897        assert_eq!(filtered_names(&args, &tests), Vec::<String>::new());
2898    }
2899
2900    // --- tag boolean expression syntax ---
2901
2902    #[test]
2903    fn filter_test_tag_or_expression() {
2904        // `:tag:a|b` matches tests tagged with `a` OR `b`
2905        let test_a = make_tagged_test("t1", "m", vec!["a"]);
2906        let test_b = make_tagged_test("t2", "m", vec!["b"]);
2907        let test_c = make_tagged_test("t3", "m", vec!["c"]);
2908        assert!(filter_test(&test_a, ":tag:a|b", false));
2909        assert!(filter_test(&test_b, ":tag:a|b", false));
2910        assert!(!filter_test(&test_c, ":tag:a|b", false));
2911    }
2912
2913    #[test]
2914    fn filter_test_tag_and_expression() {
2915        // `:tag:a&b` matches tests tagged with BOTH `a` AND `b`
2916        let test_ab = make_tagged_test("t1", "m", vec!["a", "b"]);
2917        let test_a = make_tagged_test("t2", "m", vec!["a"]);
2918        let test_b = make_tagged_test("t3", "m", vec!["b"]);
2919        assert!(filter_test(&test_ab, ":tag:a&b", false));
2920        assert!(!filter_test(&test_a, ":tag:a&b", false));
2921        assert!(!filter_test(&test_b, ":tag:a&b", false));
2922    }
2923
2924    #[test]
2925    fn filter_test_tag_mixed_and_or() {
2926        // `:tag:a|b&c` means `a OR (b AND c)` — `&` has higher precedence
2927        let test_a = make_tagged_test("t1", "m", vec!["a"]);
2928        let test_bc = make_tagged_test("t2", "m", vec!["b", "c"]);
2929        let test_b = make_tagged_test("t3", "m", vec!["b"]);
2930        let test_c = make_tagged_test("t4", "m", vec!["c"]);
2931        let test_none = make_test("t5", "m");
2932        assert!(filter_test(&test_a, ":tag:a|b&c", false));
2933        assert!(filter_test(&test_bc, ":tag:a|b&c", false));
2934        assert!(!filter_test(&test_b, ":tag:a|b&c", false));
2935        assert!(!filter_test(&test_c, ":tag:a|b&c", false));
2936        assert!(!filter_test(&test_none, ":tag:a|b&c", false));
2937    }
2938
2939    #[test]
2940    fn filter_test_tag_exact_flag_does_not_affect_tags() {
2941        // `--exact` should not change tag matching behavior
2942        let test = make_tagged_test("t1", "m", vec!["fast"]);
2943        assert!(filter_test(&test, ":tag:fast", true));
2944        assert!(!filter_test(&test, ":tag:slow", true));
2945    }
2946
2947    #[test]
2948    fn include_by_tag_or_expression() {
2949        let tests = vec![
2950            make_tagged_test("t1", "m", vec!["unit"]),
2951            make_tagged_test("t2", "m", vec!["integration"]),
2952            make_tagged_test("t3", "m", vec!["e2e"]),
2953        ];
2954        let args = make_args(vec![":tag:unit|integration"], vec![], false);
2955        assert_eq!(filtered_names(&args, &tests), vec!["m::t1", "m::t2"]);
2956    }
2957
2958    #[test]
2959    fn skip_by_tag_and_expression() {
2960        let tests = vec![
2961            make_tagged_test("t1", "m", vec!["slow", "network"]),
2962            make_tagged_test("t2", "m", vec!["slow"]),
2963            make_tagged_test("t3", "m", vec!["network"]),
2964            make_test("t4", "m"),
2965        ];
2966        // Skip only tests that are BOTH slow AND network
2967        let args = make_args(vec![], vec![":tag:slow&network"], false);
2968        assert_eq!(
2969            filtered_names(&args, &tests),
2970            vec!["m::t2", "m::t3", "m::t4"]
2971        );
2972    }
2973}