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    /// Dispatch one method call synchronously. Catches owner panics and
926    /// turns them into `Err("hosted rpc owner panicked: …")` so the
927    /// dispatcher loop never dies. The lock is acquired *inside* the
928    /// `catch_unwind` closure on purpose: when the owner panics, the
929    /// `MutexGuard` drops during the unwind, which poisons the mutex.
930    /// Every subsequent `dispatch` call then short-circuits with the
931    /// stable `"hosted rpc owner poisoned"` error and does NOT retry the
932    /// (possibly half-mutated) owner.
933    ///
934    /// For cells constructed via [`Self::from_async_owner`], synchronous
935    /// dispatch is unsupported and the call returns
936    /// `Err("hosted rpc owner cell uses the async dispatch path; use dispatch_async or dispatch_blocking")`.
937    /// Note that under the tokio feature a plain `HostedRpcDep` owner may
938    /// also end up in the async cell variant via the blanket bridge, so
939    /// this branch is not strictly limited to user-authored async owners.
940    /// The sync runtime never builds async cells, so this branch only
941    /// fires in misuse cases.
942    pub fn dispatch(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
943        match &self.inner {
944            HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
945            #[cfg(feature = "tokio")]
946            HostedRpcOwnerCellInner::Async(_) => Err(
947                "hosted rpc owner cell uses the async dispatch path; use dispatch_async or dispatch_blocking"
948                    .to_string(),
949            ),
950        }
951    }
952
953    /// Async dispatch entry point used by the tokio runtime's parent-side
954    /// HostedRpc loop and by the in-process transport's `block_on` bridge.
955    /// Works for both `Sync` and `Async` cell variants:
956    ///
957    /// - `Sync` variant: invokes the synchronous dispatcher inline (no
958    ///   `await` actually happens).
959    /// - `Async` variant: awaits the user's async dispatcher with panic
960    ///   capture so an `await`-side panic poisons the cell.
961    #[cfg(feature = "tokio")]
962    pub async fn dispatch_async(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
963        match &self.inner {
964            HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
965            HostedRpcOwnerCellInner::Async(cell) => {
966                async_dispatch_inner(cell, method_idx, args).await
967            }
968        }
969    }
970
971    /// Synchronous bridge to [`Self::dispatch_async`] for sync call sites
972    /// (such as [`InProcessHostedRpcTransport::call`]) that need to feed
973    /// an async owner cell. Drives the future with
974    /// [`tokio::task::block_in_place`] + [`tokio::runtime::Handle::block_on`]
975    /// when an async cell is present, and falls back to the regular sync
976    /// dispatch otherwise.
977    ///
978    /// `Sync` cells short-circuit through the regular sync path with no
979    /// runtime requirement. `Async` cells require a running multi-thread
980    /// Tokio runtime, matching the IPC transport's existing requirement.
981    #[cfg(feature = "tokio")]
982    pub fn dispatch_blocking(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
983        match &self.inner {
984            HostedRpcOwnerCellInner::Sync(mtx) => sync_dispatch_inner(mtx, method_idx, args),
985            HostedRpcOwnerCellInner::Async(cell) => {
986                let handle = tokio::runtime::Handle::try_current().map_err(|_| {
987                    "hosted rpc owner is async-only and no Tokio runtime is active at the dispatch site"
988                        .to_string()
989                })?;
990                // `block_in_place` panics on a `current_thread` runtime
991                // even though `Handle::try_current()` succeeded. Probe
992                // the runtime flavor and return a clean error instead
993                // of hitting the panic — the API contract is
994                // `Result<_, String>`.
995                if !matches!(
996                    handle.runtime_flavor(),
997                    tokio::runtime::RuntimeFlavor::MultiThread
998                ) {
999                    return Err(
1000                        "hosted rpc owner is async-only and the current Tokio runtime is not multi-threaded"
1001                            .to_string(),
1002                    );
1003                }
1004                tokio::task::block_in_place(|| {
1005                    handle.block_on(async_dispatch_inner(cell, method_idx, args))
1006                })
1007            }
1008        }
1009    }
1010}
1011
1012fn sync_dispatch_inner(
1013    mtx: &Mutex<Box<dyn HostedRpcDispatcher>>,
1014    method_idx: u32,
1015    args: &[u8],
1016) -> Result<Vec<u8>, String> {
1017    // The lock acquire lives inside the catch_unwind closure on
1018    // purpose. If we acquired the lock outside and the user dispatch
1019    // panicked, the panic would be caught before the MutexGuard had a
1020    // chance to drop during unwinding, leaving the mutex healthy — and
1021    // we want it poisoned so that subsequent calls see a deterministic
1022    // "owner is dead" error rather than re-entering a half-mutated
1023    // owner value.
1024    let dispatch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1025        let mut guard = match mtx.lock() {
1026            Ok(g) => g,
1027            Err(_) => return Err("hosted rpc owner poisoned".to_string()),
1028        };
1029        guard.dispatch(method_idx, args)
1030    }));
1031    panic_payload_to_err(dispatch_result)
1032}
1033
1034#[cfg(feature = "tokio")]
1035async fn async_dispatch_inner(
1036    cell: &AsyncOwnerCell,
1037    method_idx: u32,
1038    args: &[u8],
1039) -> Result<Vec<u8>, String> {
1040    use futures::FutureExt;
1041    use std::sync::atomic::Ordering;
1042
1043    // Fast-path check: avoids acquiring the async mutex when the owner
1044    // has already been poisoned by an earlier panic.
1045    if cell.poisoned.load(Ordering::SeqCst) {
1046        return Err("hosted rpc owner poisoned".to_string());
1047    }
1048    let mut guard = cell.inner.lock().await;
1049    // Re-check inside the lock: a second dispatch can park on
1050    // `lock().await` *before* the first dispatch panics. Without this
1051    // re-check the second waiter would acquire the lock and re-enter
1052    // the (possibly half-mutated) owner because the poison flag is
1053    // only stored after the panicking task drops its guard. This
1054    // mirrors the std::sync::Mutex poisoning semantics the sync cell
1055    // gets for free.
1056    if cell.poisoned.load(Ordering::SeqCst) {
1057        return Err("hosted rpc owner poisoned".to_string());
1058    }
1059    let fut = std::panic::AssertUnwindSafe(async {
1060        AsyncHostedRpcDispatcher::dispatch(&mut **guard, method_idx, args).await
1061    });
1062    let outcome = fut.catch_unwind().await;
1063    match outcome {
1064        Ok(r) => {
1065            drop(guard);
1066            r
1067        }
1068        Err(payload) => {
1069            // Set the poison flag *while still holding the guard* so any
1070            // waiter that subsequently acquires the mutex sees the flag
1071            // on its in-lock re-check above and short-circuits without
1072            // re-entering the owner.
1073            cell.poisoned.store(true, Ordering::SeqCst);
1074            drop(guard);
1075            let msg = panic_payload_to_string(&payload);
1076            Err(format!("hosted rpc owner panicked: {msg}"))
1077        }
1078    }
1079}
1080
1081fn panic_payload_to_err(
1082    dispatch_result: Result<Result<Vec<u8>, String>, Box<dyn Any + Send>>,
1083) -> Result<Vec<u8>, String> {
1084    match dispatch_result {
1085        Ok(r) => r,
1086        Err(payload) => {
1087            let msg = panic_payload_to_string(&payload);
1088            Err(format!("hosted rpc owner panicked: {msg}"))
1089        }
1090    }
1091}
1092
1093fn panic_payload_to_string(payload: &Box<dyn Any + Send>) -> String {
1094    if let Some(s) = payload.downcast_ref::<&str>() {
1095        (*s).to_string()
1096    } else if let Some(s) = payload.downcast_ref::<String>() {
1097        s.clone()
1098    } else {
1099        "<non-string panic payload>".to_string()
1100    }
1101}
1102
1103/// Support type for `#[test_dep(scope = Hosted, worker = both(T))]`.
1104///
1105/// One macro-emitted `worker = both(T)` registration is lowered into
1106/// **two** `RegisteredDependency` entries that both point at the same
1107/// parent-side owner — one for the descriptor (Hosted) view, one for
1108/// the RPC stub (HostedRpc) view. To keep the owner unique under
1109/// either view, both registrations route through a single
1110/// `HostedBothShared` cell created by the macro-emitted weak cache:
1111///
1112/// - the **descriptor view** asks for the cached descriptor bytes
1113///   (`HostedDep::descriptor` / `AsyncHostedDep::descriptor` is only
1114///   called once, on the first construction);
1115/// - the **RPC view** asks for the inner [`HostedRpcOwnerCell`], so
1116///   the parent-side dispatcher sees the same owner the descriptor
1117///   was derived from.
1118///
1119/// This is intentionally *not* a public end-user type; only the
1120/// macro-support helpers in [`crate::__test_r_make_hosted_both_shared`]
1121/// and friends construct one.
1122pub struct HostedBothShared {
1123    descriptor_bytes: Vec<u8>,
1124    rpc_cell: Arc<HostedRpcOwnerCell>,
1125}
1126
1127impl HostedBothShared {
1128    /// Wrap a pre-computed descriptor + RPC owner cell for the `both`
1129    /// dep variant.
1130    pub fn new(descriptor_bytes: Vec<u8>, rpc_cell: Arc<HostedRpcOwnerCell>) -> Self {
1131        Self {
1132            descriptor_bytes,
1133            rpc_cell,
1134        }
1135    }
1136
1137    /// Borrow the cached descriptor bytes (computed once, on first
1138    /// construction).
1139    pub fn descriptor_bytes(&self) -> &[u8] {
1140        &self.descriptor_bytes
1141    }
1142
1143    /// Cheap clone of the inner RPC owner cell `Arc`. The
1144    /// HostedRpc-view registration's `RpcFactory::owner_into_cell`
1145    /// hands this back to the runtime.
1146    pub fn rpc_cell(&self) -> Arc<HostedRpcOwnerCell> {
1147        self.rpc_cell.clone()
1148    }
1149}
1150
1151/// Error returned by [`HostedRpcChannel::call`] when an RPC fails.
1152#[derive(Debug, Clone)]
1153pub enum HostedRpcError {
1154    /// The owner-side dispatcher returned an error string (unknown method,
1155    /// codec error, panic in the user method, …).
1156    Dispatch(String),
1157    /// The IPC transport itself failed (worker disconnected, framing error,
1158    /// runtime not in spawn-workers mode, …).
1159    Transport(String),
1160}
1161
1162impl std::fmt::Display for HostedRpcError {
1163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1164        match self {
1165            HostedRpcError::Dispatch(s) => write!(f, "hosted rpc dispatch error: {s}"),
1166            HostedRpcError::Transport(s) => write!(f, "hosted rpc transport error: {s}"),
1167        }
1168    }
1169}
1170
1171impl std::error::Error for HostedRpcError {}
1172
1173/// Trait implemented by the per-runner transport that workers use to send
1174/// RPCs to the parent's owner. The runtime provides a concrete IPC
1175/// implementation for the spawn-workers case and a direct in-process
1176/// implementation for `--nocapture` / single-process mode.
1177pub trait HostedRpcTransport: Send + Sync {
1178    /// Send one call and block until the reply arrives. `dep_id` is the
1179    /// dep's fully-qualified id (`{crate}::{module}::{name}`) used by the
1180    /// parent to route the call to the right owner.
1181    fn call(&self, dep_id: &str, method_idx: u32, args: Vec<u8>)
1182        -> Result<Vec<u8>, HostedRpcError>;
1183}
1184
1185/// Per-dep channel handed to [`HostedRpcDep::build_stub`] on the worker side.
1186///
1187/// The stub holds this channel and calls [`HostedRpcChannel::call`] from
1188/// each of its method bodies; the channel takes care of dep-id routing,
1189/// serialization framing, and waiting for the parent's reply.
1190pub struct HostedRpcChannel {
1191    dep_id: String,
1192    transport: Arc<dyn HostedRpcTransport>,
1193}
1194
1195impl HostedRpcChannel {
1196    /// Construct a channel that targets the dep identified by
1197    /// `dep_id` (a fully-qualified id) and uses the supplied transport.
1198    pub fn new(dep_id: String, transport: Arc<dyn HostedRpcTransport>) -> Self {
1199        Self { dep_id, transport }
1200    }
1201
1202    /// The fully-qualified dep id this channel routes to. Stubs almost never
1203    /// need this directly, but it's exposed for diagnostics and tests.
1204    pub fn dep_id(&self) -> &str {
1205        &self.dep_id
1206    }
1207
1208    /// Send one method call and block until the parent replies. `args` are
1209    /// already-serialized bytes; the stub method body owns the choice of
1210    /// codec.
1211    ///
1212    /// **Temporal invariant — only call this while a test body is actually
1213    /// running.** The transport assumes one
1214    /// HostedRpc request/reply pair per worker subprocess is in flight
1215    /// at a time *and* that the worker's main IPC command loop is idle
1216    /// (it only reads `Provide*` / `RunTest` between tests). Specifically:
1217    ///
1218    /// - **Do NOT call from `HostedRpcDep::build_stub`** — see that
1219    ///   method's docs for why.
1220    /// - **Do NOT call from background threads or detached tasks that
1221    ///   outlive the test body** — once the test returns the worker
1222    ///   sends `TestFinished` and the parent's next message will be a
1223    ///   `Provide*` / `RunTest`, which the transport's read side would
1224    ///   then misinterpret as a reply.
1225    /// - **Do NOT call from `Drop` / destructor-style cleanup or any
1226    ///   teardown hook that may fire after the test body has returned** —
1227    ///   that is just another form of "outside the test body" and has the
1228    ///   same IPC-framing-desync risk as a detached background thread.
1229    /// - Stub calls from inside the test body — directly or transitively
1230    ///   from helpers the test body awaits/blocks on — are the supported
1231    ///   shape.
1232    pub fn call(&self, method_idx: u32, args: Vec<u8>) -> Result<Vec<u8>, HostedRpcError> {
1233        self.transport.call(&self.dep_id, method_idx, args)
1234    }
1235}
1236
1237impl Clone for HostedRpcChannel {
1238    fn clone(&self) -> Self {
1239        Self {
1240            dep_id: self.dep_id.clone(),
1241            transport: self.transport.clone(),
1242        }
1243    }
1244}
1245
1246/// In-process transport used in `--nocapture` / single-process mode: the
1247/// stub calls the owner-side [`HostedRpcOwnerCell`] directly without
1248/// touching any IPC stream.
1249pub struct InProcessHostedRpcTransport {
1250    cells: HashMap<String, Arc<HostedRpcOwnerCell>>,
1251}
1252
1253impl InProcessHostedRpcTransport {
1254    pub fn new(cells: HashMap<String, Arc<HostedRpcOwnerCell>>) -> Self {
1255        Self { cells }
1256    }
1257}
1258
1259impl HostedRpcTransport for InProcessHostedRpcTransport {
1260    fn call(
1261        &self,
1262        dep_id: &str,
1263        method_idx: u32,
1264        args: Vec<u8>,
1265    ) -> Result<Vec<u8>, HostedRpcError> {
1266        let cell = self.cells.get(dep_id).ok_or_else(|| {
1267            HostedRpcError::Transport(format!("in-process HostedRpc: unknown dep id '{dep_id}'"))
1268        })?;
1269        // Under the tokio feature, route through `dispatch_blocking` so async
1270        // owners (and bridged sync owners stored in `Async` cells) are driven
1271        // by the surrounding multi-thread tokio runtime. Without the tokio
1272        // feature only sync cells exist, so the plain sync `dispatch` is fine.
1273        #[cfg(feature = "tokio")]
1274        let result = cell.dispatch_blocking(method_idx, &args);
1275        #[cfg(not(feature = "tokio"))]
1276        let result = cell.dispatch(method_idx, &args);
1277        result.map_err(HostedRpcError::Dispatch)
1278    }
1279}
1280
1281/// Factory pair stored on a `HostedRpc` [`RegisteredDependency`]. The macro
1282/// emits a `RpcFactory` per registered HostedRpc dep so the runtime can
1283/// (a) wrap the constructor's output into a parent dispatcher cell, and
1284/// (b) build a worker-side stub from a channel.
1285#[derive(Clone)]
1286#[allow(clippy::type_complexity)]
1287pub struct RpcFactory {
1288    /// Downcast the constructor's `Arc<dyn Any>` to the concrete
1289    /// `HostedRpcOwnerCell` for this dep.
1290    pub owner_into_cell: Arc<
1291        dyn (Fn(Arc<dyn Any + Send + Sync>) -> Arc<HostedRpcOwnerCell>) + Send + Sync + 'static,
1292    >,
1293    /// Build a worker-side stub (typed as the dep's `Stub` associated type)
1294    /// from the supplied channel, boxed as `Arc<dyn Any>`.
1295    pub build_stub:
1296        Arc<dyn (Fn(HostedRpcChannel) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
1297}
1298
1299/// Sharing strategy declared on a `#[test_dep]`. Controls how the dependency
1300/// interacts with output capturing and parallel test execution.
1301///
1302/// See `book/src/design/sharing-strategy.md` for the full description.
1303#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
1304pub enum DepScope {
1305    /// Today's behaviour: a single materialized instance shared by every test.
1306    /// Forces single-threaded execution when output capturing is on, because
1307    /// the `Arc<dyn Any>` cannot cross the parent/worker process boundary.
1308    #[default]
1309    Shared,
1310    /// Each worker child materializes its own instance independently. Tests
1311    /// inside one worker share the instance.
1312    PerWorker,
1313    /// Parent runs the constructor once and produces wire bytes; each worker
1314    /// reconstructs a local instance from those bytes via the registered
1315    /// worker reconstructor (`worker_fn`).
1316    Cloneable,
1317    /// Owner runs once in the **parent** test runner process and stays alive
1318    /// for the entire suite. The parent produces a descriptor (via
1319    /// [`HostedDep::descriptor`]) and ships those descriptor bytes to every
1320    /// worker. Each worker reconstructs a handle via
1321    /// [`HostedDep::from_descriptor`]. The owner is held in the parent
1322    /// process so singleton services (TCP listeners, Docker containers,
1323    /// gRPC server clients, env-based runtimes) are not duplicated per
1324    /// worker.
1325    Hosted,
1326    /// Like [`Self::Hosted`], but the owner stays in the parent AND workers
1327    /// talk back to it via the runtime's built-in RPC layer instead of reaching
1328    /// out via their own transport. The dep implementor provides a
1329    /// [`HostedRpcDep`] impl (sync owners) — or, under the `tokio` feature,
1330    /// an [`AsyncHostedRpcDep`] impl (async owners) — on the owner type
1331    /// with a stub type, a method dispatch function, and a stub builder.
1332    HostedRpc,
1333}
1334
1335impl DepScope {
1336    /// Returns `true` for scopes that materialize in the parent process and
1337    /// therefore force single-threaded fallback when capturing is on.
1338    pub fn requires_single_thread_when_capturing(&self) -> bool {
1339        matches!(self, DepScope::Shared)
1340    }
1341
1342    /// Returns `true` for scopes the parent should still materialize even
1343    /// when it is otherwise delegating dependency construction to workers
1344    /// (i.e. `skip_creating_dependencies` is set). `Cloneable` deps need the
1345    /// parent to compute the wire form; `Hosted` / `HostedRpc` deps need
1346    /// the parent to hold the owner alive for the whole suite.
1347    pub fn parent_must_materialize_under_spawn_workers(&self) -> bool {
1348        matches!(
1349            self,
1350            DepScope::Cloneable | DepScope::Hosted | DepScope::HostedRpc
1351        )
1352    }
1353}
1354
1355/// Function pointer-equivalent used by the worker side of a `Cloneable`
1356/// dependency. Receives the deserialized wire payload (boxed as `Any` for
1357/// type erasure) plus the current dependency view, and produces the
1358/// reconstructed worker-side value.
1359#[derive(Clone)]
1360#[allow(clippy::type_complexity)]
1361pub enum WorkerReconstructor {
1362    Sync(
1363        Arc<
1364            dyn (Fn(
1365                    Arc<dyn Any + Send + Sync>,
1366                    Arc<dyn DependencyView + Send + Sync>,
1367                ) -> Arc<dyn Any + Send + Sync + 'static>)
1368                + Send
1369                + Sync
1370                + 'static,
1371        >,
1372    ),
1373    Async(
1374        Arc<
1375            dyn (Fn(
1376                    Arc<dyn Any + Send + Sync>,
1377                    Arc<dyn DependencyView + Send + Sync>,
1378                ) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
1379                + Send
1380                + Sync
1381                + 'static,
1382        >,
1383    ),
1384}
1385
1386/// Function-pointer wrappers used by Cloneable deps to convert the
1387/// constructed value into wire bytes on the parent, and to deserialize those
1388/// bytes into a typed value on the worker.
1389#[derive(Clone)]
1390#[allow(clippy::type_complexity)]
1391pub struct CloneableCodec {
1392    /// Parent-side: `to_wire`. Receives the dependency value as `Arc<dyn Any>`,
1393    /// returns the encoded wire bytes.
1394    pub to_wire: Arc<dyn (Fn(Arc<dyn Any + Send + Sync>) -> Vec<u8>) + Send + Sync + 'static>,
1395    /// Worker-side: deserialize wire bytes into the boxed `Wire` payload that
1396    /// is then fed to the [`WorkerReconstructor`].
1397    pub from_wire_bytes: Arc<dyn (Fn(&[u8]) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
1398}
1399
1400#[derive(Clone)]
1401pub struct RegisteredDependency {
1402    pub name: String, // TODO: Should we use TypeId here?
1403    pub crate_name: String,
1404    pub module_path: String,
1405    pub constructor: DependencyConstructor,
1406    pub dependencies: Vec<String>,
1407    /// Sharing strategy declared on the constructor. Defaults to
1408    /// [`DepScope::Shared`] for backward compatibility.
1409    pub scope: DepScope,
1410    /// Worker-side reconstructor for `Cloneable` and `Hosted` deps
1411    /// (`None` otherwise). For `Cloneable` the wire payload IS the dep value;
1412    /// for `Hosted` the wire payload is the descriptor passed to
1413    /// [`HostedDep::from_descriptor`](crate::internal::HostedDep::from_descriptor).
1414    pub worker_fn: Option<WorkerReconstructor>,
1415    /// Wire-bytes codec for `Cloneable` deps (`None` otherwise). The codec
1416    /// shape is shared with [`Self::hosted_codec`] but the runtime dispatches
1417    /// on whichever field is populated.
1418    pub cloneable_codec: Option<CloneableCodec>,
1419    /// Descriptor-bytes codec for `Hosted` deps (`None` otherwise). Same
1420    /// shape as [`Self::cloneable_codec`]; the codec encodes the value
1421    /// returned by [`HostedDep::descriptor`](crate::internal::HostedDep::descriptor)
1422    /// into wire bytes on the parent (where the owner lives), and decodes
1423    /// those bytes in the worker before they are passed to the registered
1424    /// worker reconstructor.
1425    pub hosted_codec: Option<CloneableCodec>,
1426    /// Factories for `HostedRpc` deps (`None` otherwise). The parent uses
1427    /// [`RpcFactory::owner_into_cell`] to extract the `HostedRpcOwnerCell`
1428    /// returned by the constructor; the worker uses [`RpcFactory::build_stub`]
1429    /// to construct its `Stub` from a fresh [`HostedRpcChannel`].
1430    pub rpc_factory: Option<RpcFactory>,
1431    /// Planner-only sibling dep names that must be retained together
1432    /// with this dep during pruning. Unlike `dependencies`, companions
1433    /// are **not** real dependency edges — no constructor argument is
1434    /// derived from a companion, and no topological ordering is
1435    /// implied. The pruner simply treats companions as mutually
1436    /// reachable: if any companion in a group is in the keep-set, the
1437    /// whole group is retained.
1438    ///
1439    /// Currently set by the `#[test_dep(scope = Hosted, worker = both(T))]`
1440    /// macro lowering, which registers two paired dep entries (the
1441    /// Hosted owner view and the HostedRpc stub view) backed by the
1442    /// same parent-side `Arc<HostedBothShared>` cache. The async
1443    /// flavour of that lowering has a sync resolver on the stub side
1444    /// that assumes the Hosted side has already populated the shared
1445    /// cache; if pruning ever dropped the Hosted half because the
1446    /// selected tests only parameterised on the stub view, that
1447    /// resolver would panic. Pairing the two as companions guarantees
1448    /// the Hosted half is retained whenever either half is needed.
1449    pub companions: Vec<String>,
1450}
1451
1452impl RegisteredDependency {
1453    /// Construct a `Shared` (legacy / default-scope) dependency. Preserves the
1454    /// pre-scopes constructor signature so downstream code that built
1455    /// `RegisteredDependency` directly keeps compiling.
1456    pub fn new_shared(
1457        name: String,
1458        crate_name: String,
1459        module_path: String,
1460        constructor: DependencyConstructor,
1461        dependencies: Vec<String>,
1462    ) -> Self {
1463        Self {
1464            name,
1465            crate_name,
1466            module_path,
1467            constructor,
1468            dependencies,
1469            scope: DepScope::Shared,
1470            worker_fn: None,
1471            cloneable_codec: None,
1472            hosted_codec: None,
1473            rpc_factory: None,
1474            companions: Vec::new(),
1475        }
1476    }
1477}
1478
1479impl Debug for RegisteredDependency {
1480    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1481        f.debug_struct("RegisteredDependency")
1482            .field("name", &self.name)
1483            .field("crate_name", &self.crate_name)
1484            .field("module_path", &self.module_path)
1485            .finish()
1486    }
1487}
1488
1489impl PartialEq for RegisteredDependency {
1490    fn eq(&self, other: &Self) -> bool {
1491        self.name == other.name
1492    }
1493}
1494
1495impl Eq for RegisteredDependency {}
1496
1497impl Hash for RegisteredDependency {
1498    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1499        self.name.hash(state);
1500    }
1501}
1502
1503impl RegisteredDependency {
1504    pub fn crate_and_module(&self) -> String {
1505        [&self.crate_name, &self.module_path]
1506            .into_iter()
1507            .filter(|s| !s.is_empty())
1508            .cloned()
1509            .collect::<Vec<String>>()
1510            .join("::")
1511    }
1512
1513    /// Fully-qualified identifier used for cross-process bookkeeping of
1514    /// Cloneable dependencies. The shape is `{crate_name}::{module_path}::{name}`
1515    /// with empty segments dropped, so two deps with the same `name` registered
1516    /// in different modules get distinct identifiers.
1517    pub fn qualified_id(&self) -> String {
1518        [&self.crate_name, &self.module_path, &self.name]
1519            .into_iter()
1520            .filter(|s| !s.is_empty())
1521            .cloned()
1522            .collect::<Vec<String>>()
1523            .join("::")
1524    }
1525}
1526
1527pub static REGISTERED_DEPENDENCY_CONSTRUCTORS: Mutex<Vec<RegisteredDependency>> =
1528    Mutex::new(Vec::new());
1529
1530#[derive(Debug, Clone)]
1531pub enum RegisteredTestSuiteProperty {
1532    Sequential {
1533        name: String,
1534        crate_name: String,
1535        module_path: String,
1536    },
1537    Tag {
1538        name: String,
1539        crate_name: String,
1540        module_path: String,
1541        tag: String,
1542    },
1543    Timeout {
1544        name: String,
1545        crate_name: String,
1546        module_path: String,
1547        timeout: Duration,
1548    },
1549}
1550
1551impl RegisteredTestSuiteProperty {
1552    pub fn crate_name(&self) -> &String {
1553        match self {
1554            RegisteredTestSuiteProperty::Sequential { crate_name, .. } => crate_name,
1555            RegisteredTestSuiteProperty::Tag { crate_name, .. } => crate_name,
1556            RegisteredTestSuiteProperty::Timeout { crate_name, .. } => crate_name,
1557        }
1558    }
1559
1560    pub fn module_path(&self) -> &String {
1561        match self {
1562            RegisteredTestSuiteProperty::Sequential { module_path, .. } => module_path,
1563            RegisteredTestSuiteProperty::Tag { module_path, .. } => module_path,
1564            RegisteredTestSuiteProperty::Timeout { module_path, .. } => module_path,
1565        }
1566    }
1567
1568    pub fn name(&self) -> &String {
1569        match self {
1570            RegisteredTestSuiteProperty::Sequential { name, .. } => name,
1571            RegisteredTestSuiteProperty::Tag { name, .. } => name,
1572            RegisteredTestSuiteProperty::Timeout { name, .. } => name,
1573        }
1574    }
1575
1576    pub fn crate_and_module(&self) -> String {
1577        [self.crate_name(), self.module_path(), self.name()]
1578            .into_iter()
1579            .filter(|s| !s.is_empty())
1580            .cloned()
1581            .collect::<Vec<String>>()
1582            .join("::")
1583    }
1584}
1585
1586pub static REGISTERED_TESTSUITE_PROPS: Mutex<Vec<RegisteredTestSuiteProperty>> =
1587    Mutex::new(Vec::new());
1588
1589#[derive(Clone)]
1590#[allow(clippy::type_complexity)]
1591pub enum TestGeneratorFunction {
1592    Sync(Arc<dyn Fn() -> Vec<GeneratedTest> + Send + Sync + 'static>),
1593    Async(
1594        Arc<
1595            dyn (Fn() -> Pin<Box<dyn Future<Output = Vec<GeneratedTest>> + Send>>)
1596                + Send
1597                + Sync
1598                + 'static,
1599        >,
1600    ),
1601}
1602
1603pub struct DynamicTestRegistration {
1604    tests: Vec<GeneratedTest>,
1605}
1606
1607impl Default for DynamicTestRegistration {
1608    fn default() -> Self {
1609        Self::new()
1610    }
1611}
1612
1613impl DynamicTestRegistration {
1614    pub fn new() -> Self {
1615        Self { tests: Vec::new() }
1616    }
1617
1618    pub fn to_vec(self) -> Vec<GeneratedTest> {
1619        self.tests
1620    }
1621
1622    pub fn add_sync_test<R: TestReturnValue + 'static>(
1623        &mut self,
1624        name: impl AsRef<str>,
1625        props: TestProperties,
1626        dependencies: Option<Vec<String>>,
1627        run: impl Fn(Arc<dyn DependencyView + Send + Sync>) -> R + Send + Sync + Clone + 'static,
1628    ) {
1629        self.tests.push(GeneratedTest {
1630            name: name.as_ref().to_string(),
1631            run: TestFunction::Sync(Arc::new(move |deps| {
1632                Box::new(run(deps)) as Box<dyn TestReturnValue>
1633            })),
1634            props,
1635            dependencies,
1636        });
1637    }
1638
1639    #[cfg(feature = "tokio")]
1640    pub fn add_async_test<R: TestReturnValue + 'static>(
1641        &mut self,
1642        name: impl AsRef<str>,
1643        props: TestProperties,
1644        dependencies: Option<Vec<String>>,
1645        run: impl (Fn(Arc<dyn DependencyView + Send + Sync>) -> Pin<Box<dyn Future<Output = R> + Send>>)
1646            + Send
1647            + Sync
1648            + Clone
1649            + 'static,
1650    ) {
1651        self.tests.push(GeneratedTest {
1652            name: name.as_ref().to_string(),
1653            run: TestFunction::Async(Arc::new(move |deps| {
1654                let run = run.clone();
1655                Box::pin(async move {
1656                    let r = run(deps).await;
1657                    Box::new(r) as Box<dyn TestReturnValue>
1658                })
1659            })),
1660            props,
1661            dependencies,
1662        });
1663    }
1664}
1665
1666#[derive(Clone)]
1667pub struct GeneratedTest {
1668    pub name: String,
1669    pub run: TestFunction,
1670    pub props: TestProperties,
1671    pub dependencies: Option<Vec<String>>,
1672}
1673
1674#[derive(Clone)]
1675pub struct RegisteredTestGenerator {
1676    pub name: String,
1677    pub crate_name: String,
1678    pub module_path: String,
1679    pub run: TestGeneratorFunction,
1680    pub is_ignored: bool,
1681}
1682
1683impl RegisteredTestGenerator {
1684    pub fn crate_and_module(&self) -> String {
1685        [&self.crate_name, &self.module_path]
1686            .into_iter()
1687            .filter(|s| !s.is_empty())
1688            .cloned()
1689            .collect::<Vec<String>>()
1690            .join("::")
1691    }
1692}
1693
1694pub static REGISTERED_TEST_GENERATORS: Mutex<Vec<RegisteredTestGenerator>> = Mutex::new(Vec::new());
1695
1696pub(crate) fn filter_test(test: &RegisteredTest, filter: &str, exact: bool) -> bool {
1697    if let Some(tag_list) = filter.strip_prefix(":tag:") {
1698        if tag_list.is_empty() {
1699            // Filtering for tags with NO TAGS
1700            test.props.tags.is_empty()
1701        } else {
1702            let or_tags = tag_list.split('|').collect::<Vec<&str>>();
1703            let mut result = false;
1704            for or_tag in or_tags {
1705                let and_tags = or_tag.split('&').collect::<Vec<&str>>();
1706                let mut and_result = true;
1707                for and_tag in and_tags {
1708                    if !test.props.tags.contains(&and_tag.to_string()) {
1709                        and_result = false;
1710                        break;
1711                    }
1712                }
1713                if and_result {
1714                    result = true;
1715                    break;
1716                }
1717            }
1718            result
1719        }
1720    } else if exact {
1721        test.filterable_name() == filter
1722    } else {
1723        test.filterable_name().contains(filter)
1724    }
1725}
1726
1727pub(crate) fn apply_suite_props_to_tests(
1728    tests: &[RegisteredTest],
1729    props: &[RegisteredTestSuiteProperty],
1730) -> Vec<RegisteredTest> {
1731    let props_with_prefix = props
1732        .iter()
1733        .map(|prop| (prop.crate_and_module(), prop))
1734        .collect::<Vec<_>>();
1735
1736    let mut result = Vec::new();
1737    for test in tests {
1738        let mut test = test.clone();
1739        for (prefix, prop) in &props_with_prefix {
1740            if test.crate_and_module().starts_with(prefix) {
1741                match prop {
1742                    RegisteredTestSuiteProperty::Tag { tag, .. } => {
1743                        test.props.tags.push(tag.clone());
1744                    }
1745                    RegisteredTestSuiteProperty::Timeout { timeout, .. } => {
1746                        if test.props.timeout.is_none() {
1747                            test.props.timeout = Some(*timeout);
1748                        }
1749                    }
1750                    RegisteredTestSuiteProperty::Sequential { .. } => {
1751                        // handled in TestSuiteExecution
1752                    }
1753                }
1754            }
1755        }
1756        result.push(test);
1757    }
1758    result
1759}
1760
1761pub(crate) fn filter_registered_tests(
1762    args: &Arguments,
1763    registered_tests: &[RegisteredTest],
1764) -> Vec<RegisteredTest> {
1765    registered_tests
1766        .iter()
1767        .filter(|registered_test| {
1768            !args
1769                .skip
1770                .iter()
1771                .any(|skip| filter_test(registered_test, skip, args.exact))
1772        })
1773        .filter(|registered_test| {
1774            args.filter.is_empty()
1775                || args
1776                    .filter
1777                    .iter()
1778                    .any(|filter| filter_test(registered_test, filter, args.exact))
1779        })
1780        .filter(|registered_tests| {
1781            (args.bench && registered_tests.run.is_bench())
1782                || (args.test && !registered_tests.run.is_bench())
1783                || (!args.bench && !args.test)
1784        })
1785        .filter(|registered_test| {
1786            !args.exclude_should_panic || registered_test.props.should_panic == ShouldPanic::No
1787        })
1788        .cloned()
1789        .collect::<Vec<_>>()
1790}
1791
1792fn add_generated_tests(
1793    target: &mut Vec<RegisteredTest>,
1794    generator: &RegisteredTestGenerator,
1795    generated: Vec<GeneratedTest>,
1796) {
1797    target.extend(generated.into_iter().map(|mut test| {
1798        test.props.is_ignored |= generator.is_ignored;
1799        RegisteredTest {
1800            name: format!("{}::{}", generator.name, test.name),
1801            crate_name: generator.crate_name.clone(),
1802            module_path: generator.module_path.clone(),
1803            run: test.run,
1804            props: test.props,
1805            dependencies: test.dependencies,
1806        }
1807    }));
1808}
1809
1810#[cfg(feature = "tokio")]
1811pub(crate) async fn generate_tests(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
1812    let mut result = Vec::new();
1813    for generator in generators {
1814        match &generator.run {
1815            TestGeneratorFunction::Sync(generator_fn) => {
1816                let tests = generator_fn();
1817                add_generated_tests(&mut result, generator, tests);
1818            }
1819            TestGeneratorFunction::Async(generator_fn) => {
1820                let tests = generator_fn().await;
1821                add_generated_tests(&mut result, generator, tests);
1822            }
1823        }
1824    }
1825    result
1826}
1827
1828pub(crate) fn generate_tests_sync(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
1829    let mut result = Vec::new();
1830    for generator in generators {
1831        match &generator.run {
1832            TestGeneratorFunction::Sync(generator_fn) => {
1833                let tests = generator_fn();
1834                add_generated_tests(&mut result, generator, tests);
1835            }
1836            TestGeneratorFunction::Async(_) => {
1837                panic!("Async test generators are not supported in sync mode")
1838            }
1839        }
1840    }
1841    result
1842}
1843
1844pub(crate) fn get_ensure_time(args: &Arguments, test: &RegisteredTest) -> Option<TimeThreshold> {
1845    let should_ensure_time = match test.props.ensure_time_control {
1846        ReportTimeControl::Default => args.ensure_time,
1847        ReportTimeControl::Enabled => true,
1848        ReportTimeControl::Disabled => false,
1849    };
1850    if should_ensure_time {
1851        match test.props.test_type {
1852            TestType::UnitTest => Some(args.unit_test_threshold()),
1853            TestType::IntegrationTest => Some(args.integration_test_threshold()),
1854        }
1855    } else {
1856        None
1857    }
1858}
1859
1860#[derive(Clone)]
1861pub enum TestResult {
1862    Passed {
1863        captured: Vec<CapturedOutput>,
1864        exec_time: Duration,
1865    },
1866    Benchmarked {
1867        captured: Vec<CapturedOutput>,
1868        exec_time: Duration,
1869        ns_iter_summ: Summary,
1870        mb_s: usize,
1871    },
1872    Failed {
1873        cause: FailureCause,
1874        captured: Vec<CapturedOutput>,
1875        exec_time: Duration,
1876    },
1877    Ignored {
1878        captured: Vec<CapturedOutput>,
1879    },
1880}
1881
1882impl TestResult {
1883    pub fn passed(exec_time: Duration) -> Self {
1884        TestResult::Passed {
1885            captured: Vec::new(),
1886            exec_time,
1887        }
1888    }
1889
1890    pub fn benchmarked(exec_time: Duration, ns_iter_summ: Summary, mb_s: usize) -> Self {
1891        TestResult::Benchmarked {
1892            captured: Vec::new(),
1893            exec_time,
1894            ns_iter_summ,
1895            mb_s,
1896        }
1897    }
1898
1899    pub fn failed(exec_time: Duration, cause: FailureCause) -> Self {
1900        TestResult::Failed {
1901            cause,
1902            captured: Vec::new(),
1903            exec_time,
1904        }
1905    }
1906
1907    pub fn ignored() -> Self {
1908        TestResult::Ignored {
1909            captured: Vec::new(),
1910        }
1911    }
1912
1913    pub(crate) fn is_passed(&self) -> bool {
1914        matches!(self, TestResult::Passed { .. })
1915    }
1916
1917    pub(crate) fn is_benchmarked(&self) -> bool {
1918        matches!(self, TestResult::Benchmarked { .. })
1919    }
1920
1921    pub(crate) fn is_failed(&self) -> bool {
1922        matches!(self, TestResult::Failed { .. })
1923    }
1924
1925    pub(crate) fn is_ignored(&self) -> bool {
1926        matches!(self, TestResult::Ignored { .. })
1927    }
1928
1929    pub(crate) fn captured_output(&self) -> &Vec<CapturedOutput> {
1930        match self {
1931            TestResult::Passed { captured, .. } => captured,
1932            TestResult::Failed { captured, .. } => captured,
1933            TestResult::Ignored { captured, .. } => captured,
1934            TestResult::Benchmarked { captured, .. } => captured,
1935        }
1936    }
1937
1938    pub(crate) fn stats(&self) -> Option<&Summary> {
1939        match self {
1940            TestResult::Benchmarked { ns_iter_summ, .. } => Some(ns_iter_summ),
1941            _ => None,
1942        }
1943    }
1944
1945    pub(crate) fn set_captured_output(&mut self, captured: Vec<CapturedOutput>) {
1946        match self {
1947            TestResult::Passed {
1948                captured: captured_ref,
1949                ..
1950            } => *captured_ref = captured,
1951            TestResult::Failed {
1952                captured: captured_ref,
1953                ..
1954            } => *captured_ref = captured,
1955            TestResult::Ignored {
1956                captured: captured_ref,
1957            } => *captured_ref = captured,
1958            TestResult::Benchmarked {
1959                captured: captured_ref,
1960                ..
1961            } => *captured_ref = captured,
1962        }
1963    }
1964
1965    pub(crate) fn from_result<A>(
1966        should_panic: &ShouldPanic,
1967        elapsed: Duration,
1968        result: Result<Result<A, FailureCause>, Box<dyn Any + Send>>,
1969    ) -> Self {
1970        match result {
1971            Ok(Ok(_)) => {
1972                if should_panic == &ShouldPanic::No {
1973                    TestResult::passed(elapsed)
1974                } else {
1975                    TestResult::failed(
1976                        elapsed,
1977                        FailureCause::HarnessError("Test did not panic as expected".to_string()),
1978                    )
1979                }
1980            }
1981            Ok(Err(cause)) => TestResult::failed(elapsed, cause),
1982            Err(panic) => TestResult::from_panic(should_panic, elapsed, panic),
1983        }
1984    }
1985
1986    pub(crate) fn from_summary(
1987        should_panic: &ShouldPanic,
1988        elapsed: Duration,
1989        result: Result<Summary, Box<dyn Any + Send>>,
1990        bytes: u64,
1991    ) -> Self {
1992        match result {
1993            Ok(summary) => {
1994                let ns_iter = max(summary.median as u64, 1);
1995                let mb_s = bytes * 1000 / ns_iter;
1996                TestResult::benchmarked(elapsed, summary, mb_s as usize)
1997            }
1998            Err(panic) => Self::from_panic(should_panic, elapsed, panic),
1999        }
2000    }
2001
2002    fn from_panic(
2003        should_panic: &ShouldPanic,
2004        elapsed: Duration,
2005        panic: Box<dyn Any + Send>,
2006    ) -> Self {
2007        let captured = crate::panic_hook::take_current_panic_capture();
2008
2009        let panic_cause = if let Some(cause) = captured {
2010            cause
2011        } else {
2012            let message = panic
2013                .downcast_ref::<String>()
2014                .cloned()
2015                .or(panic.downcast_ref::<&str>().map(|s| s.to_string()));
2016            PanicCause {
2017                message,
2018                location: None,
2019                backtrace: None,
2020            }
2021        };
2022
2023        match should_panic {
2024            ShouldPanic::WithMessage(expected) => match &panic_cause.message {
2025                Some(message) if message.contains(expected) => TestResult::passed(elapsed),
2026                _ => TestResult::failed(
2027                    elapsed,
2028                    FailureCause::Panic(PanicCause {
2029                        message: Some(format!(
2030                            "Test panicked with unexpected message: {}",
2031                            panic_cause.message.as_deref().unwrap_or_default()
2032                        )),
2033                        location: None,
2034                        backtrace: None,
2035                    }),
2036                ),
2037            },
2038            ShouldPanic::Yes => TestResult::passed(elapsed),
2039            ShouldPanic::No => TestResult::failed(elapsed, FailureCause::Panic(panic_cause)),
2040        }
2041    }
2042
2043    pub(crate) fn failure_message(&self) -> Option<String> {
2044        self.failure_cause().map(|c| c.render())
2045    }
2046
2047    pub fn failure_cause(&self) -> Option<&FailureCause> {
2048        match self {
2049            TestResult::Failed { cause, .. } => Some(cause),
2050            _ => None,
2051        }
2052    }
2053}
2054
2055pub struct SuiteResult {
2056    pub passed: usize,
2057    pub failed: usize,
2058    pub ignored: usize,
2059    pub measured: usize,
2060    pub filtered_out: usize,
2061    pub exec_time: Duration,
2062}
2063
2064impl SuiteResult {
2065    pub fn from_test_results(
2066        registered_tests: &[RegisteredTest],
2067        results: &[(RegisteredTest, TestResult)],
2068        exec_time: Duration,
2069    ) -> Self {
2070        let passed = results
2071            .iter()
2072            .filter(|(_, result)| result.is_passed())
2073            .count();
2074        let measured = results
2075            .iter()
2076            .filter(|(_, result)| result.is_benchmarked())
2077            .count();
2078        let failed = results
2079            .iter()
2080            .filter(|(_, result)| result.is_failed())
2081            .count();
2082        let ignored = results
2083            .iter()
2084            .filter(|(_, result)| result.is_ignored())
2085            .count();
2086        let filtered_out = registered_tests.len() - results.len();
2087
2088        Self {
2089            passed,
2090            failed,
2091            ignored,
2092            measured,
2093            filtered_out,
2094            exec_time,
2095        }
2096    }
2097
2098    pub fn exit_code(results: &[(RegisteredTest, TestResult)]) -> ExitCode {
2099        if results.iter().any(|(_, result)| result.is_failed()) {
2100            ExitCode::from(101)
2101        } else {
2102            ExitCode::SUCCESS
2103        }
2104    }
2105}
2106
2107pub trait DependencyView: Debug {
2108    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>>;
2109}
2110
2111impl DependencyView for Arc<dyn DependencyView + Send + Sync> {
2112    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2113        self.as_ref().get(name)
2114    }
2115}
2116
2117#[derive(Debug, Clone, Eq, PartialEq)]
2118pub enum CapturedOutput {
2119    Stdout { timestamp: SystemTime, line: String },
2120    Stderr { timestamp: SystemTime, line: String },
2121}
2122
2123impl CapturedOutput {
2124    pub fn stdout(line: String) -> Self {
2125        CapturedOutput::Stdout {
2126            timestamp: SystemTime::now(),
2127            line,
2128        }
2129    }
2130
2131    pub fn stderr(line: String) -> Self {
2132        CapturedOutput::Stderr {
2133            timestamp: SystemTime::now(),
2134            line,
2135        }
2136    }
2137
2138    pub fn timestamp(&self) -> SystemTime {
2139        match self {
2140            CapturedOutput::Stdout { timestamp, .. } => *timestamp,
2141            CapturedOutput::Stderr { timestamp, .. } => *timestamp,
2142        }
2143    }
2144
2145    pub fn line(&self) -> &str {
2146        match self {
2147            CapturedOutput::Stdout { line, .. } => line,
2148            CapturedOutput::Stderr { line, .. } => line,
2149        }
2150    }
2151}
2152
2153impl PartialOrd for CapturedOutput {
2154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2155        Some(self.cmp(other))
2156    }
2157}
2158
2159impl Ord for CapturedOutput {
2160    fn cmp(&self, other: &Self) -> Ordering {
2161        self.timestamp().cmp(&other.timestamp())
2162    }
2163}
2164
2165#[cfg(test)]
2166mod error_reporting_tests {
2167    use super::*;
2168    use std::panic::{catch_unwind, AssertUnwindSafe};
2169    use std::time::Duration;
2170
2171    fn simulate_runner(
2172        test_fn: impl FnOnce() -> Box<dyn TestReturnValue> + std::panic::UnwindSafe,
2173    ) -> TestResult {
2174        crate::panic_hook::install_panic_hook();
2175        let test_id = crate::panic_hook::next_test_id();
2176        crate::panic_hook::set_current_test_id(test_id);
2177        let result = catch_unwind(AssertUnwindSafe(move || {
2178            let ret = test_fn();
2179            ret.into_result()?;
2180            Ok(())
2181        }));
2182        let test_result =
2183            TestResult::from_result(&ShouldPanic::No, Duration::from_millis(1), result);
2184        crate::panic_hook::clear_current_test_id();
2185        test_result
2186    }
2187
2188    #[test]
2189    fn panic_with_assert_eq() {
2190        let result = simulate_runner(|| {
2191            assert_eq!(1, 2);
2192            Box::new(())
2193        });
2194        assert!(result.is_failed());
2195        let msg = result.failure_message().unwrap();
2196        println!("=== panic assert_eq failure message ===\n{msg}\n===");
2197        assert!(
2198            msg.contains("assertion `left == right` failed"),
2199            "Expected assertion message, got: {msg}"
2200        );
2201        assert!(
2202            msg.contains("at "),
2203            "Expected location info in message, got: {msg}"
2204        );
2205    }
2206
2207    #[test]
2208    fn string_error() {
2209        let result = simulate_runner(|| {
2210            let r: Result<(), String> = Err("something went wrong".to_string());
2211            Box::new(r)
2212        });
2213        assert!(result.is_failed());
2214        let msg = result.failure_message().unwrap();
2215        println!("=== string error failure message ===\n{msg}\n===");
2216        assert_eq!(msg, "something went wrong");
2217    }
2218
2219    #[test]
2220    fn anyhow_error() {
2221        let result = simulate_runner(|| {
2222            let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
2223            let err = anyhow::anyhow!(inner).context("operation failed");
2224            let r: Result<(), anyhow::Error> = Err(err);
2225            Box::new(r)
2226        });
2227        assert!(result.is_failed());
2228        let msg = result.failure_message().unwrap();
2229        println!("=== anyhow error failure message ===\n{msg}\n===");
2230        assert!(
2231            msg.contains("operation failed"),
2232            "Expected 'operation failed', got: {msg}"
2233        );
2234        assert!(
2235            msg.contains("file not found"),
2236            "Expected 'file not found', got: {msg}"
2237        );
2238    }
2239
2240    #[test]
2241    fn std_io_error() {
2242        let result = simulate_runner(|| {
2243            let r: Result<(), std::io::Error> = Err(std::io::Error::new(
2244                std::io::ErrorKind::NotFound,
2245                "file not found",
2246            ));
2247            Box::new(r)
2248        });
2249        assert!(result.is_failed());
2250        let msg = result.failure_message().unwrap();
2251        println!("=== std io error failure message ===\n{msg}\n===");
2252        // Should use Display (not Debug), so no "Custom { kind: NotFound, ... }"
2253        assert_eq!(msg, "file not found");
2254    }
2255
2256    #[test]
2257    fn panic_with_location_info() {
2258        let result = simulate_runner(|| {
2259            panic!("test panic with location");
2260            #[allow(unreachable_code)]
2261            Box::new(())
2262        });
2263        assert!(result.is_failed());
2264        let cause = result.failure_cause().unwrap();
2265        match cause {
2266            FailureCause::Panic(p) => {
2267                assert!(p.location.is_some(), "Expected location info");
2268                let loc = p.location.as_ref().unwrap();
2269                assert!(
2270                    loc.file.contains("internal.rs"),
2271                    "Expected file to contain internal.rs, got: {}",
2272                    loc.file
2273                );
2274                assert!(loc.line > 0, "Expected non-zero line number");
2275            }
2276            other => panic!("Expected Panic cause, got: {other:?}"),
2277        }
2278    }
2279
2280    #[test]
2281    fn panic_render_includes_location() {
2282        let result = simulate_runner(|| {
2283            panic!("location test");
2284            #[allow(unreachable_code)]
2285            Box::new(())
2286        });
2287        let msg = result.failure_message().unwrap();
2288        assert!(
2289            msg.contains("location test"),
2290            "Expected panic message, got: {msg}"
2291        );
2292        assert!(
2293            msg.contains("\n  at "),
2294            "Expected location line in render, got: {msg}"
2295        );
2296    }
2297
2298    #[test]
2299    fn should_panic_with_message_matching() {
2300        crate::panic_hook::install_panic_hook();
2301        let test_id = crate::panic_hook::next_test_id();
2302        crate::panic_hook::set_current_test_id(test_id);
2303        let result = catch_unwind(AssertUnwindSafe(|| {
2304            panic!("expected panic message");
2305        }));
2306        let test_result = TestResult::from_result(
2307            &ShouldPanic::WithMessage("expected panic".to_string()),
2308            Duration::from_millis(1),
2309            result.map(|_| Ok(())),
2310        );
2311        crate::panic_hook::clear_current_test_id();
2312        assert!(
2313            test_result.is_passed(),
2314            "Expected test to pass with matching panic message"
2315        );
2316    }
2317
2318    #[test]
2319    fn should_panic_with_wrong_message() {
2320        crate::panic_hook::install_panic_hook();
2321        let test_id = crate::panic_hook::next_test_id();
2322        crate::panic_hook::set_current_test_id(test_id);
2323        let result = catch_unwind(AssertUnwindSafe(|| {
2324            panic!("actual panic message");
2325        }));
2326        let test_result = TestResult::from_result(
2327            &ShouldPanic::WithMessage("completely different".to_string()),
2328            Duration::from_millis(1),
2329            result.map(|_| Ok(())),
2330        );
2331        crate::panic_hook::clear_current_test_id();
2332        assert!(
2333            test_result.is_failed(),
2334            "Expected test to fail with wrong panic message"
2335        );
2336        let msg = test_result.failure_message().unwrap();
2337        assert!(
2338            msg.contains("unexpected message"),
2339            "Expected 'unexpected message' in: {msg}"
2340        );
2341    }
2342
2343    #[test]
2344    fn pretty_assertions_diff() {
2345        let result = simulate_runner(|| {
2346            pretty_assertions::assert_eq!("hello world\nfoo\nbar\n", "hello world\nbaz\nbar\n");
2347            Box::new(())
2348        });
2349        assert!(result.is_failed());
2350        let cause = result.failure_cause().unwrap();
2351
2352        // Should be a Panic variant (assert_eq! panics)
2353        let panic_cause = match cause {
2354            FailureCause::Panic(p) => p,
2355            other => panic!("Expected Panic cause, got: {other:?}"),
2356        };
2357
2358        // The panic message should contain the colorful diff from pretty_assertions
2359        let message = panic_cause.message.as_deref().unwrap();
2360        println!("=== pretty_assertions failure message ===\n{message}\n===");
2361        assert!(
2362            message.contains("foo") && message.contains("baz"),
2363            "Expected diff with 'foo' and 'baz', got: {message}"
2364        );
2365
2366        // Location should be captured
2367        assert!(panic_cause.location.is_some(), "Expected location info");
2368
2369        // The rendered output should NOT contain backtrace noise when RUST_BACKTRACE is unset
2370        let rendered = cause.render();
2371        println!("=== pretty_assertions rendered ===\n{rendered}\n===");
2372        assert!(
2373            !rendered.contains("stack backtrace") && !rendered.contains("Stack backtrace"),
2374            "Expected no backtrace noise in rendered output, got: {rendered}"
2375        );
2376        // Should contain location
2377        assert!(
2378            rendered.contains("\n  at "),
2379            "Expected location in rendered output, got: {rendered}"
2380        );
2381    }
2382
2383    #[test]
2384    fn detached_thread_panic_detected() {
2385        crate::panic_hook::install_panic_hook();
2386        let test_id = crate::panic_hook::next_test_id();
2387        crate::panic_hook::set_current_test_id(test_id);
2388        crate::panic_hook::create_detached_collector(test_id);
2389
2390        let result = catch_unwind(AssertUnwindSafe(|| {
2391            let handle = crate::spawn::spawn_thread(|| {
2392                panic!("background thread panic");
2393            });
2394            let _ = handle.join();
2395        }));
2396
2397        let mut test_result = TestResult::from_result(
2398            &ShouldPanic::No,
2399            Duration::from_millis(1),
2400            result.map(|_| Ok(())),
2401        );
2402
2403        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
2404            let panics = match collector.lock() {
2405                Ok(p) => p,
2406                Err(poisoned) => poisoned.into_inner(),
2407            };
2408            if !panics.is_empty() && test_result.is_passed() {
2409                let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
2410                test_result = TestResult::failed(
2411                    Duration::from_millis(1),
2412                    FailureCause::Panic(PanicCause {
2413                        message: Some(format!(
2414                            "Detached task(s) panicked:\n{}",
2415                            messages.join("\n---\n")
2416                        )),
2417                        location: panics.first().and_then(|p| p.location.clone()),
2418                        backtrace: panics.first().and_then(|p| p.backtrace.clone()),
2419                    }),
2420                );
2421            }
2422        }
2423
2424        crate::panic_hook::clear_current_test_id();
2425
2426        assert!(
2427            test_result.is_failed(),
2428            "Expected test to fail due to detached panic"
2429        );
2430        let msg = test_result.failure_message().unwrap();
2431        assert!(
2432            msg.contains("Detached task(s) panicked"),
2433            "Expected detached panic message, got: {msg}"
2434        );
2435        assert!(
2436            msg.contains("background thread panic"),
2437            "Expected original panic message, got: {msg}"
2438        );
2439    }
2440
2441    #[test]
2442    fn detached_thread_panic_ignored_with_policy() {
2443        crate::panic_hook::install_panic_hook();
2444        let test_id = crate::panic_hook::next_test_id();
2445        crate::panic_hook::set_current_test_id(test_id);
2446        crate::panic_hook::create_detached_collector(test_id);
2447
2448        let result = catch_unwind(AssertUnwindSafe(|| {
2449            let handle = crate::spawn::spawn_thread(|| {
2450                panic!("ignored thread panic");
2451            });
2452            let _ = handle.join();
2453        }));
2454
2455        let test_result = TestResult::from_result(
2456            &ShouldPanic::No,
2457            Duration::from_millis(1),
2458            result.map(|_| Ok(())),
2459        );
2460
2461        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
2462            let panics = match collector.lock() {
2463                Ok(p) => p,
2464                Err(poisoned) => poisoned.into_inner(),
2465            };
2466            // Verify panics were captured but Ignore policy does not fail the test
2467            assert!(
2468                !panics.is_empty(),
2469                "Expected panics in collector even with Ignore policy"
2470            );
2471        }
2472
2473        crate::panic_hook::clear_current_test_id();
2474
2475        assert!(
2476            test_result.is_passed(),
2477            "Expected test to pass with Ignore policy"
2478        );
2479    }
2480
2481    #[cfg(feature = "tokio")]
2482    #[test]
2483    fn detached_task_panic_detected() {
2484        let rt = tokio::runtime::Runtime::new().unwrap();
2485        rt.block_on(async {
2486            crate::panic_hook::install_panic_hook();
2487            let test_id = crate::panic_hook::next_test_id();
2488            crate::panic_hook::set_current_test_id(test_id);
2489            crate::panic_hook::create_detached_collector(test_id);
2490
2491            let handle = crate::spawn::spawn(async {
2492                panic!("detached task panic");
2493            });
2494            let _ = handle.await;
2495
2496            let collector = crate::panic_hook::take_detached_collector(test_id).unwrap();
2497            let panics = collector.lock().unwrap();
2498
2499            assert_eq!(panics.len(), 1);
2500            assert!(
2501                panics[0]
2502                    .message
2503                    .as_ref()
2504                    .unwrap()
2505                    .contains("detached task panic"),
2506                "Expected panic message, got: {:?}",
2507                panics[0].message
2508            );
2509
2510            crate::panic_hook::clear_current_test_id();
2511        });
2512    }
2513
2514    #[test]
2515    fn failure_cause_variants() {
2516        // ReturnedMessage
2517        let cause = FailureCause::ReturnedMessage("simple message".to_string());
2518        assert_eq!(cause.render(), "simple message");
2519        assert!(cause.panic_message().is_none());
2520
2521        // ReturnedError (prefer display)
2522        let cause = FailureCause::ReturnedError {
2523            display: "display text".to_string(),
2524            debug: "debug text".to_string(),
2525            prefer_debug: false,
2526            error: Arc::new("display text".to_string()),
2527        };
2528        assert_eq!(cause.render(), "display text");
2529
2530        // ReturnedError (prefer debug, e.g. anyhow)
2531        let cause = FailureCause::ReturnedError {
2532            display: "display text".to_string(),
2533            debug: "debug text".to_string(),
2534            prefer_debug: true,
2535            error: Arc::new("debug text".to_string()),
2536        };
2537        assert_eq!(cause.render(), "debug text");
2538
2539        // HarnessError
2540        let cause = FailureCause::HarnessError("harness error".to_string());
2541        assert_eq!(cause.render(), "harness error");
2542
2543        // Panic with message
2544        let cause = FailureCause::Panic(PanicCause {
2545            message: Some("panic msg".to_string()),
2546            location: None,
2547            backtrace: None,
2548        });
2549        assert_eq!(cause.render(), "panic msg");
2550        assert_eq!(cause.panic_message(), Some("panic msg"));
2551    }
2552}
2553
2554#[cfg(test)]
2555mod filter_tests {
2556    use super::*;
2557
2558    fn make_test(name: &str, module_path: &str) -> RegisteredTest {
2559        RegisteredTest {
2560            name: name.to_string(),
2561            crate_name: "mycrate".to_string(),
2562            module_path: module_path.to_string(),
2563            run: TestFunction::Sync(Arc::new(|_| Box::new(()))),
2564            props: TestProperties::default(),
2565            dependencies: None,
2566        }
2567    }
2568
2569    fn make_tagged_test(name: &str, module_path: &str, tags: Vec<&str>) -> RegisteredTest {
2570        let mut test = make_test(name, module_path);
2571        test.props.tags = tags.into_iter().map(String::from).collect();
2572        test
2573    }
2574
2575    fn make_args(filters: Vec<&str>, skip: Vec<&str>, exact: bool) -> Arguments {
2576        Arguments {
2577            filter: filters.into_iter().map(String::from).collect(),
2578            skip: skip.into_iter().map(String::from).collect(),
2579            exact,
2580            ..Default::default()
2581        }
2582    }
2583
2584    fn filtered_names(args: &Arguments, tests: &[RegisteredTest]) -> Vec<String> {
2585        filter_registered_tests(args, tests)
2586            .into_iter()
2587            .map(|t| t.filterable_name())
2588            .collect()
2589    }
2590
2591    // --- filter_test unit tests ---
2592
2593    #[test]
2594    fn filter_test_substring_match() {
2595        let test = make_test("hello_world", "mod1");
2596        assert!(filter_test(&test, "hello", false));
2597        assert!(filter_test(&test, "world", false));
2598        assert!(filter_test(&test, "mod1::hello", false));
2599        assert!(!filter_test(&test, "nonexistent", false));
2600    }
2601
2602    #[test]
2603    fn filter_test_exact_match() {
2604        let test = make_test("hello_world", "mod1");
2605        assert!(filter_test(&test, "mod1::hello_world", true));
2606        assert!(!filter_test(&test, "hello_world", true));
2607        assert!(!filter_test(&test, "hello", true));
2608    }
2609
2610    #[test]
2611    fn filter_test_tag_match() {
2612        let test = make_tagged_test("t1", "mod1", vec!["fast", "unit"]);
2613        assert!(filter_test(&test, ":tag:fast", false));
2614        assert!(filter_test(&test, ":tag:unit", false));
2615        assert!(!filter_test(&test, ":tag:slow", false));
2616    }
2617
2618    #[test]
2619    fn filter_test_tag_empty_matches_untagged() {
2620        let untagged = make_test("t1", "mod1");
2621        let tagged = make_tagged_test("t2", "mod1", vec!["fast"]);
2622        assert!(filter_test(&untagged, ":tag:", false));
2623        assert!(!filter_test(&tagged, ":tag:", false));
2624    }
2625
2626    // --- filter_registered_tests: multiple include filters (OR semantics) ---
2627
2628    #[test]
2629    fn no_filters_includes_all() {
2630        let tests = vec![make_test("a", "m"), make_test("b", "m")];
2631        let args = make_args(vec![], vec![], false);
2632        assert_eq!(filtered_names(&args, &tests), vec!["m::a", "m::b"]);
2633    }
2634
2635    #[test]
2636    fn single_filter_substring() {
2637        let tests = vec![
2638            make_test("alpha", "m"),
2639            make_test("beta", "m"),
2640            make_test("alphabet", "m"),
2641        ];
2642        let args = make_args(vec!["alpha"], vec![], false);
2643        assert_eq!(
2644            filtered_names(&args, &tests),
2645            vec!["m::alpha", "m::alphabet"]
2646        );
2647    }
2648
2649    #[test]
2650    fn multiple_filters_or_semantics() {
2651        let tests = vec![
2652            make_test("alpha", "m"),
2653            make_test("beta", "m"),
2654            make_test("gamma", "m"),
2655        ];
2656        let args = make_args(vec!["alpha", "gamma"], vec![], false);
2657        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::gamma"]);
2658    }
2659
2660    #[test]
2661    fn multiple_filters_exact() {
2662        let tests = vec![
2663            make_test("alpha", "m"),
2664            make_test("alphabet", "m"),
2665            make_test("beta", "m"),
2666        ];
2667        let args = make_args(vec!["m::alpha", "m::beta"], vec![], true);
2668        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::beta"]);
2669    }
2670
2671    // --- skip behavior ---
2672
2673    #[test]
2674    fn skip_substring_match() {
2675        let tests = vec![
2676            make_test("fast_test", "m"),
2677            make_test("slow_test", "m"),
2678            make_test("slower_test", "m"),
2679        ];
2680        let args = make_args(vec![], vec!["slow"], false);
2681        assert_eq!(filtered_names(&args, &tests), vec!["m::fast_test"]);
2682    }
2683
2684    #[test]
2685    fn skip_exact_match() {
2686        let tests = vec![make_test("slow_test", "m"), make_test("slower_test", "m")];
2687        let args = make_args(vec![], vec!["m::slow_test"], true);
2688        assert_eq!(filtered_names(&args, &tests), vec!["m::slower_test"]);
2689    }
2690
2691    #[test]
2692    fn skip_with_tag() {
2693        let tests = vec![
2694            make_tagged_test("t1", "m", vec!["slow"]),
2695            make_tagged_test("t2", "m", vec!["fast"]),
2696            make_test("t3", "m"),
2697        ];
2698        let args = make_args(vec![], vec![":tag:slow"], false);
2699        assert_eq!(filtered_names(&args, &tests), vec!["m::t2", "m::t3"]);
2700    }
2701
2702    // --- combined include + skip ---
2703
2704    #[test]
2705    fn include_and_skip_combined() {
2706        let tests = vec![
2707            make_test("alpha_fast", "m"),
2708            make_test("alpha_slow", "m"),
2709            make_test("beta_fast", "m"),
2710        ];
2711        // Include anything with "alpha", but skip anything with "slow"
2712        let args = make_args(vec!["alpha"], vec!["slow"], false);
2713        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha_fast"]);
2714    }
2715
2716    #[test]
2717    fn skip_wins_over_include() {
2718        let tests = vec![make_test("target", "m")];
2719        // Both include and skip match the same test — skip should win
2720        let args = make_args(vec!["target"], vec!["target"], false);
2721        assert_eq!(filtered_names(&args, &tests), Vec::<String>::new());
2722    }
2723
2724    // --- tag boolean expression syntax ---
2725
2726    #[test]
2727    fn filter_test_tag_or_expression() {
2728        // `:tag:a|b` matches tests tagged with `a` OR `b`
2729        let test_a = make_tagged_test("t1", "m", vec!["a"]);
2730        let test_b = make_tagged_test("t2", "m", vec!["b"]);
2731        let test_c = make_tagged_test("t3", "m", vec!["c"]);
2732        assert!(filter_test(&test_a, ":tag:a|b", false));
2733        assert!(filter_test(&test_b, ":tag:a|b", false));
2734        assert!(!filter_test(&test_c, ":tag:a|b", false));
2735    }
2736
2737    #[test]
2738    fn filter_test_tag_and_expression() {
2739        // `:tag:a&b` matches tests tagged with BOTH `a` AND `b`
2740        let test_ab = make_tagged_test("t1", "m", vec!["a", "b"]);
2741        let test_a = make_tagged_test("t2", "m", vec!["a"]);
2742        let test_b = make_tagged_test("t3", "m", vec!["b"]);
2743        assert!(filter_test(&test_ab, ":tag:a&b", false));
2744        assert!(!filter_test(&test_a, ":tag:a&b", false));
2745        assert!(!filter_test(&test_b, ":tag:a&b", false));
2746    }
2747
2748    #[test]
2749    fn filter_test_tag_mixed_and_or() {
2750        // `:tag:a|b&c` means `a OR (b AND c)` — `&` has higher precedence
2751        let test_a = make_tagged_test("t1", "m", vec!["a"]);
2752        let test_bc = make_tagged_test("t2", "m", vec!["b", "c"]);
2753        let test_b = make_tagged_test("t3", "m", vec!["b"]);
2754        let test_c = make_tagged_test("t4", "m", vec!["c"]);
2755        let test_none = make_test("t5", "m");
2756        assert!(filter_test(&test_a, ":tag:a|b&c", false));
2757        assert!(filter_test(&test_bc, ":tag:a|b&c", false));
2758        assert!(!filter_test(&test_b, ":tag:a|b&c", false));
2759        assert!(!filter_test(&test_c, ":tag:a|b&c", false));
2760        assert!(!filter_test(&test_none, ":tag:a|b&c", false));
2761    }
2762
2763    #[test]
2764    fn filter_test_tag_exact_flag_does_not_affect_tags() {
2765        // `--exact` should not change tag matching behavior
2766        let test = make_tagged_test("t1", "m", vec!["fast"]);
2767        assert!(filter_test(&test, ":tag:fast", true));
2768        assert!(!filter_test(&test, ":tag:slow", true));
2769    }
2770
2771    #[test]
2772    fn include_by_tag_or_expression() {
2773        let tests = vec![
2774            make_tagged_test("t1", "m", vec!["unit"]),
2775            make_tagged_test("t2", "m", vec!["integration"]),
2776            make_tagged_test("t3", "m", vec!["e2e"]),
2777        ];
2778        let args = make_args(vec![":tag:unit|integration"], vec![], false);
2779        assert_eq!(filtered_names(&args, &tests), vec!["m::t1", "m::t2"]);
2780    }
2781
2782    #[test]
2783    fn skip_by_tag_and_expression() {
2784        let tests = vec![
2785            make_tagged_test("t1", "m", vec!["slow", "network"]),
2786            make_tagged_test("t2", "m", vec!["slow"]),
2787            make_tagged_test("t3", "m", vec!["network"]),
2788            make_test("t4", "m"),
2789        ];
2790        // Skip only tests that are BOTH slow AND network
2791        let args = make_args(vec![], vec![":tag:slow&network"], false);
2792        assert_eq!(
2793            filtered_names(&args, &tests),
2794            vec!["m::t2", "m::t3", "m::t4"]
2795        );
2796    }
2797}