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/// Type-erased, parent-owned cell that holds the owner value behind a
693/// `Mutex` and exposes a `&self` dispatch entry point. Constructed by the
694/// macro-generated registration code on the parent (the `DependencyConstructor`
695/// for a `HostedRpc` dep returns one of these wrapped in `Arc<dyn Any>`)
696/// and kept alive in `_hosted_owners` for the suite's lifetime.
697pub struct HostedRpcOwnerCell {
698    inner: Mutex<Box<dyn HostedRpcDispatcher>>,
699}
700
701impl HostedRpcOwnerCell {
702    /// Wrap an owner value into a `HostedRpcOwnerCell`. The owner type must
703    /// implement [`HostedRpcDep`].
704    pub fn from_owner<T: HostedRpcDep>(owner: T) -> Self {
705        Self {
706            inner: Mutex::new(Box::new(owner) as Box<dyn HostedRpcDispatcher>),
707        }
708    }
709
710    /// Dispatch one method call. Catches owner panics and turns them into
711    /// `Err("hosted rpc owner panicked: …")` so the dispatcher loop never
712    /// dies. The lock is acquired *inside* the `catch_unwind` closure on
713    /// purpose: when the owner panics, the `MutexGuard` drops during the
714    /// unwind, which poisons the mutex. Every subsequent `dispatch` call
715    /// then short-circuits with the stable `"hosted rpc owner poisoned"`
716    /// error and does NOT retry the (possibly half-mutated) owner.
717    pub fn dispatch(&self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
718        // The lock acquire lives inside the catch_unwind closure on
719        // purpose. If we acquired the lock outside and the user dispatch
720        // panicked, the panic would be caught before the MutexGuard had a
721        // chance to drop during unwinding, leaving the mutex healthy — and
722        // we want it poisoned so that subsequent calls see a deterministic
723        // "owner is dead" error rather than re-entering a half-mutated
724        // owner value.
725        let dispatch_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
726            let mut guard = match self.inner.lock() {
727                Ok(g) => g,
728                Err(_) => return Err("hosted rpc owner poisoned".to_string()),
729            };
730            guard.dispatch(method_idx, args)
731        }));
732        match dispatch_result {
733            Ok(r) => r,
734            Err(payload) => {
735                let msg = if let Some(s) = payload.downcast_ref::<&str>() {
736                    (*s).to_string()
737                } else if let Some(s) = payload.downcast_ref::<String>() {
738                    s.clone()
739                } else {
740                    "<non-string panic payload>".to_string()
741                };
742                Err(format!("hosted rpc owner panicked: {msg}"))
743            }
744        }
745    }
746}
747
748/// Support type for `#[test_dep(scope = Hosted, worker = both(T))]`.
749///
750/// One macro-emitted `worker = both(T)` registration is lowered into
751/// **two** `RegisteredDependency` entries that both point at the same
752/// parent-side owner — one for the descriptor (Hosted) view, one for
753/// the RPC stub (HostedRpc) view. To keep the owner unique under
754/// either view, both registrations route through a single
755/// `HostedBothShared` cell created by the macro-emitted weak cache:
756///
757/// - the **descriptor view** asks for the cached descriptor bytes
758///   (`HostedDep::descriptor` / `AsyncHostedDep::descriptor` is only
759///   called once, on the first construction);
760/// - the **RPC view** asks for the inner [`HostedRpcOwnerCell`], so
761///   the parent-side dispatcher sees the same owner the descriptor
762///   was derived from.
763///
764/// This is intentionally *not* a public end-user type; only the
765/// macro-support helpers in [`crate::__test_r_make_hosted_both_shared`]
766/// and friends construct one.
767pub struct HostedBothShared {
768    descriptor_bytes: Vec<u8>,
769    rpc_cell: Arc<HostedRpcOwnerCell>,
770}
771
772impl HostedBothShared {
773    /// Wrap a pre-computed descriptor + RPC owner cell for the `both`
774    /// dep variant.
775    pub fn new(descriptor_bytes: Vec<u8>, rpc_cell: Arc<HostedRpcOwnerCell>) -> Self {
776        Self {
777            descriptor_bytes,
778            rpc_cell,
779        }
780    }
781
782    /// Borrow the cached descriptor bytes (computed once, on first
783    /// construction).
784    pub fn descriptor_bytes(&self) -> &[u8] {
785        &self.descriptor_bytes
786    }
787
788    /// Cheap clone of the inner RPC owner cell `Arc`. The
789    /// HostedRpc-view registration's `RpcFactory::owner_into_cell`
790    /// hands this back to the runtime.
791    pub fn rpc_cell(&self) -> Arc<HostedRpcOwnerCell> {
792        self.rpc_cell.clone()
793    }
794}
795
796/// Error returned by [`HostedRpcChannel::call`] when an RPC fails.
797#[derive(Debug, Clone)]
798pub enum HostedRpcError {
799    /// The owner-side dispatcher returned an error string (unknown method,
800    /// codec error, panic in the user method, …).
801    Dispatch(String),
802    /// The IPC transport itself failed (worker disconnected, framing error,
803    /// runtime not in spawn-workers mode, …).
804    Transport(String),
805}
806
807impl std::fmt::Display for HostedRpcError {
808    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
809        match self {
810            HostedRpcError::Dispatch(s) => write!(f, "hosted rpc dispatch error: {s}"),
811            HostedRpcError::Transport(s) => write!(f, "hosted rpc transport error: {s}"),
812        }
813    }
814}
815
816impl std::error::Error for HostedRpcError {}
817
818/// Trait implemented by the per-runner transport that workers use to send
819/// RPCs to the parent's owner. The runtime provides a concrete IPC
820/// implementation for the spawn-workers case and a direct in-process
821/// implementation for `--nocapture` / single-process mode.
822pub trait HostedRpcTransport: Send + Sync {
823    /// Send one call and block until the reply arrives. `dep_id` is the
824    /// dep's fully-qualified id (`{crate}::{module}::{name}`) used by the
825    /// parent to route the call to the right owner.
826    fn call(&self, dep_id: &str, method_idx: u32, args: Vec<u8>)
827        -> Result<Vec<u8>, HostedRpcError>;
828}
829
830/// Per-dep channel handed to [`HostedRpcDep::build_stub`] on the worker side.
831///
832/// The stub holds this channel and calls [`HostedRpcChannel::call`] from
833/// each of its method bodies; the channel takes care of dep-id routing,
834/// serialization framing, and waiting for the parent's reply.
835pub struct HostedRpcChannel {
836    dep_id: String,
837    transport: Arc<dyn HostedRpcTransport>,
838}
839
840impl HostedRpcChannel {
841    /// Construct a channel that targets the dep identified by
842    /// `dep_id` (a fully-qualified id) and uses the supplied transport.
843    pub fn new(dep_id: String, transport: Arc<dyn HostedRpcTransport>) -> Self {
844        Self { dep_id, transport }
845    }
846
847    /// The fully-qualified dep id this channel routes to. Stubs almost never
848    /// need this directly, but it's exposed for diagnostics and tests.
849    pub fn dep_id(&self) -> &str {
850        &self.dep_id
851    }
852
853    /// Send one method call and block until the parent replies. `args` are
854    /// already-serialized bytes; the stub method body owns the choice of
855    /// codec.
856    ///
857    /// **Temporal invariant — only call this while a test body is actually
858    /// running.** The transport assumes one
859    /// HostedRpc request/reply pair per worker subprocess is in flight
860    /// at a time *and* that the worker's main IPC command loop is idle
861    /// (it only reads `Provide*` / `RunTest` between tests). Specifically:
862    ///
863    /// - **Do NOT call from `HostedRpcDep::build_stub`** — see that
864    ///   method's docs for why.
865    /// - **Do NOT call from background threads or detached tasks that
866    ///   outlive the test body** — once the test returns the worker
867    ///   sends `TestFinished` and the parent's next message will be a
868    ///   `Provide*` / `RunTest`, which the transport's read side would
869    ///   then misinterpret as a reply.
870    /// - **Do NOT call from `Drop` / destructor-style cleanup or any
871    ///   teardown hook that may fire after the test body has returned** —
872    ///   that is just another form of "outside the test body" and has the
873    ///   same IPC-framing-desync risk as a detached background thread.
874    /// - Stub calls from inside the test body — directly or transitively
875    ///   from helpers the test body awaits/blocks on — are the supported
876    ///   shape.
877    pub fn call(&self, method_idx: u32, args: Vec<u8>) -> Result<Vec<u8>, HostedRpcError> {
878        self.transport.call(&self.dep_id, method_idx, args)
879    }
880}
881
882impl Clone for HostedRpcChannel {
883    fn clone(&self) -> Self {
884        Self {
885            dep_id: self.dep_id.clone(),
886            transport: self.transport.clone(),
887        }
888    }
889}
890
891/// In-process transport used in `--nocapture` / single-process mode: the
892/// stub calls the owner-side [`HostedRpcOwnerCell`] directly without
893/// touching any IPC stream.
894pub struct InProcessHostedRpcTransport {
895    cells: HashMap<String, Arc<HostedRpcOwnerCell>>,
896}
897
898impl InProcessHostedRpcTransport {
899    pub fn new(cells: HashMap<String, Arc<HostedRpcOwnerCell>>) -> Self {
900        Self { cells }
901    }
902}
903
904impl HostedRpcTransport for InProcessHostedRpcTransport {
905    fn call(
906        &self,
907        dep_id: &str,
908        method_idx: u32,
909        args: Vec<u8>,
910    ) -> Result<Vec<u8>, HostedRpcError> {
911        let cell = self.cells.get(dep_id).ok_or_else(|| {
912            HostedRpcError::Transport(format!("in-process HostedRpc: unknown dep id '{dep_id}'"))
913        })?;
914        cell.dispatch(method_idx, &args)
915            .map_err(HostedRpcError::Dispatch)
916    }
917}
918
919/// Factory pair stored on a `HostedRpc` [`RegisteredDependency`]. The macro
920/// emits a `RpcFactory` per registered HostedRpc dep so the runtime can
921/// (a) wrap the constructor's output into a parent dispatcher cell, and
922/// (b) build a worker-side stub from a channel.
923#[derive(Clone)]
924#[allow(clippy::type_complexity)]
925pub struct RpcFactory {
926    /// Downcast the constructor's `Arc<dyn Any>` to the concrete
927    /// `HostedRpcOwnerCell` for this dep.
928    pub owner_into_cell: Arc<
929        dyn (Fn(Arc<dyn Any + Send + Sync>) -> Arc<HostedRpcOwnerCell>) + Send + Sync + 'static,
930    >,
931    /// Build a worker-side stub (typed as the dep's `Stub` associated type)
932    /// from the supplied channel, boxed as `Arc<dyn Any>`.
933    pub build_stub:
934        Arc<dyn (Fn(HostedRpcChannel) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
935}
936
937/// Sharing strategy declared on a `#[test_dep]`. Controls how the dependency
938/// interacts with output capturing and parallel test execution.
939///
940/// See `book/src/design/sharing-strategy.md` for the full description.
941#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
942pub enum DepScope {
943    /// Today's behaviour: a single materialized instance shared by every test.
944    /// Forces single-threaded execution when output capturing is on, because
945    /// the `Arc<dyn Any>` cannot cross the parent/worker process boundary.
946    #[default]
947    Shared,
948    /// Each worker child materializes its own instance independently. Tests
949    /// inside one worker share the instance.
950    PerWorker,
951    /// Parent runs the constructor once and produces wire bytes; each worker
952    /// reconstructs a local instance from those bytes via the registered
953    /// worker reconstructor (`worker_fn`).
954    Cloneable,
955    /// Owner runs once in the **parent** test runner process and stays alive
956    /// for the entire suite. The parent produces a descriptor (via
957    /// [`HostedDep::descriptor`]) and ships those descriptor bytes to every
958    /// worker. Each worker reconstructs a handle via
959    /// [`HostedDep::from_descriptor`]. The owner is held in the parent
960    /// process so singleton services (TCP listeners, Docker containers,
961    /// gRPC server clients, env-based runtimes) are not duplicated per
962    /// worker.
963    Hosted,
964    /// Like [`Self::Hosted`], but the owner stays in the parent AND workers
965    /// talk back to it via the runtime's built-in RPC layer instead of reaching
966    /// out via their own transport. The dep implementor provides a
967    /// [`HostedRpcDep`] impl on the owner type with a stub type, a method
968    /// dispatch function, and a stub builder.
969    HostedRpc,
970}
971
972impl DepScope {
973    /// Returns `true` for scopes that materialize in the parent process and
974    /// therefore force single-threaded fallback when capturing is on.
975    pub fn requires_single_thread_when_capturing(&self) -> bool {
976        matches!(self, DepScope::Shared)
977    }
978
979    /// Returns `true` for scopes the parent should still materialize even
980    /// when it is otherwise delegating dependency construction to workers
981    /// (i.e. `skip_creating_dependencies` is set). `Cloneable` deps need the
982    /// parent to compute the wire form; `Hosted` / `HostedRpc` deps need
983    /// the parent to hold the owner alive for the whole suite.
984    pub fn parent_must_materialize_under_spawn_workers(&self) -> bool {
985        matches!(
986            self,
987            DepScope::Cloneable | DepScope::Hosted | DepScope::HostedRpc
988        )
989    }
990}
991
992/// Function pointer-equivalent used by the worker side of a `Cloneable`
993/// dependency. Receives the deserialized wire payload (boxed as `Any` for
994/// type erasure) plus the current dependency view, and produces the
995/// reconstructed worker-side value.
996#[derive(Clone)]
997#[allow(clippy::type_complexity)]
998pub enum WorkerReconstructor {
999    Sync(
1000        Arc<
1001            dyn (Fn(
1002                    Arc<dyn Any + Send + Sync>,
1003                    Arc<dyn DependencyView + Send + Sync>,
1004                ) -> Arc<dyn Any + Send + Sync + 'static>)
1005                + Send
1006                + Sync
1007                + 'static,
1008        >,
1009    ),
1010    Async(
1011        Arc<
1012            dyn (Fn(
1013                    Arc<dyn Any + Send + Sync>,
1014                    Arc<dyn DependencyView + Send + Sync>,
1015                ) -> Pin<Box<dyn Future<Output = Arc<dyn Any + Send + Sync>>>>)
1016                + Send
1017                + Sync
1018                + 'static,
1019        >,
1020    ),
1021}
1022
1023/// Function-pointer wrappers used by Cloneable deps to convert the
1024/// constructed value into wire bytes on the parent, and to deserialize those
1025/// bytes into a typed value on the worker.
1026#[derive(Clone)]
1027#[allow(clippy::type_complexity)]
1028pub struct CloneableCodec {
1029    /// Parent-side: `to_wire`. Receives the dependency value as `Arc<dyn Any>`,
1030    /// returns the encoded wire bytes.
1031    pub to_wire: Arc<dyn (Fn(Arc<dyn Any + Send + Sync>) -> Vec<u8>) + Send + Sync + 'static>,
1032    /// Worker-side: deserialize wire bytes into the boxed `Wire` payload that
1033    /// is then fed to the [`WorkerReconstructor`].
1034    pub from_wire_bytes: Arc<dyn (Fn(&[u8]) -> Arc<dyn Any + Send + Sync>) + Send + Sync + 'static>,
1035}
1036
1037#[derive(Clone)]
1038pub struct RegisteredDependency {
1039    pub name: String, // TODO: Should we use TypeId here?
1040    pub crate_name: String,
1041    pub module_path: String,
1042    pub constructor: DependencyConstructor,
1043    pub dependencies: Vec<String>,
1044    /// Sharing strategy declared on the constructor. Defaults to
1045    /// [`DepScope::Shared`] for backward compatibility.
1046    pub scope: DepScope,
1047    /// Worker-side reconstructor for `Cloneable` and `Hosted` deps
1048    /// (`None` otherwise). For `Cloneable` the wire payload IS the dep value;
1049    /// for `Hosted` the wire payload is the descriptor passed to
1050    /// [`HostedDep::from_descriptor`](crate::internal::HostedDep::from_descriptor).
1051    pub worker_fn: Option<WorkerReconstructor>,
1052    /// Wire-bytes codec for `Cloneable` deps (`None` otherwise). The codec
1053    /// shape is shared with [`Self::hosted_codec`] but the runtime dispatches
1054    /// on whichever field is populated.
1055    pub cloneable_codec: Option<CloneableCodec>,
1056    /// Descriptor-bytes codec for `Hosted` deps (`None` otherwise). Same
1057    /// shape as [`Self::cloneable_codec`]; the codec encodes the value
1058    /// returned by [`HostedDep::descriptor`](crate::internal::HostedDep::descriptor)
1059    /// into wire bytes on the parent (where the owner lives), and decodes
1060    /// those bytes in the worker before they are passed to the registered
1061    /// worker reconstructor.
1062    pub hosted_codec: Option<CloneableCodec>,
1063    /// Factories for `HostedRpc` deps (`None` otherwise). The parent uses
1064    /// [`RpcFactory::owner_into_cell`] to extract the `HostedRpcOwnerCell`
1065    /// returned by the constructor; the worker uses [`RpcFactory::build_stub`]
1066    /// to construct its `Stub` from a fresh [`HostedRpcChannel`].
1067    pub rpc_factory: Option<RpcFactory>,
1068    /// Planner-only sibling dep names that must be retained together
1069    /// with this dep during pruning. Unlike `dependencies`, companions
1070    /// are **not** real dependency edges — no constructor argument is
1071    /// derived from a companion, and no topological ordering is
1072    /// implied. The pruner simply treats companions as mutually
1073    /// reachable: if any companion in a group is in the keep-set, the
1074    /// whole group is retained.
1075    ///
1076    /// Currently set by the `#[test_dep(scope = Hosted, worker = both(T))]`
1077    /// macro lowering, which registers two paired dep entries (the
1078    /// Hosted owner view and the HostedRpc stub view) backed by the
1079    /// same parent-side `Arc<HostedBothShared>` cache. The async
1080    /// flavour of that lowering has a sync resolver on the stub side
1081    /// that assumes the Hosted side has already populated the shared
1082    /// cache; if pruning ever dropped the Hosted half because the
1083    /// selected tests only parameterised on the stub view, that
1084    /// resolver would panic. Pairing the two as companions guarantees
1085    /// the Hosted half is retained whenever either half is needed.
1086    pub companions: Vec<String>,
1087}
1088
1089impl RegisteredDependency {
1090    /// Construct a `Shared` (legacy / default-scope) dependency. Preserves the
1091    /// pre-scopes constructor signature so downstream code that built
1092    /// `RegisteredDependency` directly keeps compiling.
1093    pub fn new_shared(
1094        name: String,
1095        crate_name: String,
1096        module_path: String,
1097        constructor: DependencyConstructor,
1098        dependencies: Vec<String>,
1099    ) -> Self {
1100        Self {
1101            name,
1102            crate_name,
1103            module_path,
1104            constructor,
1105            dependencies,
1106            scope: DepScope::Shared,
1107            worker_fn: None,
1108            cloneable_codec: None,
1109            hosted_codec: None,
1110            rpc_factory: None,
1111            companions: Vec::new(),
1112        }
1113    }
1114}
1115
1116impl Debug for RegisteredDependency {
1117    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1118        f.debug_struct("RegisteredDependency")
1119            .field("name", &self.name)
1120            .field("crate_name", &self.crate_name)
1121            .field("module_path", &self.module_path)
1122            .finish()
1123    }
1124}
1125
1126impl PartialEq for RegisteredDependency {
1127    fn eq(&self, other: &Self) -> bool {
1128        self.name == other.name
1129    }
1130}
1131
1132impl Eq for RegisteredDependency {}
1133
1134impl Hash for RegisteredDependency {
1135    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1136        self.name.hash(state);
1137    }
1138}
1139
1140impl RegisteredDependency {
1141    pub fn crate_and_module(&self) -> String {
1142        [&self.crate_name, &self.module_path]
1143            .into_iter()
1144            .filter(|s| !s.is_empty())
1145            .cloned()
1146            .collect::<Vec<String>>()
1147            .join("::")
1148    }
1149
1150    /// Fully-qualified identifier used for cross-process bookkeeping of
1151    /// Cloneable dependencies. The shape is `{crate_name}::{module_path}::{name}`
1152    /// with empty segments dropped, so two deps with the same `name` registered
1153    /// in different modules get distinct identifiers.
1154    pub fn qualified_id(&self) -> String {
1155        [&self.crate_name, &self.module_path, &self.name]
1156            .into_iter()
1157            .filter(|s| !s.is_empty())
1158            .cloned()
1159            .collect::<Vec<String>>()
1160            .join("::")
1161    }
1162}
1163
1164pub static REGISTERED_DEPENDENCY_CONSTRUCTORS: Mutex<Vec<RegisteredDependency>> =
1165    Mutex::new(Vec::new());
1166
1167#[derive(Debug, Clone)]
1168pub enum RegisteredTestSuiteProperty {
1169    Sequential {
1170        name: String,
1171        crate_name: String,
1172        module_path: String,
1173    },
1174    Tag {
1175        name: String,
1176        crate_name: String,
1177        module_path: String,
1178        tag: String,
1179    },
1180    Timeout {
1181        name: String,
1182        crate_name: String,
1183        module_path: String,
1184        timeout: Duration,
1185    },
1186}
1187
1188impl RegisteredTestSuiteProperty {
1189    pub fn crate_name(&self) -> &String {
1190        match self {
1191            RegisteredTestSuiteProperty::Sequential { crate_name, .. } => crate_name,
1192            RegisteredTestSuiteProperty::Tag { crate_name, .. } => crate_name,
1193            RegisteredTestSuiteProperty::Timeout { crate_name, .. } => crate_name,
1194        }
1195    }
1196
1197    pub fn module_path(&self) -> &String {
1198        match self {
1199            RegisteredTestSuiteProperty::Sequential { module_path, .. } => module_path,
1200            RegisteredTestSuiteProperty::Tag { module_path, .. } => module_path,
1201            RegisteredTestSuiteProperty::Timeout { module_path, .. } => module_path,
1202        }
1203    }
1204
1205    pub fn name(&self) -> &String {
1206        match self {
1207            RegisteredTestSuiteProperty::Sequential { name, .. } => name,
1208            RegisteredTestSuiteProperty::Tag { name, .. } => name,
1209            RegisteredTestSuiteProperty::Timeout { name, .. } => name,
1210        }
1211    }
1212
1213    pub fn crate_and_module(&self) -> String {
1214        [self.crate_name(), self.module_path(), self.name()]
1215            .into_iter()
1216            .filter(|s| !s.is_empty())
1217            .cloned()
1218            .collect::<Vec<String>>()
1219            .join("::")
1220    }
1221}
1222
1223pub static REGISTERED_TESTSUITE_PROPS: Mutex<Vec<RegisteredTestSuiteProperty>> =
1224    Mutex::new(Vec::new());
1225
1226#[derive(Clone)]
1227#[allow(clippy::type_complexity)]
1228pub enum TestGeneratorFunction {
1229    Sync(Arc<dyn Fn() -> Vec<GeneratedTest> + Send + Sync + 'static>),
1230    Async(
1231        Arc<
1232            dyn (Fn() -> Pin<Box<dyn Future<Output = Vec<GeneratedTest>> + Send>>)
1233                + Send
1234                + Sync
1235                + 'static,
1236        >,
1237    ),
1238}
1239
1240pub struct DynamicTestRegistration {
1241    tests: Vec<GeneratedTest>,
1242}
1243
1244impl Default for DynamicTestRegistration {
1245    fn default() -> Self {
1246        Self::new()
1247    }
1248}
1249
1250impl DynamicTestRegistration {
1251    pub fn new() -> Self {
1252        Self { tests: Vec::new() }
1253    }
1254
1255    pub fn to_vec(self) -> Vec<GeneratedTest> {
1256        self.tests
1257    }
1258
1259    pub fn add_sync_test<R: TestReturnValue + 'static>(
1260        &mut self,
1261        name: impl AsRef<str>,
1262        props: TestProperties,
1263        dependencies: Option<Vec<String>>,
1264        run: impl Fn(Arc<dyn DependencyView + Send + Sync>) -> R + Send + Sync + Clone + 'static,
1265    ) {
1266        self.tests.push(GeneratedTest {
1267            name: name.as_ref().to_string(),
1268            run: TestFunction::Sync(Arc::new(move |deps| {
1269                Box::new(run(deps)) as Box<dyn TestReturnValue>
1270            })),
1271            props,
1272            dependencies,
1273        });
1274    }
1275
1276    #[cfg(feature = "tokio")]
1277    pub fn add_async_test<R: TestReturnValue + 'static>(
1278        &mut self,
1279        name: impl AsRef<str>,
1280        props: TestProperties,
1281        dependencies: Option<Vec<String>>,
1282        run: impl (Fn(Arc<dyn DependencyView + Send + Sync>) -> Pin<Box<dyn Future<Output = R> + Send>>)
1283            + Send
1284            + Sync
1285            + Clone
1286            + 'static,
1287    ) {
1288        self.tests.push(GeneratedTest {
1289            name: name.as_ref().to_string(),
1290            run: TestFunction::Async(Arc::new(move |deps| {
1291                let run = run.clone();
1292                Box::pin(async move {
1293                    let r = run(deps).await;
1294                    Box::new(r) as Box<dyn TestReturnValue>
1295                })
1296            })),
1297            props,
1298            dependencies,
1299        });
1300    }
1301}
1302
1303#[derive(Clone)]
1304pub struct GeneratedTest {
1305    pub name: String,
1306    pub run: TestFunction,
1307    pub props: TestProperties,
1308    pub dependencies: Option<Vec<String>>,
1309}
1310
1311#[derive(Clone)]
1312pub struct RegisteredTestGenerator {
1313    pub name: String,
1314    pub crate_name: String,
1315    pub module_path: String,
1316    pub run: TestGeneratorFunction,
1317    pub is_ignored: bool,
1318}
1319
1320impl RegisteredTestGenerator {
1321    pub fn crate_and_module(&self) -> String {
1322        [&self.crate_name, &self.module_path]
1323            .into_iter()
1324            .filter(|s| !s.is_empty())
1325            .cloned()
1326            .collect::<Vec<String>>()
1327            .join("::")
1328    }
1329}
1330
1331pub static REGISTERED_TEST_GENERATORS: Mutex<Vec<RegisteredTestGenerator>> = Mutex::new(Vec::new());
1332
1333pub(crate) fn filter_test(test: &RegisteredTest, filter: &str, exact: bool) -> bool {
1334    if let Some(tag_list) = filter.strip_prefix(":tag:") {
1335        if tag_list.is_empty() {
1336            // Filtering for tags with NO TAGS
1337            test.props.tags.is_empty()
1338        } else {
1339            let or_tags = tag_list.split('|').collect::<Vec<&str>>();
1340            let mut result = false;
1341            for or_tag in or_tags {
1342                let and_tags = or_tag.split('&').collect::<Vec<&str>>();
1343                let mut and_result = true;
1344                for and_tag in and_tags {
1345                    if !test.props.tags.contains(&and_tag.to_string()) {
1346                        and_result = false;
1347                        break;
1348                    }
1349                }
1350                if and_result {
1351                    result = true;
1352                    break;
1353                }
1354            }
1355            result
1356        }
1357    } else if exact {
1358        test.filterable_name() == filter
1359    } else {
1360        test.filterable_name().contains(filter)
1361    }
1362}
1363
1364pub(crate) fn apply_suite_props_to_tests(
1365    tests: &[RegisteredTest],
1366    props: &[RegisteredTestSuiteProperty],
1367) -> Vec<RegisteredTest> {
1368    let props_with_prefix = props
1369        .iter()
1370        .map(|prop| (prop.crate_and_module(), prop))
1371        .collect::<Vec<_>>();
1372
1373    let mut result = Vec::new();
1374    for test in tests {
1375        let mut test = test.clone();
1376        for (prefix, prop) in &props_with_prefix {
1377            if test.crate_and_module().starts_with(prefix) {
1378                match prop {
1379                    RegisteredTestSuiteProperty::Tag { tag, .. } => {
1380                        test.props.tags.push(tag.clone());
1381                    }
1382                    RegisteredTestSuiteProperty::Timeout { timeout, .. } => {
1383                        if test.props.timeout.is_none() {
1384                            test.props.timeout = Some(*timeout);
1385                        }
1386                    }
1387                    RegisteredTestSuiteProperty::Sequential { .. } => {
1388                        // handled in TestSuiteExecution
1389                    }
1390                }
1391            }
1392        }
1393        result.push(test);
1394    }
1395    result
1396}
1397
1398pub(crate) fn filter_registered_tests(
1399    args: &Arguments,
1400    registered_tests: &[RegisteredTest],
1401) -> Vec<RegisteredTest> {
1402    registered_tests
1403        .iter()
1404        .filter(|registered_test| {
1405            !args
1406                .skip
1407                .iter()
1408                .any(|skip| filter_test(registered_test, skip, args.exact))
1409        })
1410        .filter(|registered_test| {
1411            args.filter.is_empty()
1412                || args
1413                    .filter
1414                    .iter()
1415                    .any(|filter| filter_test(registered_test, filter, args.exact))
1416        })
1417        .filter(|registered_tests| {
1418            (args.bench && registered_tests.run.is_bench())
1419                || (args.test && !registered_tests.run.is_bench())
1420                || (!args.bench && !args.test)
1421        })
1422        .filter(|registered_test| {
1423            !args.exclude_should_panic || registered_test.props.should_panic == ShouldPanic::No
1424        })
1425        .cloned()
1426        .collect::<Vec<_>>()
1427}
1428
1429fn add_generated_tests(
1430    target: &mut Vec<RegisteredTest>,
1431    generator: &RegisteredTestGenerator,
1432    generated: Vec<GeneratedTest>,
1433) {
1434    target.extend(generated.into_iter().map(|mut test| {
1435        test.props.is_ignored |= generator.is_ignored;
1436        RegisteredTest {
1437            name: format!("{}::{}", generator.name, test.name),
1438            crate_name: generator.crate_name.clone(),
1439            module_path: generator.module_path.clone(),
1440            run: test.run,
1441            props: test.props,
1442            dependencies: test.dependencies,
1443        }
1444    }));
1445}
1446
1447#[cfg(feature = "tokio")]
1448pub(crate) async fn generate_tests(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
1449    let mut result = Vec::new();
1450    for generator in generators {
1451        match &generator.run {
1452            TestGeneratorFunction::Sync(generator_fn) => {
1453                let tests = generator_fn();
1454                add_generated_tests(&mut result, generator, tests);
1455            }
1456            TestGeneratorFunction::Async(generator_fn) => {
1457                let tests = generator_fn().await;
1458                add_generated_tests(&mut result, generator, tests);
1459            }
1460        }
1461    }
1462    result
1463}
1464
1465pub(crate) fn generate_tests_sync(generators: &[RegisteredTestGenerator]) -> Vec<RegisteredTest> {
1466    let mut result = Vec::new();
1467    for generator in generators {
1468        match &generator.run {
1469            TestGeneratorFunction::Sync(generator_fn) => {
1470                let tests = generator_fn();
1471                add_generated_tests(&mut result, generator, tests);
1472            }
1473            TestGeneratorFunction::Async(_) => {
1474                panic!("Async test generators are not supported in sync mode")
1475            }
1476        }
1477    }
1478    result
1479}
1480
1481pub(crate) fn get_ensure_time(args: &Arguments, test: &RegisteredTest) -> Option<TimeThreshold> {
1482    let should_ensure_time = match test.props.ensure_time_control {
1483        ReportTimeControl::Default => args.ensure_time,
1484        ReportTimeControl::Enabled => true,
1485        ReportTimeControl::Disabled => false,
1486    };
1487    if should_ensure_time {
1488        match test.props.test_type {
1489            TestType::UnitTest => Some(args.unit_test_threshold()),
1490            TestType::IntegrationTest => Some(args.integration_test_threshold()),
1491        }
1492    } else {
1493        None
1494    }
1495}
1496
1497#[derive(Clone)]
1498pub enum TestResult {
1499    Passed {
1500        captured: Vec<CapturedOutput>,
1501        exec_time: Duration,
1502    },
1503    Benchmarked {
1504        captured: Vec<CapturedOutput>,
1505        exec_time: Duration,
1506        ns_iter_summ: Summary,
1507        mb_s: usize,
1508    },
1509    Failed {
1510        cause: FailureCause,
1511        captured: Vec<CapturedOutput>,
1512        exec_time: Duration,
1513    },
1514    Ignored {
1515        captured: Vec<CapturedOutput>,
1516    },
1517}
1518
1519impl TestResult {
1520    pub fn passed(exec_time: Duration) -> Self {
1521        TestResult::Passed {
1522            captured: Vec::new(),
1523            exec_time,
1524        }
1525    }
1526
1527    pub fn benchmarked(exec_time: Duration, ns_iter_summ: Summary, mb_s: usize) -> Self {
1528        TestResult::Benchmarked {
1529            captured: Vec::new(),
1530            exec_time,
1531            ns_iter_summ,
1532            mb_s,
1533        }
1534    }
1535
1536    pub fn failed(exec_time: Duration, cause: FailureCause) -> Self {
1537        TestResult::Failed {
1538            cause,
1539            captured: Vec::new(),
1540            exec_time,
1541        }
1542    }
1543
1544    pub fn ignored() -> Self {
1545        TestResult::Ignored {
1546            captured: Vec::new(),
1547        }
1548    }
1549
1550    pub(crate) fn is_passed(&self) -> bool {
1551        matches!(self, TestResult::Passed { .. })
1552    }
1553
1554    pub(crate) fn is_benchmarked(&self) -> bool {
1555        matches!(self, TestResult::Benchmarked { .. })
1556    }
1557
1558    pub(crate) fn is_failed(&self) -> bool {
1559        matches!(self, TestResult::Failed { .. })
1560    }
1561
1562    pub(crate) fn is_ignored(&self) -> bool {
1563        matches!(self, TestResult::Ignored { .. })
1564    }
1565
1566    pub(crate) fn captured_output(&self) -> &Vec<CapturedOutput> {
1567        match self {
1568            TestResult::Passed { captured, .. } => captured,
1569            TestResult::Failed { captured, .. } => captured,
1570            TestResult::Ignored { captured, .. } => captured,
1571            TestResult::Benchmarked { captured, .. } => captured,
1572        }
1573    }
1574
1575    pub(crate) fn stats(&self) -> Option<&Summary> {
1576        match self {
1577            TestResult::Benchmarked { ns_iter_summ, .. } => Some(ns_iter_summ),
1578            _ => None,
1579        }
1580    }
1581
1582    pub(crate) fn set_captured_output(&mut self, captured: Vec<CapturedOutput>) {
1583        match self {
1584            TestResult::Passed {
1585                captured: captured_ref,
1586                ..
1587            } => *captured_ref = captured,
1588            TestResult::Failed {
1589                captured: captured_ref,
1590                ..
1591            } => *captured_ref = captured,
1592            TestResult::Ignored {
1593                captured: captured_ref,
1594            } => *captured_ref = captured,
1595            TestResult::Benchmarked {
1596                captured: captured_ref,
1597                ..
1598            } => *captured_ref = captured,
1599        }
1600    }
1601
1602    pub(crate) fn from_result<A>(
1603        should_panic: &ShouldPanic,
1604        elapsed: Duration,
1605        result: Result<Result<A, FailureCause>, Box<dyn Any + Send>>,
1606    ) -> Self {
1607        match result {
1608            Ok(Ok(_)) => {
1609                if should_panic == &ShouldPanic::No {
1610                    TestResult::passed(elapsed)
1611                } else {
1612                    TestResult::failed(
1613                        elapsed,
1614                        FailureCause::HarnessError("Test did not panic as expected".to_string()),
1615                    )
1616                }
1617            }
1618            Ok(Err(cause)) => TestResult::failed(elapsed, cause),
1619            Err(panic) => TestResult::from_panic(should_panic, elapsed, panic),
1620        }
1621    }
1622
1623    pub(crate) fn from_summary(
1624        should_panic: &ShouldPanic,
1625        elapsed: Duration,
1626        result: Result<Summary, Box<dyn Any + Send>>,
1627        bytes: u64,
1628    ) -> Self {
1629        match result {
1630            Ok(summary) => {
1631                let ns_iter = max(summary.median as u64, 1);
1632                let mb_s = bytes * 1000 / ns_iter;
1633                TestResult::benchmarked(elapsed, summary, mb_s as usize)
1634            }
1635            Err(panic) => Self::from_panic(should_panic, elapsed, panic),
1636        }
1637    }
1638
1639    fn from_panic(
1640        should_panic: &ShouldPanic,
1641        elapsed: Duration,
1642        panic: Box<dyn Any + Send>,
1643    ) -> Self {
1644        let captured = crate::panic_hook::take_current_panic_capture();
1645
1646        let panic_cause = if let Some(cause) = captured {
1647            cause
1648        } else {
1649            let message = panic
1650                .downcast_ref::<String>()
1651                .cloned()
1652                .or(panic.downcast_ref::<&str>().map(|s| s.to_string()));
1653            PanicCause {
1654                message,
1655                location: None,
1656                backtrace: None,
1657            }
1658        };
1659
1660        match should_panic {
1661            ShouldPanic::WithMessage(expected) => match &panic_cause.message {
1662                Some(message) if message.contains(expected) => TestResult::passed(elapsed),
1663                _ => TestResult::failed(
1664                    elapsed,
1665                    FailureCause::Panic(PanicCause {
1666                        message: Some(format!(
1667                            "Test panicked with unexpected message: {}",
1668                            panic_cause.message.as_deref().unwrap_or_default()
1669                        )),
1670                        location: None,
1671                        backtrace: None,
1672                    }),
1673                ),
1674            },
1675            ShouldPanic::Yes => TestResult::passed(elapsed),
1676            ShouldPanic::No => TestResult::failed(elapsed, FailureCause::Panic(panic_cause)),
1677        }
1678    }
1679
1680    pub(crate) fn failure_message(&self) -> Option<String> {
1681        self.failure_cause().map(|c| c.render())
1682    }
1683
1684    pub fn failure_cause(&self) -> Option<&FailureCause> {
1685        match self {
1686            TestResult::Failed { cause, .. } => Some(cause),
1687            _ => None,
1688        }
1689    }
1690}
1691
1692pub struct SuiteResult {
1693    pub passed: usize,
1694    pub failed: usize,
1695    pub ignored: usize,
1696    pub measured: usize,
1697    pub filtered_out: usize,
1698    pub exec_time: Duration,
1699}
1700
1701impl SuiteResult {
1702    pub fn from_test_results(
1703        registered_tests: &[RegisteredTest],
1704        results: &[(RegisteredTest, TestResult)],
1705        exec_time: Duration,
1706    ) -> Self {
1707        let passed = results
1708            .iter()
1709            .filter(|(_, result)| result.is_passed())
1710            .count();
1711        let measured = results
1712            .iter()
1713            .filter(|(_, result)| result.is_benchmarked())
1714            .count();
1715        let failed = results
1716            .iter()
1717            .filter(|(_, result)| result.is_failed())
1718            .count();
1719        let ignored = results
1720            .iter()
1721            .filter(|(_, result)| result.is_ignored())
1722            .count();
1723        let filtered_out = registered_tests.len() - results.len();
1724
1725        Self {
1726            passed,
1727            failed,
1728            ignored,
1729            measured,
1730            filtered_out,
1731            exec_time,
1732        }
1733    }
1734
1735    pub fn exit_code(results: &[(RegisteredTest, TestResult)]) -> ExitCode {
1736        if results.iter().any(|(_, result)| result.is_failed()) {
1737            ExitCode::from(101)
1738        } else {
1739            ExitCode::SUCCESS
1740        }
1741    }
1742}
1743
1744pub trait DependencyView: Debug {
1745    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>>;
1746}
1747
1748impl DependencyView for Arc<dyn DependencyView + Send + Sync> {
1749    fn get(&self, name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
1750        self.as_ref().get(name)
1751    }
1752}
1753
1754#[derive(Debug, Clone, Eq, PartialEq)]
1755pub enum CapturedOutput {
1756    Stdout { timestamp: SystemTime, line: String },
1757    Stderr { timestamp: SystemTime, line: String },
1758}
1759
1760impl CapturedOutput {
1761    pub fn stdout(line: String) -> Self {
1762        CapturedOutput::Stdout {
1763            timestamp: SystemTime::now(),
1764            line,
1765        }
1766    }
1767
1768    pub fn stderr(line: String) -> Self {
1769        CapturedOutput::Stderr {
1770            timestamp: SystemTime::now(),
1771            line,
1772        }
1773    }
1774
1775    pub fn timestamp(&self) -> SystemTime {
1776        match self {
1777            CapturedOutput::Stdout { timestamp, .. } => *timestamp,
1778            CapturedOutput::Stderr { timestamp, .. } => *timestamp,
1779        }
1780    }
1781
1782    pub fn line(&self) -> &str {
1783        match self {
1784            CapturedOutput::Stdout { line, .. } => line,
1785            CapturedOutput::Stderr { line, .. } => line,
1786        }
1787    }
1788}
1789
1790impl PartialOrd for CapturedOutput {
1791    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1792        Some(self.cmp(other))
1793    }
1794}
1795
1796impl Ord for CapturedOutput {
1797    fn cmp(&self, other: &Self) -> Ordering {
1798        self.timestamp().cmp(&other.timestamp())
1799    }
1800}
1801
1802#[cfg(test)]
1803mod error_reporting_tests {
1804    use super::*;
1805    use std::panic::{catch_unwind, AssertUnwindSafe};
1806    use std::time::Duration;
1807
1808    fn simulate_runner(
1809        test_fn: impl FnOnce() -> Box<dyn TestReturnValue> + std::panic::UnwindSafe,
1810    ) -> TestResult {
1811        crate::panic_hook::install_panic_hook();
1812        let test_id = crate::panic_hook::next_test_id();
1813        crate::panic_hook::set_current_test_id(test_id);
1814        let result = catch_unwind(AssertUnwindSafe(move || {
1815            let ret = test_fn();
1816            ret.into_result()?;
1817            Ok(())
1818        }));
1819        let test_result =
1820            TestResult::from_result(&ShouldPanic::No, Duration::from_millis(1), result);
1821        crate::panic_hook::clear_current_test_id();
1822        test_result
1823    }
1824
1825    #[test]
1826    fn panic_with_assert_eq() {
1827        let result = simulate_runner(|| {
1828            assert_eq!(1, 2);
1829            Box::new(())
1830        });
1831        assert!(result.is_failed());
1832        let msg = result.failure_message().unwrap();
1833        println!("=== panic assert_eq failure message ===\n{msg}\n===");
1834        assert!(
1835            msg.contains("assertion `left == right` failed"),
1836            "Expected assertion message, got: {msg}"
1837        );
1838        assert!(
1839            msg.contains("at "),
1840            "Expected location info in message, got: {msg}"
1841        );
1842    }
1843
1844    #[test]
1845    fn string_error() {
1846        let result = simulate_runner(|| {
1847            let r: Result<(), String> = Err("something went wrong".to_string());
1848            Box::new(r)
1849        });
1850        assert!(result.is_failed());
1851        let msg = result.failure_message().unwrap();
1852        println!("=== string error failure message ===\n{msg}\n===");
1853        assert_eq!(msg, "something went wrong");
1854    }
1855
1856    #[test]
1857    fn anyhow_error() {
1858        let result = simulate_runner(|| {
1859            let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1860            let err = anyhow::anyhow!(inner).context("operation failed");
1861            let r: Result<(), anyhow::Error> = Err(err);
1862            Box::new(r)
1863        });
1864        assert!(result.is_failed());
1865        let msg = result.failure_message().unwrap();
1866        println!("=== anyhow error failure message ===\n{msg}\n===");
1867        assert!(
1868            msg.contains("operation failed"),
1869            "Expected 'operation failed', got: {msg}"
1870        );
1871        assert!(
1872            msg.contains("file not found"),
1873            "Expected 'file not found', got: {msg}"
1874        );
1875    }
1876
1877    #[test]
1878    fn std_io_error() {
1879        let result = simulate_runner(|| {
1880            let r: Result<(), std::io::Error> = Err(std::io::Error::new(
1881                std::io::ErrorKind::NotFound,
1882                "file not found",
1883            ));
1884            Box::new(r)
1885        });
1886        assert!(result.is_failed());
1887        let msg = result.failure_message().unwrap();
1888        println!("=== std io error failure message ===\n{msg}\n===");
1889        // Should use Display (not Debug), so no "Custom { kind: NotFound, ... }"
1890        assert_eq!(msg, "file not found");
1891    }
1892
1893    #[test]
1894    fn panic_with_location_info() {
1895        let result = simulate_runner(|| {
1896            panic!("test panic with location");
1897            #[allow(unreachable_code)]
1898            Box::new(())
1899        });
1900        assert!(result.is_failed());
1901        let cause = result.failure_cause().unwrap();
1902        match cause {
1903            FailureCause::Panic(p) => {
1904                assert!(p.location.is_some(), "Expected location info");
1905                let loc = p.location.as_ref().unwrap();
1906                assert!(
1907                    loc.file.contains("internal.rs"),
1908                    "Expected file to contain internal.rs, got: {}",
1909                    loc.file
1910                );
1911                assert!(loc.line > 0, "Expected non-zero line number");
1912            }
1913            other => panic!("Expected Panic cause, got: {other:?}"),
1914        }
1915    }
1916
1917    #[test]
1918    fn panic_render_includes_location() {
1919        let result = simulate_runner(|| {
1920            panic!("location test");
1921            #[allow(unreachable_code)]
1922            Box::new(())
1923        });
1924        let msg = result.failure_message().unwrap();
1925        assert!(
1926            msg.contains("location test"),
1927            "Expected panic message, got: {msg}"
1928        );
1929        assert!(
1930            msg.contains("\n  at "),
1931            "Expected location line in render, got: {msg}"
1932        );
1933    }
1934
1935    #[test]
1936    fn should_panic_with_message_matching() {
1937        crate::panic_hook::install_panic_hook();
1938        let test_id = crate::panic_hook::next_test_id();
1939        crate::panic_hook::set_current_test_id(test_id);
1940        let result = catch_unwind(AssertUnwindSafe(|| {
1941            panic!("expected panic message");
1942        }));
1943        let test_result = TestResult::from_result(
1944            &ShouldPanic::WithMessage("expected panic".to_string()),
1945            Duration::from_millis(1),
1946            result.map(|_| Ok(())),
1947        );
1948        crate::panic_hook::clear_current_test_id();
1949        assert!(
1950            test_result.is_passed(),
1951            "Expected test to pass with matching panic message"
1952        );
1953    }
1954
1955    #[test]
1956    fn should_panic_with_wrong_message() {
1957        crate::panic_hook::install_panic_hook();
1958        let test_id = crate::panic_hook::next_test_id();
1959        crate::panic_hook::set_current_test_id(test_id);
1960        let result = catch_unwind(AssertUnwindSafe(|| {
1961            panic!("actual panic message");
1962        }));
1963        let test_result = TestResult::from_result(
1964            &ShouldPanic::WithMessage("completely different".to_string()),
1965            Duration::from_millis(1),
1966            result.map(|_| Ok(())),
1967        );
1968        crate::panic_hook::clear_current_test_id();
1969        assert!(
1970            test_result.is_failed(),
1971            "Expected test to fail with wrong panic message"
1972        );
1973        let msg = test_result.failure_message().unwrap();
1974        assert!(
1975            msg.contains("unexpected message"),
1976            "Expected 'unexpected message' in: {msg}"
1977        );
1978    }
1979
1980    #[test]
1981    fn pretty_assertions_diff() {
1982        let result = simulate_runner(|| {
1983            pretty_assertions::assert_eq!("hello world\nfoo\nbar\n", "hello world\nbaz\nbar\n");
1984            Box::new(())
1985        });
1986        assert!(result.is_failed());
1987        let cause = result.failure_cause().unwrap();
1988
1989        // Should be a Panic variant (assert_eq! panics)
1990        let panic_cause = match cause {
1991            FailureCause::Panic(p) => p,
1992            other => panic!("Expected Panic cause, got: {other:?}"),
1993        };
1994
1995        // The panic message should contain the colorful diff from pretty_assertions
1996        let message = panic_cause.message.as_deref().unwrap();
1997        println!("=== pretty_assertions failure message ===\n{message}\n===");
1998        assert!(
1999            message.contains("foo") && message.contains("baz"),
2000            "Expected diff with 'foo' and 'baz', got: {message}"
2001        );
2002
2003        // Location should be captured
2004        assert!(panic_cause.location.is_some(), "Expected location info");
2005
2006        // The rendered output should NOT contain backtrace noise when RUST_BACKTRACE is unset
2007        let rendered = cause.render();
2008        println!("=== pretty_assertions rendered ===\n{rendered}\n===");
2009        assert!(
2010            !rendered.contains("stack backtrace") && !rendered.contains("Stack backtrace"),
2011            "Expected no backtrace noise in rendered output, got: {rendered}"
2012        );
2013        // Should contain location
2014        assert!(
2015            rendered.contains("\n  at "),
2016            "Expected location in rendered output, got: {rendered}"
2017        );
2018    }
2019
2020    #[test]
2021    fn detached_thread_panic_detected() {
2022        crate::panic_hook::install_panic_hook();
2023        let test_id = crate::panic_hook::next_test_id();
2024        crate::panic_hook::set_current_test_id(test_id);
2025        crate::panic_hook::create_detached_collector(test_id);
2026
2027        let result = catch_unwind(AssertUnwindSafe(|| {
2028            let handle = crate::spawn::spawn_thread(|| {
2029                panic!("background thread panic");
2030            });
2031            let _ = handle.join();
2032        }));
2033
2034        let mut test_result = TestResult::from_result(
2035            &ShouldPanic::No,
2036            Duration::from_millis(1),
2037            result.map(|_| Ok(())),
2038        );
2039
2040        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
2041            let panics = match collector.lock() {
2042                Ok(p) => p,
2043                Err(poisoned) => poisoned.into_inner(),
2044            };
2045            if !panics.is_empty() && test_result.is_passed() {
2046                let messages: Vec<String> = panics.iter().map(|p| p.render()).collect();
2047                test_result = TestResult::failed(
2048                    Duration::from_millis(1),
2049                    FailureCause::Panic(PanicCause {
2050                        message: Some(format!(
2051                            "Detached task(s) panicked:\n{}",
2052                            messages.join("\n---\n")
2053                        )),
2054                        location: panics.first().and_then(|p| p.location.clone()),
2055                        backtrace: panics.first().and_then(|p| p.backtrace.clone()),
2056                    }),
2057                );
2058            }
2059        }
2060
2061        crate::panic_hook::clear_current_test_id();
2062
2063        assert!(
2064            test_result.is_failed(),
2065            "Expected test to fail due to detached panic"
2066        );
2067        let msg = test_result.failure_message().unwrap();
2068        assert!(
2069            msg.contains("Detached task(s) panicked"),
2070            "Expected detached panic message, got: {msg}"
2071        );
2072        assert!(
2073            msg.contains("background thread panic"),
2074            "Expected original panic message, got: {msg}"
2075        );
2076    }
2077
2078    #[test]
2079    fn detached_thread_panic_ignored_with_policy() {
2080        crate::panic_hook::install_panic_hook();
2081        let test_id = crate::panic_hook::next_test_id();
2082        crate::panic_hook::set_current_test_id(test_id);
2083        crate::panic_hook::create_detached_collector(test_id);
2084
2085        let result = catch_unwind(AssertUnwindSafe(|| {
2086            let handle = crate::spawn::spawn_thread(|| {
2087                panic!("ignored thread panic");
2088            });
2089            let _ = handle.join();
2090        }));
2091
2092        let test_result = TestResult::from_result(
2093            &ShouldPanic::No,
2094            Duration::from_millis(1),
2095            result.map(|_| Ok(())),
2096        );
2097
2098        if let Some(collector) = crate::panic_hook::take_detached_collector(test_id) {
2099            let panics = match collector.lock() {
2100                Ok(p) => p,
2101                Err(poisoned) => poisoned.into_inner(),
2102            };
2103            // Verify panics were captured but Ignore policy does not fail the test
2104            assert!(
2105                !panics.is_empty(),
2106                "Expected panics in collector even with Ignore policy"
2107            );
2108        }
2109
2110        crate::panic_hook::clear_current_test_id();
2111
2112        assert!(
2113            test_result.is_passed(),
2114            "Expected test to pass with Ignore policy"
2115        );
2116    }
2117
2118    #[cfg(feature = "tokio")]
2119    #[test]
2120    fn detached_task_panic_detected() {
2121        let rt = tokio::runtime::Runtime::new().unwrap();
2122        rt.block_on(async {
2123            crate::panic_hook::install_panic_hook();
2124            let test_id = crate::panic_hook::next_test_id();
2125            crate::panic_hook::set_current_test_id(test_id);
2126            crate::panic_hook::create_detached_collector(test_id);
2127
2128            let handle = crate::spawn::spawn(async {
2129                panic!("detached task panic");
2130            });
2131            let _ = handle.await;
2132
2133            let collector = crate::panic_hook::take_detached_collector(test_id).unwrap();
2134            let panics = collector.lock().unwrap();
2135
2136            assert_eq!(panics.len(), 1);
2137            assert!(
2138                panics[0]
2139                    .message
2140                    .as_ref()
2141                    .unwrap()
2142                    .contains("detached task panic"),
2143                "Expected panic message, got: {:?}",
2144                panics[0].message
2145            );
2146
2147            crate::panic_hook::clear_current_test_id();
2148        });
2149    }
2150
2151    #[test]
2152    fn failure_cause_variants() {
2153        // ReturnedMessage
2154        let cause = FailureCause::ReturnedMessage("simple message".to_string());
2155        assert_eq!(cause.render(), "simple message");
2156        assert!(cause.panic_message().is_none());
2157
2158        // ReturnedError (prefer display)
2159        let cause = FailureCause::ReturnedError {
2160            display: "display text".to_string(),
2161            debug: "debug text".to_string(),
2162            prefer_debug: false,
2163            error: Arc::new("display text".to_string()),
2164        };
2165        assert_eq!(cause.render(), "display text");
2166
2167        // ReturnedError (prefer debug, e.g. anyhow)
2168        let cause = FailureCause::ReturnedError {
2169            display: "display text".to_string(),
2170            debug: "debug text".to_string(),
2171            prefer_debug: true,
2172            error: Arc::new("debug text".to_string()),
2173        };
2174        assert_eq!(cause.render(), "debug text");
2175
2176        // HarnessError
2177        let cause = FailureCause::HarnessError("harness error".to_string());
2178        assert_eq!(cause.render(), "harness error");
2179
2180        // Panic with message
2181        let cause = FailureCause::Panic(PanicCause {
2182            message: Some("panic msg".to_string()),
2183            location: None,
2184            backtrace: None,
2185        });
2186        assert_eq!(cause.render(), "panic msg");
2187        assert_eq!(cause.panic_message(), Some("panic msg"));
2188    }
2189}
2190
2191#[cfg(test)]
2192mod filter_tests {
2193    use super::*;
2194
2195    fn make_test(name: &str, module_path: &str) -> RegisteredTest {
2196        RegisteredTest {
2197            name: name.to_string(),
2198            crate_name: "mycrate".to_string(),
2199            module_path: module_path.to_string(),
2200            run: TestFunction::Sync(Arc::new(|_| Box::new(()))),
2201            props: TestProperties::default(),
2202            dependencies: None,
2203        }
2204    }
2205
2206    fn make_tagged_test(name: &str, module_path: &str, tags: Vec<&str>) -> RegisteredTest {
2207        let mut test = make_test(name, module_path);
2208        test.props.tags = tags.into_iter().map(String::from).collect();
2209        test
2210    }
2211
2212    fn make_args(filters: Vec<&str>, skip: Vec<&str>, exact: bool) -> Arguments {
2213        Arguments {
2214            filter: filters.into_iter().map(String::from).collect(),
2215            skip: skip.into_iter().map(String::from).collect(),
2216            exact,
2217            ..Default::default()
2218        }
2219    }
2220
2221    fn filtered_names(args: &Arguments, tests: &[RegisteredTest]) -> Vec<String> {
2222        filter_registered_tests(args, tests)
2223            .into_iter()
2224            .map(|t| t.filterable_name())
2225            .collect()
2226    }
2227
2228    // --- filter_test unit tests ---
2229
2230    #[test]
2231    fn filter_test_substring_match() {
2232        let test = make_test("hello_world", "mod1");
2233        assert!(filter_test(&test, "hello", false));
2234        assert!(filter_test(&test, "world", false));
2235        assert!(filter_test(&test, "mod1::hello", false));
2236        assert!(!filter_test(&test, "nonexistent", false));
2237    }
2238
2239    #[test]
2240    fn filter_test_exact_match() {
2241        let test = make_test("hello_world", "mod1");
2242        assert!(filter_test(&test, "mod1::hello_world", true));
2243        assert!(!filter_test(&test, "hello_world", true));
2244        assert!(!filter_test(&test, "hello", true));
2245    }
2246
2247    #[test]
2248    fn filter_test_tag_match() {
2249        let test = make_tagged_test("t1", "mod1", vec!["fast", "unit"]);
2250        assert!(filter_test(&test, ":tag:fast", false));
2251        assert!(filter_test(&test, ":tag:unit", false));
2252        assert!(!filter_test(&test, ":tag:slow", false));
2253    }
2254
2255    #[test]
2256    fn filter_test_tag_empty_matches_untagged() {
2257        let untagged = make_test("t1", "mod1");
2258        let tagged = make_tagged_test("t2", "mod1", vec!["fast"]);
2259        assert!(filter_test(&untagged, ":tag:", false));
2260        assert!(!filter_test(&tagged, ":tag:", false));
2261    }
2262
2263    // --- filter_registered_tests: multiple include filters (OR semantics) ---
2264
2265    #[test]
2266    fn no_filters_includes_all() {
2267        let tests = vec![make_test("a", "m"), make_test("b", "m")];
2268        let args = make_args(vec![], vec![], false);
2269        assert_eq!(filtered_names(&args, &tests), vec!["m::a", "m::b"]);
2270    }
2271
2272    #[test]
2273    fn single_filter_substring() {
2274        let tests = vec![
2275            make_test("alpha", "m"),
2276            make_test("beta", "m"),
2277            make_test("alphabet", "m"),
2278        ];
2279        let args = make_args(vec!["alpha"], vec![], false);
2280        assert_eq!(
2281            filtered_names(&args, &tests),
2282            vec!["m::alpha", "m::alphabet"]
2283        );
2284    }
2285
2286    #[test]
2287    fn multiple_filters_or_semantics() {
2288        let tests = vec![
2289            make_test("alpha", "m"),
2290            make_test("beta", "m"),
2291            make_test("gamma", "m"),
2292        ];
2293        let args = make_args(vec!["alpha", "gamma"], vec![], false);
2294        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::gamma"]);
2295    }
2296
2297    #[test]
2298    fn multiple_filters_exact() {
2299        let tests = vec![
2300            make_test("alpha", "m"),
2301            make_test("alphabet", "m"),
2302            make_test("beta", "m"),
2303        ];
2304        let args = make_args(vec!["m::alpha", "m::beta"], vec![], true);
2305        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha", "m::beta"]);
2306    }
2307
2308    // --- skip behavior ---
2309
2310    #[test]
2311    fn skip_substring_match() {
2312        let tests = vec![
2313            make_test("fast_test", "m"),
2314            make_test("slow_test", "m"),
2315            make_test("slower_test", "m"),
2316        ];
2317        let args = make_args(vec![], vec!["slow"], false);
2318        assert_eq!(filtered_names(&args, &tests), vec!["m::fast_test"]);
2319    }
2320
2321    #[test]
2322    fn skip_exact_match() {
2323        let tests = vec![make_test("slow_test", "m"), make_test("slower_test", "m")];
2324        let args = make_args(vec![], vec!["m::slow_test"], true);
2325        assert_eq!(filtered_names(&args, &tests), vec!["m::slower_test"]);
2326    }
2327
2328    #[test]
2329    fn skip_with_tag() {
2330        let tests = vec![
2331            make_tagged_test("t1", "m", vec!["slow"]),
2332            make_tagged_test("t2", "m", vec!["fast"]),
2333            make_test("t3", "m"),
2334        ];
2335        let args = make_args(vec![], vec![":tag:slow"], false);
2336        assert_eq!(filtered_names(&args, &tests), vec!["m::t2", "m::t3"]);
2337    }
2338
2339    // --- combined include + skip ---
2340
2341    #[test]
2342    fn include_and_skip_combined() {
2343        let tests = vec![
2344            make_test("alpha_fast", "m"),
2345            make_test("alpha_slow", "m"),
2346            make_test("beta_fast", "m"),
2347        ];
2348        // Include anything with "alpha", but skip anything with "slow"
2349        let args = make_args(vec!["alpha"], vec!["slow"], false);
2350        assert_eq!(filtered_names(&args, &tests), vec!["m::alpha_fast"]);
2351    }
2352
2353    #[test]
2354    fn skip_wins_over_include() {
2355        let tests = vec![make_test("target", "m")];
2356        // Both include and skip match the same test — skip should win
2357        let args = make_args(vec!["target"], vec!["target"], false);
2358        assert_eq!(filtered_names(&args, &tests), Vec::<String>::new());
2359    }
2360
2361    // --- tag boolean expression syntax ---
2362
2363    #[test]
2364    fn filter_test_tag_or_expression() {
2365        // `:tag:a|b` matches tests tagged with `a` OR `b`
2366        let test_a = make_tagged_test("t1", "m", vec!["a"]);
2367        let test_b = make_tagged_test("t2", "m", vec!["b"]);
2368        let test_c = make_tagged_test("t3", "m", vec!["c"]);
2369        assert!(filter_test(&test_a, ":tag:a|b", false));
2370        assert!(filter_test(&test_b, ":tag:a|b", false));
2371        assert!(!filter_test(&test_c, ":tag:a|b", false));
2372    }
2373
2374    #[test]
2375    fn filter_test_tag_and_expression() {
2376        // `:tag:a&b` matches tests tagged with BOTH `a` AND `b`
2377        let test_ab = make_tagged_test("t1", "m", vec!["a", "b"]);
2378        let test_a = make_tagged_test("t2", "m", vec!["a"]);
2379        let test_b = make_tagged_test("t3", "m", vec!["b"]);
2380        assert!(filter_test(&test_ab, ":tag:a&b", false));
2381        assert!(!filter_test(&test_a, ":tag:a&b", false));
2382        assert!(!filter_test(&test_b, ":tag:a&b", false));
2383    }
2384
2385    #[test]
2386    fn filter_test_tag_mixed_and_or() {
2387        // `:tag:a|b&c` means `a OR (b AND c)` — `&` has higher precedence
2388        let test_a = make_tagged_test("t1", "m", vec!["a"]);
2389        let test_bc = make_tagged_test("t2", "m", vec!["b", "c"]);
2390        let test_b = make_tagged_test("t3", "m", vec!["b"]);
2391        let test_c = make_tagged_test("t4", "m", vec!["c"]);
2392        let test_none = make_test("t5", "m");
2393        assert!(filter_test(&test_a, ":tag:a|b&c", false));
2394        assert!(filter_test(&test_bc, ":tag:a|b&c", false));
2395        assert!(!filter_test(&test_b, ":tag:a|b&c", false));
2396        assert!(!filter_test(&test_c, ":tag:a|b&c", false));
2397        assert!(!filter_test(&test_none, ":tag:a|b&c", false));
2398    }
2399
2400    #[test]
2401    fn filter_test_tag_exact_flag_does_not_affect_tags() {
2402        // `--exact` should not change tag matching behavior
2403        let test = make_tagged_test("t1", "m", vec!["fast"]);
2404        assert!(filter_test(&test, ":tag:fast", true));
2405        assert!(!filter_test(&test, ":tag:slow", true));
2406    }
2407
2408    #[test]
2409    fn include_by_tag_or_expression() {
2410        let tests = vec![
2411            make_tagged_test("t1", "m", vec!["unit"]),
2412            make_tagged_test("t2", "m", vec!["integration"]),
2413            make_tagged_test("t3", "m", vec!["e2e"]),
2414        ];
2415        let args = make_args(vec![":tag:unit|integration"], vec![], false);
2416        assert_eq!(filtered_names(&args, &tests), vec!["m::t1", "m::t2"]);
2417    }
2418
2419    #[test]
2420    fn skip_by_tag_and_expression() {
2421        let tests = vec![
2422            make_tagged_test("t1", "m", vec!["slow", "network"]),
2423            make_tagged_test("t2", "m", vec!["slow"]),
2424            make_tagged_test("t3", "m", vec!["network"]),
2425            make_test("t4", "m"),
2426        ];
2427        // Skip only tests that are BOTH slow AND network
2428        let args = make_args(vec![], vec![":tag:slow&network"], false);
2429        assert_eq!(
2430            filtered_names(&args, &tests),
2431            vec!["m::t2", "m::t3", "m::t4"]
2432        );
2433    }
2434}