Skip to main content

test_r_core/
lib.rs

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