Skip to main content

test_r_core/
lib.rs

1pub mod args;
2pub mod bench;
3mod execution;
4pub mod internal;
5mod ipc;
6mod output;
7mod panic_hook;
8pub mod spawn;
9mod stats;
10#[cfg(feature = "tokio")]
11mod tokio;
12pub mod worker;
13
14#[allow(dead_code)]
15mod sync;
16
17#[cfg(not(feature = "tokio"))]
18pub use sync::test_runner;
19
20#[cfg(feature = "tokio")]
21pub use tokio::test_runner;
22
23pub use worker::worker_index;
24
25/// Re-export of [`desert_rust`] so that proc-macros emitted by
26/// `test-r-macro` (e.g. [`test_r::hosted_rpc`]) can refer to it via
27/// `::test_r::core::desert_rust::...` without forcing downstream
28/// users to add `desert_rust` to their own `Cargo.toml`.
29///
30/// This is an internal support re-export — user code should not depend
31/// on it being available at this path. The `#[doc(hidden)]` attribute
32/// keeps it out of the rendered docs and signals that the surface
33/// belongs to the macro support layer, not the public test-r API.
34#[doc(hidden)]
35pub use desert_rust;
36
37// =====================================================================
38// Hosted descriptor codec / worker reconstructor helpers.
39//
40// These two functions are the single place the descriptor-based Hosted
41// dep wiring is built. The `tokio` cargo feature on `test-r-core`
42// selects which variant of each function compiles, so that:
43//
44// - Under the **tokio** runtime, Hosted deps always use the *async*
45//   path (`AsyncHostedDep::descriptor` / `from_descriptor`). Thanks to the
46//   blanket `impl<T: HostedDep> AsyncHostedDep for T`, every sync `HostedDep`
47//   automatically reaches the async path with no user-visible cost (the
48//   bridged `from_descriptor` just wraps the sync impl in
49//   `std::future::ready(...)`).
50// - Under the **sync** runtime, Hosted deps always use the *sync* path
51//   (`HostedDep::descriptor` / `from_descriptor`). This intentionally
52//   keeps sync builds free of any block-poll machinery: a Hosted dep
53//   that only implements `AsyncHostedDep` simply fails to compile
54//   here, rather than panicking at runtime.
55//
56// The macro now emits a single uniform call to these helpers regardless
57// of the (now deprecated) `async_worker` attribute; see
58// `test-r-macro/src/deps.rs`.
59// =====================================================================
60
61/// **Hidden macro-support helper.**
62///
63/// Build the parent-side codec for a Hosted dep. Under the `tokio`
64/// runtime this dispatches through [`internal::AsyncHostedDep`]; under
65/// the sync runtime it dispatches through [`internal::HostedDep`]. The
66/// returned [`internal::CloneableCodec`] knows how to:
67///
68/// - downcast the owner `Arc<dyn Any + Send + Sync>` back to `T` and
69///   produce the descriptor bytes (`to_wire`), and
70/// - turn raw descriptor bytes into a boxed payload that the matching
71///   reconstructor below will hand off to `from_descriptor`
72///   (`from_wire_bytes`).
73///
74/// Not part of the public API; only the proc-macro emits calls to it.
75#[doc(hidden)]
76#[cfg(feature = "tokio")]
77pub fn __test_r_make_hosted_codec<T>() -> internal::CloneableCodec
78where
79    T: internal::AsyncHostedDep,
80{
81    use std::sync::Arc;
82    internal::CloneableCodec {
83        to_wire: Arc::new(|any: Arc<dyn std::any::Any + Send + Sync>| {
84            let value: Arc<T> = any
85                .downcast::<T>()
86                .expect("Hosted dependency type mismatch in descriptor()");
87            <T as internal::AsyncHostedDep>::descriptor(&*value)
88        }),
89        from_wire_bytes: Arc::new(|bytes: &[u8]| {
90            // The "wire payload" for a Hosted dep is the raw descriptor
91            // bytes; the matching reconstructor will run from_descriptor
92            // against them on the worker side.
93            let boxed: Arc<dyn std::any::Any + Send + Sync> = Arc::new(bytes.to_vec());
94            boxed
95        }),
96    }
97}
98
99/// **Hidden macro-support helper.** Sync-runtime variant of
100/// [`__test_r_make_hosted_codec`]; see that doc-comment.
101#[doc(hidden)]
102#[cfg(not(feature = "tokio"))]
103pub fn __test_r_make_hosted_codec<T>() -> internal::CloneableCodec
104where
105    T: internal::HostedDep,
106{
107    use std::sync::Arc;
108    internal::CloneableCodec {
109        to_wire: Arc::new(|any: Arc<dyn std::any::Any + Send + Sync>| {
110            let value: Arc<T> = any
111                .downcast::<T>()
112                .expect("Hosted dependency type mismatch in descriptor()");
113            <T as internal::HostedDep>::descriptor(&*value)
114        }),
115        from_wire_bytes: Arc::new(|bytes: &[u8]| {
116            let boxed: Arc<dyn std::any::Any + Send + Sync> = Arc::new(bytes.to_vec());
117            boxed
118        }),
119    }
120}
121
122/// **Hidden macro-support helper.**
123///
124/// Build the worker-side [`internal::WorkerReconstructor`] for a Hosted
125/// dep. Under the `tokio` runtime this returns an
126/// [`internal::WorkerReconstructor::Async`] closure that awaits
127/// [`internal::AsyncHostedDep::from_descriptor`]; under the sync
128/// runtime it returns an [`internal::WorkerReconstructor::Sync`]
129/// closure that calls [`internal::HostedDep::from_descriptor`].
130///
131/// The descriptor `Vec<u8>` is produced by the matching
132/// [`__test_r_make_hosted_codec`] above; the two helpers must stay in
133/// lockstep.
134///
135/// Not part of the public API; only the proc-macro emits calls to it.
136#[doc(hidden)]
137#[cfg(feature = "tokio")]
138pub fn __test_r_make_hosted_worker_reconstructor<T>() -> internal::WorkerReconstructor
139where
140    T: internal::AsyncHostedDep,
141{
142    use std::sync::Arc;
143    internal::WorkerReconstructor::Async(Arc::new(
144        |wire_payload: Arc<dyn std::any::Any + Send + Sync>, _deps| {
145            Box::pin(async move {
146                let bytes: Arc<Vec<u8>> = wire_payload
147                    .downcast::<Vec<u8>>()
148                    .expect("Hosted worker reconstructor expected Vec<u8> descriptor payload");
149                let value: T = <T as internal::AsyncHostedDep>::from_descriptor(&bytes).await;
150                let boxed: Arc<dyn std::any::Any + Send + Sync> = Arc::new(value);
151                boxed
152            })
153        },
154    ))
155}
156
157/// **Hidden macro-support helper.** Sync-runtime variant of
158/// [`__test_r_make_hosted_worker_reconstructor`]; see that
159/// doc-comment.
160#[doc(hidden)]
161#[cfg(not(feature = "tokio"))]
162pub fn __test_r_make_hosted_worker_reconstructor<T>() -> internal::WorkerReconstructor
163where
164    T: internal::HostedDep,
165{
166    use std::sync::Arc;
167    internal::WorkerReconstructor::Sync(Arc::new(
168        |wire_payload: Arc<dyn std::any::Any + Send + Sync>, _deps| {
169            let bytes: Arc<Vec<u8>> = wire_payload
170                .downcast::<Vec<u8>>()
171                .expect("Hosted worker reconstructor expected Vec<u8> descriptor payload");
172            let value: T = <T as internal::HostedDep>::from_descriptor(&bytes);
173            let boxed: Arc<dyn std::any::Any + Send + Sync> = Arc::new(value);
174            boxed
175        },
176    ))
177}
178
179// =====================================================================
180// `worker = both(T)` helpers.
181//
182// `#[test_dep(scope = Hosted, worker = both(Trait))]` is lowered by the
183// macro into two `RegisteredDependency` entries (one Hosted, one
184// HostedRpc) that share a single parent-side owner via
185// [`internal::HostedBothShared`]. The three helpers below centralize
186// the shared logic:
187//
188// - `__test_r_make_hosted_both_shared::<T>(owner_arc, rpc_cell)` builds
189//   the cell used by the macro's weak cache from an already-`Arc`'d
190//   owner plus a prebuilt RPC cell. The macro acquire helper
191//   constructs the `Arc<T>` once and shares it between this cell, the
192//   RPC dispatch cell, and the parent-side owner getter — no second
193//   owner instance, no `T: Clone` requirement. Cfg-selected on `tokio`
194//   so the descriptor call uses `AsyncHostedDep::descriptor` under
195//   tokio and `HostedDep::descriptor` under sync, mirroring the
196//   single-view helpers.
197// - `__test_r_make_hosted_both_codec()` produces the Hosted-view
198//   codec; both bytes (`to_wire`) and payload (`from_wire_bytes`)
199//   shapes match the existing single-view Hosted codec so the
200//   runtime worker side stays unchanged.
201// - `__test_r_make_hosted_both_rpc_factory::<T>()` produces the
202//   HostedRpc-view factory. It downcasts the shared cell to extract
203//   the inner `Arc<HostedRpcOwnerCell>`, and reuses the same
204//   `build_stub(channel)` path the legacy HostedRpc factory uses.
205// =====================================================================
206
207/// **Hidden macro-support helper.** Build the shared owner cell for
208/// a `worker = both(T)` dep from an already-`Arc`'d owner plus a
209/// pre-built RPC cell. The macro acquire helper constructs `Arc<T>`
210/// once and clones it into both this helper and the RPC cell so the
211/// parent-side getter, the descriptor view, and the RPC dispatcher
212/// observe exactly one owner instance.
213///
214/// Tokio variant: descriptor is computed via
215/// [`internal::AsyncHostedDep::descriptor`].
216#[doc(hidden)]
217#[cfg(feature = "tokio")]
218pub fn __test_r_make_hosted_both_shared<T>(
219    owner: std::sync::Arc<T>,
220    rpc_cell: std::sync::Arc<internal::HostedRpcOwnerCell>,
221) -> internal::HostedBothShared
222where
223    T: internal::AsyncHostedDep,
224{
225    use std::sync::Arc;
226    let descriptor_bytes = <T as internal::AsyncHostedDep>::descriptor(&*owner);
227    let owner_any: Arc<dyn std::any::Any + Send + Sync> = owner;
228    internal::HostedBothShared::new(descriptor_bytes, owner_any, rpc_cell)
229}
230
231/// **Hidden macro-support helper.** Sync-runtime variant of
232/// [`__test_r_make_hosted_both_shared`]; descriptor is computed via
233/// [`internal::HostedDep::descriptor`].
234#[doc(hidden)]
235#[cfg(not(feature = "tokio"))]
236pub fn __test_r_make_hosted_both_shared<T>(
237    owner: std::sync::Arc<T>,
238    rpc_cell: std::sync::Arc<internal::HostedRpcOwnerCell>,
239) -> internal::HostedBothShared
240where
241    T: internal::HostedDep,
242{
243    use std::sync::Arc;
244    let descriptor_bytes = <T as internal::HostedDep>::descriptor(&*owner);
245    let owner_any: Arc<dyn std::any::Any + Send + Sync> = owner;
246    internal::HostedBothShared::new(descriptor_bytes, owner_any, rpc_cell)
247}
248
249/// **Hidden macro-support helper.** Wrap a HostedRpc owner value into a
250/// [`internal::HostedRpcOwnerCell`]. The tokio variant goes through
251/// [`internal::HostedRpcOwnerCell::from_async_owner`] so async owners
252/// are dispatched asynchronously; the sync variant uses the back-compat
253/// sync constructor. Used by the `#[test_dep(scope = HostedRpc)]`
254/// lowering so the choice of async vs sync cell happens in one place.
255#[doc(hidden)]
256#[cfg(feature = "tokio")]
257pub fn __test_r_make_hosted_rpc_cell<T>(owner: T) -> internal::HostedRpcOwnerCell
258where
259    T: internal::AsyncHostedRpcDep,
260{
261    internal::HostedRpcOwnerCell::from_async_owner(owner)
262}
263
264/// **Hidden macro-support helper.** Sync-runtime variant of
265/// [`__test_r_make_hosted_rpc_cell`].
266#[doc(hidden)]
267#[cfg(not(feature = "tokio"))]
268pub fn __test_r_make_hosted_rpc_cell<T>(owner: T) -> internal::HostedRpcOwnerCell
269where
270    T: internal::HostedRpcDep,
271{
272    internal::HostedRpcOwnerCell::from_owner(owner)
273}
274
275/// **Hidden macro-support helper.** Hosted-view codec for the `both`
276/// variant. Wire format is identical to the existing single-view
277/// Hosted codec so the worker reconstructor (still
278/// [`__test_r_make_hosted_worker_reconstructor`]) stays unchanged.
279#[doc(hidden)]
280pub fn __test_r_make_hosted_both_codec() -> internal::CloneableCodec {
281    use std::any::Any;
282    use std::sync::Arc;
283    internal::CloneableCodec {
284        to_wire: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
285            let shared: Arc<internal::HostedBothShared> = any
286                .downcast::<internal::HostedBothShared>()
287                .expect("HostedBothShared downcast failed in both-codec to_wire");
288            shared.descriptor_bytes().to_vec()
289        }),
290        from_wire_bytes: Arc::new(|bytes: &[u8]| {
291            // Same payload shape as the single-view Hosted codec: a
292            // boxed `Vec<u8>` for the worker reconstructor to consume.
293            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(bytes.to_vec());
294            boxed
295        }),
296    }
297}
298
299/// **Hidden macro-support helper.** HostedRpc-view factory for the
300/// `both` variant. Pulls the inner `Arc<HostedRpcOwnerCell>` out of
301/// the shared cell and reuses the user's
302/// [`internal::HostedRpcDep::build_stub`] for the worker stub.
303#[doc(hidden)]
304#[cfg(feature = "tokio")]
305pub fn __test_r_make_hosted_both_rpc_factory<T, Stub>() -> internal::RpcFactory
306where
307    T: internal::AsyncHostedRpcDep<Stub = Stub>,
308    Stub: Send + Sync + 'static,
309{
310    use std::any::Any;
311    use std::sync::Arc;
312    internal::RpcFactory {
313        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
314            let shared: Arc<internal::HostedBothShared> = any
315                .downcast::<internal::HostedBothShared>()
316                .expect("HostedBothShared downcast failed in both-rpc-factory owner_into_cell");
317            shared.rpc_cell()
318        }),
319        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
320            let stub: Stub = <T as internal::AsyncHostedRpcDep>::build_stub(channel);
321            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
322            boxed
323        }),
324    }
325}
326
327/// **Hidden macro-support helper.** Sync-runtime variant of
328/// [`__test_r_make_hosted_both_rpc_factory`]. The `build_stub` is sourced
329/// from [`internal::HostedRpcDep::build_stub`] because the sync runtime
330/// cannot drive `AsyncHostedRpcDep` owners.
331#[doc(hidden)]
332#[cfg(not(feature = "tokio"))]
333pub fn __test_r_make_hosted_both_rpc_factory<T, Stub>() -> internal::RpcFactory
334where
335    T: internal::HostedRpcDep<Stub = Stub>,
336    Stub: Send + Sync + 'static,
337{
338    use std::any::Any;
339    use std::sync::Arc;
340    internal::RpcFactory {
341        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
342            let shared: Arc<internal::HostedBothShared> = any
343                .downcast::<internal::HostedBothShared>()
344                .expect("HostedBothShared downcast failed in both-rpc-factory owner_into_cell");
345            shared.rpc_cell()
346        }),
347        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
348            let stub: Stub = <T as internal::HostedRpcDep>::build_stub(channel);
349            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
350            boxed
351        }),
352    }
353}
354
355/// **Hidden macro-support helper.** Build a `RpcFactory` for the
356/// stand-alone `scope = HostedRpc` lowering (no `both(T)` companion).
357/// Tokio variant goes through [`internal::AsyncHostedRpcDep::build_stub`]
358/// so async owners flow through one entry point.
359#[doc(hidden)]
360#[cfg(feature = "tokio")]
361pub fn __test_r_make_hosted_rpc_factory<T, Stub>() -> internal::RpcFactory
362where
363    T: internal::AsyncHostedRpcDep<Stub = Stub>,
364    Stub: Send + Sync + 'static,
365{
366    use std::any::Any;
367    use std::sync::Arc;
368    internal::RpcFactory {
369        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
370            any.downcast::<internal::HostedRpcOwnerCell>()
371                .expect("HostedRpc owner downcast to HostedRpcOwnerCell failed")
372        }),
373        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
374            let stub: Stub = <T as internal::AsyncHostedRpcDep>::build_stub(channel);
375            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
376            boxed
377        }),
378    }
379}
380
381/// **Hidden macro-support helper.** Sync-runtime variant of
382/// [`__test_r_make_hosted_rpc_factory`].
383#[doc(hidden)]
384#[cfg(not(feature = "tokio"))]
385pub fn __test_r_make_hosted_rpc_factory<T, Stub>() -> internal::RpcFactory
386where
387    T: internal::HostedRpcDep<Stub = Stub>,
388    Stub: Send + Sync + 'static,
389{
390    use std::any::Any;
391    use std::sync::Arc;
392    internal::RpcFactory {
393        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
394            any.downcast::<internal::HostedRpcOwnerCell>()
395                .expect("HostedRpc owner downcast to HostedRpcOwnerCell failed")
396        }),
397        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
398            let stub: Stub = <T as internal::HostedRpcDep>::build_stub(channel);
399            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
400            boxed
401        }),
402    }
403}
404
405#[cfg(test)]
406mod hosted_helper_tests {
407    //! Exercise the feature-gated
408    //! [`__test_r_make_hosted_codec`] /
409    //! [`__test_r_make_hosted_worker_reconstructor`] helpers end to
410    //! end against a tiny `HostedDep` fixture.
411    //!
412    //! Both helper variants must reject `WorkerReconstructor::Sync`
413    //! vs `Async` choice at the cargo-feature level, so we keep this
414    //! test cfg-aware: it asserts the matching variant under each
415    //! feature.
416    use super::*;
417    use std::any::Any;
418    use std::sync::Arc;
419
420    /// Minimal sync `HostedDep` fixture. Under the `tokio` feature
421    /// the blanket bridge also makes it `AsyncHostedDep`, so the same fixture
422    /// is usable against both helper variants.
423    #[derive(Debug, PartialEq, Eq)]
424    struct Fixture {
425        bytes: Vec<u8>,
426    }
427
428    impl internal::HostedDep for Fixture {
429        fn descriptor(&self) -> Vec<u8> {
430            self.bytes.clone()
431        }
432        fn from_descriptor(bytes: &[u8]) -> Self {
433            Self {
434                bytes: bytes.to_vec(),
435            }
436        }
437    }
438
439    #[test]
440    fn make_hosted_codec_round_trips_descriptor_bytes() {
441        let codec = __test_r_make_hosted_codec::<Fixture>();
442        let owner: Arc<dyn Any + Send + Sync> = Arc::new(Fixture {
443            bytes: vec![1, 2, 3, 4],
444        });
445
446        let wire_bytes = (codec.to_wire)(owner);
447        assert_eq!(wire_bytes, vec![1, 2, 3, 4]);
448
449        let wire_payload = (codec.from_wire_bytes)(&wire_bytes);
450        let recovered_bytes: Arc<Vec<u8>> = wire_payload
451            .downcast::<Vec<u8>>()
452            .expect("from_wire_bytes must produce Arc<Vec<u8>>");
453        assert_eq!(*recovered_bytes, vec![1, 2, 3, 4]);
454    }
455
456    /// Under the tokio runtime, the worker reconstructor helper must
457    /// return [`internal::WorkerReconstructor::Async`].
458    #[cfg(feature = "tokio")]
459    #[test]
460    fn make_hosted_worker_reconstructor_is_async_under_tokio() {
461        let recon = __test_r_make_hosted_worker_reconstructor::<Fixture>();
462        match recon {
463            internal::WorkerReconstructor::Async(_) => {}
464            internal::WorkerReconstructor::Sync(_) => panic!(
465                "tokio build must produce a WorkerReconstructor::Async for Hosted deps; got Sync"
466            ),
467        }
468    }
469
470    /// Under the sync runtime, the worker reconstructor helper must
471    /// return [`internal::WorkerReconstructor::Sync`] so the sync
472    /// runner can drive it without any block-poll machinery.
473    #[cfg(not(feature = "tokio"))]
474    #[test]
475    fn make_hosted_worker_reconstructor_is_sync_under_sync_runtime() {
476        let recon = __test_r_make_hosted_worker_reconstructor::<Fixture>();
477        match recon {
478            internal::WorkerReconstructor::Sync(_) => {}
479            internal::WorkerReconstructor::Async(_) => panic!(
480                "sync build must produce a WorkerReconstructor::Sync for Hosted deps; got Async"
481            ),
482        }
483    }
484
485    /// Drive the sync-runtime reconstructor closure end to end on
486    /// the matching descriptor bytes the codec produces. Pinned only
487    /// for the sync build because the tokio build returns an Async
488    /// closure that needs a runtime to await.
489    #[cfg(not(feature = "tokio"))]
490    #[test]
491    fn sync_worker_reconstructor_rebuilds_fixture_from_descriptor() {
492        // Re-use a small `DependencyView` impl: the helper's worker
493        // closure ignores the view, so we pass an empty stub.
494        #[derive(Debug)]
495        struct EmptyView;
496        impl internal::DependencyView for EmptyView {
497            fn get(&self, _name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
498                None
499            }
500        }
501
502        let codec = __test_r_make_hosted_codec::<Fixture>();
503        let recon = __test_r_make_hosted_worker_reconstructor::<Fixture>();
504
505        let owner: Arc<dyn Any + Send + Sync> = Arc::new(Fixture {
506            bytes: vec![5, 6, 7],
507        });
508        let wire_bytes = (codec.to_wire)(owner);
509        let payload = (codec.from_wire_bytes)(&wire_bytes);
510
511        let deps: Arc<dyn internal::DependencyView + Send + Sync> = Arc::new(EmptyView);
512        let rebuilt = match recon {
513            internal::WorkerReconstructor::Sync(f) => f(payload, deps),
514            internal::WorkerReconstructor::Async(_) => unreachable!(
515                "sync build cannot return Async; pinned by \
516                 make_hosted_worker_reconstructor_is_sync_under_sync_runtime",
517            ),
518        };
519        let rebuilt: Arc<Fixture> = rebuilt
520            .downcast::<Fixture>()
521            .expect("worker reconstructor must produce the original Hosted dep type");
522        assert_eq!(
523            *rebuilt,
524            Fixture {
525                bytes: vec![5, 6, 7]
526            }
527        );
528    }
529
530    // -----------------------------------------------------------------
531    // `worker = both(T)` helper tests.
532    //
533    // The macro lowering for `#[test_dep(scope = Hosted, worker =
534    // both(Trait))]` is exercised end-to-end by the
535    // `sharing::hosted_both_basic` example fixtures. These unit tests
536    // pin the three pieces of cargo-feature-aware glue in this file:
537    //
538    // - `__test_r_make_hosted_both_shared::<T>(owner_arc, rpc_cell)`
539    //   builds the shared cell that the macro's weak cache hands back
540    //   to both the Hosted and HostedRpc registrations, from an
541    //   already-`Arc`'d owner and the prebuilt
542    //   [`internal::HostedRpcOwnerCell`] the macro produces.
543    // - `__test_r_make_hosted_both_codec()` serializes the cached
544    //   descriptor bytes for the Hosted view.
545    // - `__test_r_make_hosted_both_rpc_factory::<T>()` extracts the
546    //   inner `Arc<HostedRpcOwnerCell>` for the HostedRpc view and
547    //   builds the worker-side stub via `HostedRpcDep::build_stub`.
548    // -----------------------------------------------------------------
549
550    /// Minimal `HostedDep + HostedRpcDep` fixture for helper tests. The id
551    /// allocator stands in for any tiny control surface; the bytes field doubles
552    /// as the descriptor.
553    #[derive(Debug)]
554    struct BothFixture {
555        bytes: Vec<u8>,
556        counter: std::sync::Mutex<u64>,
557    }
558
559    impl BothFixture {
560        fn new(bytes: Vec<u8>) -> Self {
561            Self {
562                bytes,
563                counter: std::sync::Mutex::new(0),
564            }
565        }
566    }
567
568    impl internal::HostedDep for BothFixture {
569        fn descriptor(&self) -> Vec<u8> {
570            self.bytes.clone()
571        }
572        fn from_descriptor(bytes: &[u8]) -> Self {
573            Self::new(bytes.to_vec())
574        }
575    }
576
577    /// Stub view for the BothFixture. Holds a HostedRpcChannel so a
578    /// realistic build_stub round-trip is exercised; the tests below
579    /// just verify the factory hands back a usable `Arc<BothStub>`,
580    /// not a full IPC round-trip (covered by the example fixtures).
581    pub struct BothStub {
582        _channel: internal::HostedRpcChannel,
583    }
584
585    impl internal::HostedRpcDep for BothFixture {
586        type Stub = BothStub;
587        fn dispatch(&mut self, method_idx: u32, args: &[u8]) -> Result<Vec<u8>, String> {
588            // Delegate to the shared `&self` dispatcher so the same
589            // implementation services both the legacy `from_owner`
590            // path (which expects `&mut self`) and the new
591            // `from_shared_owner_*` path (which expects `&self`).
592            // Mirrors the shape of what `#[hosted_rpc]`'s
593            // blanket-implemented dispatcher will look like once we
594            // add the shared variant.
595            #[cfg(feature = "tokio")]
596            {
597                futures::executor::block_on(Self::dispatch_shared(self, method_idx, args))
598            }
599            #[cfg(not(feature = "tokio"))]
600            {
601                Self::dispatch_shared(self, method_idx, args)
602            }
603        }
604        fn build_stub(channel: internal::HostedRpcChannel) -> Self::Stub {
605            BothStub { _channel: channel }
606        }
607    }
608
609    impl BothFixture {
610        /// `&self`-receiver dispatcher used by the shared-owner cell
611        /// (`from_shared_owner_sync` / `from_shared_owner_async`). The
612        /// sync runtime returns a plain `Result`; the tokio runtime
613        /// wraps it in an `async fn` so the same body satisfies the
614        /// `Pin<Box<dyn Future + Send + 'a>>` closure shape.
615        #[cfg(not(feature = "tokio"))]
616        fn dispatch_shared(this: &Self, method_idx: u32, _args: &[u8]) -> Result<Vec<u8>, String> {
617            if method_idx == 1 {
618                let mut g = this.counter.lock().map_err(|e| e.to_string())?;
619                *g += 1;
620                Ok(g.to_be_bytes().to_vec())
621            } else {
622                Err(format!("BothFixture: unknown method_idx {method_idx}"))
623            }
624        }
625
626        /// Tokio variant of [`Self::dispatch_shared`]. Returning an
627        /// `async fn` lets the boxed-future closure used by
628        /// [`internal::HostedRpcOwnerCell::from_shared_owner_async`]
629        /// hold the resulting future for the duration of the call.
630        #[cfg(feature = "tokio")]
631        async fn dispatch_shared(
632            this: &Self,
633            method_idx: u32,
634            _args: &[u8],
635        ) -> Result<Vec<u8>, String> {
636            if method_idx == 1 {
637                let mut g = this.counter.lock().map_err(|e| e.to_string())?;
638                *g += 1;
639                Ok(g.to_be_bytes().to_vec())
640            } else {
641                Err(format!("BothFixture: unknown method_idx {method_idx}"))
642            }
643        }
644    }
645
646    /// Build a `HostedBothShared` cell for tests in the shape the new
647    /// macro acquire helper produces: one `Arc<T>` owner shared
648    /// between the cell and the descriptor view, with a closure-based
649    /// shared-owner cell so dispatch routes against `&T`. Used by the
650    /// helper tests below so they don't have to repeat the closure
651    /// glue, and so the parent-side `owner_arc::<T>()` accessor sees
652    /// the exact same `Arc<T>` the cell holds.
653    fn build_both_shared_for_test(
654        owner: BothFixture,
655    ) -> (Arc<BothFixture>, internal::HostedBothShared) {
656        let owner_arc = Arc::new(owner);
657        let cell_arc = build_both_rpc_cell_for_test(owner_arc.clone());
658        let shared = __test_r_make_hosted_both_shared::<BothFixture>(owner_arc.clone(), cell_arc);
659        (owner_arc, shared)
660    }
661
662    /// Build the shared `Arc<HostedRpcOwnerCell>` for the test
663    /// fixture. The closure delegates to the fixture's
664    /// `dispatch_shared` (a tiny `&self` dispatcher that mirrors what
665    /// `#[hosted_rpc]` will generate via `dispatch_<snake>_shared`).
666    /// Cargo-feature-aware so the same construction works under sync
667    /// and tokio runtimes.
668    fn build_both_rpc_cell_for_test(owner: Arc<BothFixture>) -> Arc<internal::HostedRpcOwnerCell> {
669        #[cfg(feature = "tokio")]
670        {
671            Arc::new(internal::HostedRpcOwnerCell::from_shared_owner_async(
672                owner,
673                |o, idx, args| Box::pin(BothFixture::dispatch_shared(o, idx, args)),
674            ))
675        }
676        #[cfg(not(feature = "tokio"))]
677        {
678            Arc::new(internal::HostedRpcOwnerCell::from_shared_owner_sync(
679                owner,
680                |o, idx, args| BothFixture::dispatch_shared(o, idx, args),
681            ))
682        }
683    }
684
685    /// `__test_r_make_hosted_both_shared(owner_arc, rpc_cell)` captures
686    /// the owner's descriptor bytes once and stitches together the
687    /// shared `Arc<T>` with the prebuilt `HostedRpcOwnerCell`. The
688    /// captured descriptor must match the owner's `HostedDep::descriptor()`
689    /// (or `AsyncHostedDep::descriptor()` under tokio) output exactly.
690    #[test]
691    fn make_hosted_both_shared_captures_descriptor_bytes() {
692        let (_owner, shared) = build_both_shared_for_test(BothFixture::new(vec![10, 20, 30]));
693        assert_eq!(shared.descriptor_bytes(), &[10, 20, 30]);
694        // The owner cell must be live: a dispatch call must succeed
695        // (the closure runs the method without panicking and returns
696        // a non-empty reply). Under the tokio feature the cell is the
697        // async variant — drive it through `dispatch_async` on a
698        // tokio runtime; under the sync feature the cell is sync.
699        let reply =
700            dispatch_cell_for_test(&shared.rpc_cell(), 1, &[]).expect("dispatch must succeed");
701        assert_eq!(reply, 1u64.to_be_bytes().to_vec());
702    }
703
704    /// The owner the cell dispatches against must be the exact same
705    /// `Arc<T>` the parent-side getter would return. This pins the
706    /// regression fix for the bug where `worker = both(...)` made the
707    /// parent dep map hold an `Arc<HostedBothShared>` while the
708    /// generated owner getter expected `Arc<T>` and panicked.
709    #[test]
710    fn make_hosted_both_shared_exposes_owner_arc() {
711        let (owner_arc, shared) = build_both_shared_for_test(BothFixture::new(vec![42]));
712        let recovered = shared.owner_arc::<BothFixture>();
713        assert!(
714            Arc::ptr_eq(&owner_arc, &recovered),
715            "owner_arc::<T>() must return the very same Arc the cell holds"
716        );
717        // A round trip through the cell observes the same owner via
718        // the shared counter that owner_arc points at.
719        let _ = dispatch_cell_for_test(&shared.rpc_cell(), 1, &[]).expect("dispatch must succeed");
720        let observed = *recovered
721            .counter
722            .lock()
723            .expect("BothFixture counter must not be poisoned");
724        assert_eq!(
725            observed, 1,
726            "the cell must mutate the same owner the parent getter would hand out, observed {observed}"
727        );
728    }
729
730    /// Helper: dispatch on a `HostedRpcOwnerCell` whose async/sync
731    /// variant depends on the active cargo feature. Used by the
732    /// `worker = both(T)` helper tests so the test bodies don't need
733    /// to know whether the cell was built via `from_owner` or
734    /// `from_async_owner` — both surface the same per-call result.
735    fn dispatch_cell_for_test(
736        cell: &internal::HostedRpcOwnerCell,
737        method_idx: u32,
738        args: &[u8],
739    ) -> Result<Vec<u8>, String> {
740        #[cfg(feature = "tokio")]
741        {
742            // `tokio` resolves to the local `mod tokio` inside this
743            // crate; reach the external crate with `::tokio`.
744            let rt = ::tokio::runtime::Builder::new_multi_thread()
745                .enable_all()
746                .build()
747                .expect("build tokio runtime");
748            rt.block_on(cell.dispatch_async(method_idx, args))
749        }
750        #[cfg(not(feature = "tokio"))]
751        {
752            cell.dispatch(method_idx, args)
753        }
754    }
755
756    /// `__test_r_make_hosted_both_codec().to_wire` downcasts the
757    /// shared cell and returns its captured descriptor bytes — the
758    /// same shape the worker-side reconstructor expects.
759    #[test]
760    fn make_hosted_both_codec_serializes_descriptor_bytes() {
761        let codec = __test_r_make_hosted_both_codec();
762        let (_owner, shared) = build_both_shared_for_test(BothFixture::new(vec![1, 2, 3, 4]));
763        let shared = Arc::new(shared);
764        let arc_any: Arc<dyn Any + Send + Sync> = shared;
765
766        let wire_bytes = (codec.to_wire)(arc_any);
767        assert_eq!(wire_bytes, vec![1, 2, 3, 4]);
768
769        // `from_wire_bytes` must produce the same `Arc<Vec<u8>>`
770        // payload shape the existing Hosted reconstructor consumes —
771        // otherwise the worker side would not be able to reuse the
772        // standard `__test_r_make_hosted_worker_reconstructor`.
773        let wire_payload = (codec.from_wire_bytes)(&wire_bytes);
774        let recovered_bytes: Arc<Vec<u8>> = wire_payload
775            .downcast::<Vec<u8>>()
776            .expect("from_wire_bytes must produce Arc<Vec<u8>>");
777        assert_eq!(*recovered_bytes, vec![1, 2, 3, 4]);
778    }
779
780    /// `__test_r_make_hosted_both_rpc_factory::<T, Stub>().owner_into_cell`
781    /// reaches into the shared cell, hands back the inner
782    /// `Arc<HostedRpcOwnerCell>`, and a dispatched call hits the
783    /// real owner (proven by the counter incrementing).
784    #[test]
785    fn make_hosted_both_rpc_factory_extracts_owner_cell() {
786        let factory = __test_r_make_hosted_both_rpc_factory::<BothFixture, BothStub>();
787        let (_owner, shared) = build_both_shared_for_test(BothFixture::new(vec![]));
788        let shared = Arc::new(shared);
789        let arc_any: Arc<dyn Any + Send + Sync> = shared.clone();
790
791        let cell = (factory.owner_into_cell)(arc_any);
792        assert!(
793            Arc::ptr_eq(&cell, &shared.rpc_cell()),
794            "factory must return the exact same inner HostedRpcOwnerCell Arc the \
795             shared cell holds; otherwise the RPC view would dispatch against a \
796             different owner than the descriptor view captured"
797        );
798
799        // The cell is functional: dispatch routes to the real owner
800        // method (counter starts at 0; first call must yield 1). Same
801        // cargo-feature-aware dispatch as
802        // `make_hosted_both_shared_captures_descriptor_bytes` since the
803        // underlying cell shares the same async/sync split.
804        let reply = dispatch_cell_for_test(&cell, 1, &[]).expect("dispatch must succeed");
805        assert_eq!(reply, 1u64.to_be_bytes().to_vec());
806    }
807
808    /// `__test_r_make_hosted_both_rpc_factory::<T, Stub>().build_stub`
809    /// constructs a worker-side `Stub` via the user's
810    /// `HostedRpcDep::build_stub` and boxes it as `Arc<dyn Any>` so
811    /// the runtime can route it through the standard dep view.
812    #[test]
813    fn make_hosted_both_rpc_factory_builds_stub() {
814        use internal::{HostedRpcChannel, HostedRpcError, HostedRpcTransport};
815
816        // Minimal in-process transport stand-in: every call returns
817        // the unit reply. We don't actually call into it; the test
818        // just exercises that build_stub produces a downcastable
819        // `Arc<dyn Any>` carrying the channel.
820        struct DummyTransport;
821        impl HostedRpcTransport for DummyTransport {
822            fn call(
823                &self,
824                _dep_id: &str,
825                _method_idx: u32,
826                _args: Vec<u8>,
827            ) -> Result<Vec<u8>, HostedRpcError> {
828                Ok(Vec::new())
829            }
830        }
831
832        let factory = __test_r_make_hosted_both_rpc_factory::<BothFixture, BothStub>();
833        let transport: Arc<dyn HostedRpcTransport> = Arc::new(DummyTransport);
834        let channel = HostedRpcChannel::new("test::both_fixture".to_string(), transport);
835
836        let stub_any: Arc<dyn Any + Send + Sync> = (factory.build_stub)(channel);
837        let _stub: Arc<BothStub> = stub_any
838            .downcast::<BothStub>()
839            .expect("build_stub must produce the BothFixture::Stub type");
840    }
841}