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)` builds the cell
189//   used by the macro's weak cache. Cfg-selected on `tokio` so the descriptor
190//   call uses `AsyncHostedDep::descriptor` under tokio and
191//   `HostedDep::descriptor` under sync, mirroring the single-view helpers.
192// - `__test_r_make_hosted_both_codec()` produces the Hosted-view
193//   codec; both bytes (`to_wire`) and payload (`from_wire_bytes`)
194//   shapes match the existing single-view Hosted codec so the
195//   runtime worker side stays unchanged.
196// - `__test_r_make_hosted_both_rpc_factory::<T>()` produces the
197//   HostedRpc-view factory. It downcasts the shared cell to extract
198//   the inner `Arc<HostedRpcOwnerCell>`, and reuses the same
199//   `build_stub(channel)` path the legacy HostedRpc factory uses.
200// =====================================================================
201
202/// **Hidden macro-support helper.** Build the shared owner cell for
203/// a `worker = both(T)` dep. Tokio variant: descriptor is computed
204/// via [`internal::AsyncHostedDep::descriptor`] and the RPC cell is
205/// constructed via [`internal::HostedRpcOwnerCell::from_async_owner`]
206/// so both sync and async HostedRpc owners flow through the same
207/// async dispatch path.
208#[doc(hidden)]
209#[cfg(feature = "tokio")]
210pub fn __test_r_make_hosted_both_shared<T>(owner: T) -> internal::HostedBothShared
211where
212    T: internal::AsyncHostedDep + internal::AsyncHostedRpcDep,
213{
214    use std::sync::Arc;
215    let descriptor_bytes = <T as internal::AsyncHostedDep>::descriptor(&owner);
216    let rpc_cell = Arc::new(internal::HostedRpcOwnerCell::from_async_owner(owner));
217    internal::HostedBothShared::new(descriptor_bytes, rpc_cell)
218}
219
220/// **Hidden macro-support helper.** Sync-runtime variant of
221/// [`__test_r_make_hosted_both_shared`]; descriptor is computed via
222/// [`internal::HostedDep::descriptor`].
223#[doc(hidden)]
224#[cfg(not(feature = "tokio"))]
225pub fn __test_r_make_hosted_both_shared<T>(owner: T) -> internal::HostedBothShared
226where
227    T: internal::HostedDep + internal::HostedRpcDep,
228{
229    use std::sync::Arc;
230    let descriptor_bytes = <T as internal::HostedDep>::descriptor(&owner);
231    let rpc_cell = Arc::new(internal::HostedRpcOwnerCell::from_owner(owner));
232    internal::HostedBothShared::new(descriptor_bytes, rpc_cell)
233}
234
235/// **Hidden macro-support helper.** Wrap a HostedRpc owner value into a
236/// [`internal::HostedRpcOwnerCell`]. The tokio variant goes through
237/// [`internal::HostedRpcOwnerCell::from_async_owner`] so async owners
238/// are dispatched asynchronously; the sync variant uses the back-compat
239/// sync constructor. Used by the `#[test_dep(scope = HostedRpc)]`
240/// lowering so the choice of async vs sync cell happens in one place.
241#[doc(hidden)]
242#[cfg(feature = "tokio")]
243pub fn __test_r_make_hosted_rpc_cell<T>(owner: T) -> internal::HostedRpcOwnerCell
244where
245    T: internal::AsyncHostedRpcDep,
246{
247    internal::HostedRpcOwnerCell::from_async_owner(owner)
248}
249
250/// **Hidden macro-support helper.** Sync-runtime variant of
251/// [`__test_r_make_hosted_rpc_cell`].
252#[doc(hidden)]
253#[cfg(not(feature = "tokio"))]
254pub fn __test_r_make_hosted_rpc_cell<T>(owner: T) -> internal::HostedRpcOwnerCell
255where
256    T: internal::HostedRpcDep,
257{
258    internal::HostedRpcOwnerCell::from_owner(owner)
259}
260
261/// **Hidden macro-support helper.** Hosted-view codec for the `both`
262/// variant. Wire format is identical to the existing single-view
263/// Hosted codec so the worker reconstructor (still
264/// [`__test_r_make_hosted_worker_reconstructor`]) stays unchanged.
265#[doc(hidden)]
266pub fn __test_r_make_hosted_both_codec() -> internal::CloneableCodec {
267    use std::any::Any;
268    use std::sync::Arc;
269    internal::CloneableCodec {
270        to_wire: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
271            let shared: Arc<internal::HostedBothShared> = any
272                .downcast::<internal::HostedBothShared>()
273                .expect("HostedBothShared downcast failed in both-codec to_wire");
274            shared.descriptor_bytes().to_vec()
275        }),
276        from_wire_bytes: Arc::new(|bytes: &[u8]| {
277            // Same payload shape as the single-view Hosted codec: a
278            // boxed `Vec<u8>` for the worker reconstructor to consume.
279            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(bytes.to_vec());
280            boxed
281        }),
282    }
283}
284
285/// **Hidden macro-support helper.** HostedRpc-view factory for the
286/// `both` variant. Pulls the inner `Arc<HostedRpcOwnerCell>` out of
287/// the shared cell and reuses the user's
288/// [`internal::HostedRpcDep::build_stub`] for the worker stub.
289#[doc(hidden)]
290#[cfg(feature = "tokio")]
291pub fn __test_r_make_hosted_both_rpc_factory<T, Stub>() -> internal::RpcFactory
292where
293    T: internal::AsyncHostedRpcDep<Stub = Stub>,
294    Stub: Send + Sync + 'static,
295{
296    use std::any::Any;
297    use std::sync::Arc;
298    internal::RpcFactory {
299        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
300            let shared: Arc<internal::HostedBothShared> = any
301                .downcast::<internal::HostedBothShared>()
302                .expect("HostedBothShared downcast failed in both-rpc-factory owner_into_cell");
303            shared.rpc_cell()
304        }),
305        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
306            let stub: Stub = <T as internal::AsyncHostedRpcDep>::build_stub(channel);
307            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
308            boxed
309        }),
310    }
311}
312
313/// **Hidden macro-support helper.** Sync-runtime variant of
314/// [`__test_r_make_hosted_both_rpc_factory`]. The `build_stub` is sourced
315/// from [`internal::HostedRpcDep::build_stub`] because the sync runtime
316/// cannot drive `AsyncHostedRpcDep` owners.
317#[doc(hidden)]
318#[cfg(not(feature = "tokio"))]
319pub fn __test_r_make_hosted_both_rpc_factory<T, Stub>() -> internal::RpcFactory
320where
321    T: internal::HostedRpcDep<Stub = Stub>,
322    Stub: Send + Sync + 'static,
323{
324    use std::any::Any;
325    use std::sync::Arc;
326    internal::RpcFactory {
327        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
328            let shared: Arc<internal::HostedBothShared> = any
329                .downcast::<internal::HostedBothShared>()
330                .expect("HostedBothShared downcast failed in both-rpc-factory owner_into_cell");
331            shared.rpc_cell()
332        }),
333        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
334            let stub: Stub = <T as internal::HostedRpcDep>::build_stub(channel);
335            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
336            boxed
337        }),
338    }
339}
340
341/// **Hidden macro-support helper.** Build a `RpcFactory` for the
342/// stand-alone `scope = HostedRpc` lowering (no `both(T)` companion).
343/// Tokio variant goes through [`internal::AsyncHostedRpcDep::build_stub`]
344/// so async owners flow through one entry point.
345#[doc(hidden)]
346#[cfg(feature = "tokio")]
347pub fn __test_r_make_hosted_rpc_factory<T, Stub>() -> internal::RpcFactory
348where
349    T: internal::AsyncHostedRpcDep<Stub = Stub>,
350    Stub: Send + Sync + 'static,
351{
352    use std::any::Any;
353    use std::sync::Arc;
354    internal::RpcFactory {
355        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
356            any.downcast::<internal::HostedRpcOwnerCell>()
357                .expect("HostedRpc owner downcast to HostedRpcOwnerCell failed")
358        }),
359        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
360            let stub: Stub = <T as internal::AsyncHostedRpcDep>::build_stub(channel);
361            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
362            boxed
363        }),
364    }
365}
366
367/// **Hidden macro-support helper.** Sync-runtime variant of
368/// [`__test_r_make_hosted_rpc_factory`].
369#[doc(hidden)]
370#[cfg(not(feature = "tokio"))]
371pub fn __test_r_make_hosted_rpc_factory<T, Stub>() -> internal::RpcFactory
372where
373    T: internal::HostedRpcDep<Stub = Stub>,
374    Stub: Send + Sync + 'static,
375{
376    use std::any::Any;
377    use std::sync::Arc;
378    internal::RpcFactory {
379        owner_into_cell: Arc::new(|any: Arc<dyn Any + Send + Sync>| {
380            any.downcast::<internal::HostedRpcOwnerCell>()
381                .expect("HostedRpc owner downcast to HostedRpcOwnerCell failed")
382        }),
383        build_stub: Arc::new(|channel: internal::HostedRpcChannel| {
384            let stub: Stub = <T as internal::HostedRpcDep>::build_stub(channel);
385            let boxed: Arc<dyn Any + Send + Sync> = Arc::new(stub);
386            boxed
387        }),
388    }
389}
390
391#[cfg(test)]
392mod hosted_helper_tests {
393    //! Exercise the feature-gated
394    //! [`__test_r_make_hosted_codec`] /
395    //! [`__test_r_make_hosted_worker_reconstructor`] helpers end to
396    //! end against a tiny `HostedDep` fixture.
397    //!
398    //! Both helper variants must reject `WorkerReconstructor::Sync`
399    //! vs `Async` choice at the cargo-feature level, so we keep this
400    //! test cfg-aware: it asserts the matching variant under each
401    //! feature.
402    use super::*;
403    use std::any::Any;
404    use std::sync::Arc;
405
406    /// Minimal sync `HostedDep` fixture. Under the `tokio` feature
407    /// the blanket bridge also makes it `AsyncHostedDep`, so the same fixture
408    /// is usable against both helper variants.
409    #[derive(Debug, PartialEq, Eq)]
410    struct Fixture {
411        bytes: Vec<u8>,
412    }
413
414    impl internal::HostedDep for Fixture {
415        fn descriptor(&self) -> Vec<u8> {
416            self.bytes.clone()
417        }
418        fn from_descriptor(bytes: &[u8]) -> Self {
419            Self {
420                bytes: bytes.to_vec(),
421            }
422        }
423    }
424
425    #[test]
426    fn make_hosted_codec_round_trips_descriptor_bytes() {
427        let codec = __test_r_make_hosted_codec::<Fixture>();
428        let owner: Arc<dyn Any + Send + Sync> = Arc::new(Fixture {
429            bytes: vec![1, 2, 3, 4],
430        });
431
432        let wire_bytes = (codec.to_wire)(owner);
433        assert_eq!(wire_bytes, vec![1, 2, 3, 4]);
434
435        let wire_payload = (codec.from_wire_bytes)(&wire_bytes);
436        let recovered_bytes: Arc<Vec<u8>> = wire_payload
437            .downcast::<Vec<u8>>()
438            .expect("from_wire_bytes must produce Arc<Vec<u8>>");
439        assert_eq!(*recovered_bytes, vec![1, 2, 3, 4]);
440    }
441
442    /// Under the tokio runtime, the worker reconstructor helper must
443    /// return [`internal::WorkerReconstructor::Async`].
444    #[cfg(feature = "tokio")]
445    #[test]
446    fn make_hosted_worker_reconstructor_is_async_under_tokio() {
447        let recon = __test_r_make_hosted_worker_reconstructor::<Fixture>();
448        match recon {
449            internal::WorkerReconstructor::Async(_) => {}
450            internal::WorkerReconstructor::Sync(_) => panic!(
451                "tokio build must produce a WorkerReconstructor::Async for Hosted deps; got Sync"
452            ),
453        }
454    }
455
456    /// Under the sync runtime, the worker reconstructor helper must
457    /// return [`internal::WorkerReconstructor::Sync`] so the sync
458    /// runner can drive it without any block-poll machinery.
459    #[cfg(not(feature = "tokio"))]
460    #[test]
461    fn make_hosted_worker_reconstructor_is_sync_under_sync_runtime() {
462        let recon = __test_r_make_hosted_worker_reconstructor::<Fixture>();
463        match recon {
464            internal::WorkerReconstructor::Sync(_) => {}
465            internal::WorkerReconstructor::Async(_) => panic!(
466                "sync build must produce a WorkerReconstructor::Sync for Hosted deps; got Async"
467            ),
468        }
469    }
470
471    /// Drive the sync-runtime reconstructor closure end to end on
472    /// the matching descriptor bytes the codec produces. Pinned only
473    /// for the sync build because the tokio build returns an Async
474    /// closure that needs a runtime to await.
475    #[cfg(not(feature = "tokio"))]
476    #[test]
477    fn sync_worker_reconstructor_rebuilds_fixture_from_descriptor() {
478        // Re-use a small `DependencyView` impl: the helper's worker
479        // closure ignores the view, so we pass an empty stub.
480        #[derive(Debug)]
481        struct EmptyView;
482        impl internal::DependencyView for EmptyView {
483            fn get(&self, _name: &str) -> Option<Arc<dyn Any + Send + Sync>> {
484                None
485            }
486        }
487
488        let codec = __test_r_make_hosted_codec::<Fixture>();
489        let recon = __test_r_make_hosted_worker_reconstructor::<Fixture>();
490
491        let owner: Arc<dyn Any + Send + Sync> = Arc::new(Fixture {
492            bytes: vec![5, 6, 7],
493        });
494        let wire_bytes = (codec.to_wire)(owner);
495        let payload = (codec.from_wire_bytes)(&wire_bytes);
496
497        let deps: Arc<dyn internal::DependencyView + Send + Sync> = Arc::new(EmptyView);
498        let rebuilt = match recon {
499            internal::WorkerReconstructor::Sync(f) => f(payload, deps),
500            internal::WorkerReconstructor::Async(_) => unreachable!(
501                "sync build cannot return Async; pinned by \
502                 make_hosted_worker_reconstructor_is_sync_under_sync_runtime",
503            ),
504        };
505        let rebuilt: Arc<Fixture> = rebuilt
506            .downcast::<Fixture>()
507            .expect("worker reconstructor must produce the original Hosted dep type");
508        assert_eq!(
509            *rebuilt,
510            Fixture {
511                bytes: vec![5, 6, 7]
512            }
513        );
514    }
515
516    // -----------------------------------------------------------------
517    // `worker = both(T)` helper tests.
518    //
519    // The macro lowering for `#[test_dep(scope = Hosted, worker =
520    // both(Trait))]` is exercised end-to-end by the
521    // `sharing::hosted_both_basic` example fixtures. These unit tests
522    // pin the three pieces of cargo-feature-aware glue in this file:
523    //
524    // - `__test_r_make_hosted_both_shared::<T>(owner)` builds the
525    //   shared cell that the macro's weak cache hands back to both
526    //   the Hosted and HostedRpc registrations.
527    // - `__test_r_make_hosted_both_codec()` serializes the cached
528    //   descriptor bytes for the Hosted view.
529    // - `__test_r_make_hosted_both_rpc_factory::<T>()` extracts the
530    //   inner `Arc<HostedRpcOwnerCell>` for the HostedRpc view and
531    //   builds the worker-side stub via `HostedRpcDep::build_stub`.
532    // -----------------------------------------------------------------
533
534    /// Minimal `HostedDep + HostedRpcDep` fixture for helper tests. The id
535    /// allocator stands in for any tiny control surface; the bytes field doubles
536    /// as the descriptor.
537    #[derive(Debug)]
538    struct BothFixture {
539        bytes: Vec<u8>,
540        counter: std::sync::Mutex<u64>,
541    }
542
543    impl BothFixture {
544        fn new(bytes: Vec<u8>) -> Self {
545            Self {
546                bytes,
547                counter: std::sync::Mutex::new(0),
548            }
549        }
550    }
551
552    impl internal::HostedDep for BothFixture {
553        fn descriptor(&self) -> Vec<u8> {
554            self.bytes.clone()
555        }
556        fn from_descriptor(bytes: &[u8]) -> Self {
557            Self::new(bytes.to_vec())
558        }
559    }
560
561    /// Stub view for the BothFixture. Holds a HostedRpcChannel so a
562    /// realistic build_stub round-trip is exercised; the tests below
563    /// just verify the factory hands back a usable `Arc<BothStub>`,
564    /// not a full IPC round-trip (covered by the example fixtures).
565    pub struct BothStub {
566        _channel: internal::HostedRpcChannel,
567    }
568
569    impl internal::HostedRpcDep for BothFixture {
570        type Stub = BothStub;
571        fn dispatch(&mut self, method_idx: u32, _args: &[u8]) -> Result<Vec<u8>, String> {
572            // Single method: bump the counter and return its value.
573            if method_idx == 1 {
574                let mut g = self.counter.lock().map_err(|e| e.to_string())?;
575                *g += 1;
576                Ok(g.to_be_bytes().to_vec())
577            } else {
578                Err(format!("BothFixture: unknown method_idx {method_idx}"))
579            }
580        }
581        fn build_stub(channel: internal::HostedRpcChannel) -> Self::Stub {
582            BothStub { _channel: channel }
583        }
584    }
585
586    /// `__test_r_make_hosted_both_shared(owner)` captures the
587    /// owner's descriptor bytes once and wraps the owner in a
588    /// `HostedRpcOwnerCell`. The descriptor must match the owner's
589    /// `HostedDep::descriptor()` output exactly.
590    #[test]
591    fn make_hosted_both_shared_captures_descriptor_bytes() {
592        let owner = BothFixture::new(vec![10, 20, 30]);
593        let shared = __test_r_make_hosted_both_shared::<BothFixture>(owner);
594        assert_eq!(shared.descriptor_bytes(), &[10, 20, 30]);
595        // The owner cell must be live: a dispatch call must succeed
596        // (the closure runs the method without panicking and returns
597        // a non-empty reply). Under the tokio feature the cell is the
598        // async variant — drive it through `dispatch_async` on a
599        // tokio runtime; under the sync feature the cell is sync.
600        let reply =
601            dispatch_cell_for_test(&shared.rpc_cell(), 1, &[]).expect("dispatch must succeed");
602        assert_eq!(reply, 1u64.to_be_bytes().to_vec());
603    }
604
605    /// Helper: dispatch on a `HostedRpcOwnerCell` whose async/sync
606    /// variant depends on the active cargo feature. Used by the
607    /// `worker = both(T)` helper tests so the test bodies don't need
608    /// to know whether the cell was built via `from_owner` or
609    /// `from_async_owner` — both surface the same per-call result.
610    fn dispatch_cell_for_test(
611        cell: &internal::HostedRpcOwnerCell,
612        method_idx: u32,
613        args: &[u8],
614    ) -> Result<Vec<u8>, String> {
615        #[cfg(feature = "tokio")]
616        {
617            // `tokio` resolves to the local `mod tokio` inside this
618            // crate; reach the external crate with `::tokio`.
619            let rt = ::tokio::runtime::Builder::new_multi_thread()
620                .enable_all()
621                .build()
622                .expect("build tokio runtime");
623            rt.block_on(cell.dispatch_async(method_idx, args))
624        }
625        #[cfg(not(feature = "tokio"))]
626        {
627            cell.dispatch(method_idx, args)
628        }
629    }
630
631    /// `__test_r_make_hosted_both_codec().to_wire` downcasts the
632    /// shared cell and returns its captured descriptor bytes — the
633    /// same shape the worker-side reconstructor expects.
634    #[test]
635    fn make_hosted_both_codec_serializes_descriptor_bytes() {
636        let codec = __test_r_make_hosted_both_codec();
637        let shared: Arc<internal::HostedBothShared> =
638            Arc::new(__test_r_make_hosted_both_shared::<BothFixture>(
639                BothFixture::new(vec![1, 2, 3, 4]),
640            ));
641        let arc_any: Arc<dyn Any + Send + Sync> = shared;
642
643        let wire_bytes = (codec.to_wire)(arc_any);
644        assert_eq!(wire_bytes, vec![1, 2, 3, 4]);
645
646        // `from_wire_bytes` must produce the same `Arc<Vec<u8>>`
647        // payload shape the existing Hosted reconstructor consumes —
648        // otherwise the worker side would not be able to reuse the
649        // standard `__test_r_make_hosted_worker_reconstructor`.
650        let wire_payload = (codec.from_wire_bytes)(&wire_bytes);
651        let recovered_bytes: Arc<Vec<u8>> = wire_payload
652            .downcast::<Vec<u8>>()
653            .expect("from_wire_bytes must produce Arc<Vec<u8>>");
654        assert_eq!(*recovered_bytes, vec![1, 2, 3, 4]);
655    }
656
657    /// `__test_r_make_hosted_both_rpc_factory::<T, Stub>().owner_into_cell`
658    /// reaches into the shared cell, hands back the inner
659    /// `Arc<HostedRpcOwnerCell>`, and a dispatched call hits the
660    /// real owner (proven by the counter incrementing).
661    #[test]
662    fn make_hosted_both_rpc_factory_extracts_owner_cell() {
663        let factory = __test_r_make_hosted_both_rpc_factory::<BothFixture, BothStub>();
664        let shared: Arc<internal::HostedBothShared> = Arc::new(__test_r_make_hosted_both_shared::<
665            BothFixture,
666        >(BothFixture::new(vec![])));
667        let arc_any: Arc<dyn Any + Send + Sync> = shared.clone();
668
669        let cell = (factory.owner_into_cell)(arc_any);
670        assert!(
671            Arc::ptr_eq(&cell, &shared.rpc_cell()),
672            "factory must return the exact same inner HostedRpcOwnerCell Arc the \
673             shared cell holds; otherwise the RPC view would dispatch against a \
674             different owner than the descriptor view captured"
675        );
676
677        // The cell is functional: dispatch routes to the real owner
678        // method (counter starts at 0; first call must yield 1). Same
679        // cargo-feature-aware dispatch as
680        // `make_hosted_both_shared_captures_descriptor_bytes` since the
681        // underlying cell shares the same async/sync split.
682        let reply = dispatch_cell_for_test(&cell, 1, &[]).expect("dispatch must succeed");
683        assert_eq!(reply, 1u64.to_be_bytes().to_vec());
684    }
685
686    /// `__test_r_make_hosted_both_rpc_factory::<T, Stub>().build_stub`
687    /// constructs a worker-side `Stub` via the user's
688    /// `HostedRpcDep::build_stub` and boxes it as `Arc<dyn Any>` so
689    /// the runtime can route it through the standard dep view.
690    #[test]
691    fn make_hosted_both_rpc_factory_builds_stub() {
692        use internal::{HostedRpcChannel, HostedRpcError, HostedRpcTransport};
693
694        // Minimal in-process transport stand-in: every call returns
695        // the unit reply. We don't actually call into it; the test
696        // just exercises that build_stub produces a downcastable
697        // `Arc<dyn Any>` carrying the channel.
698        struct DummyTransport;
699        impl HostedRpcTransport for DummyTransport {
700            fn call(
701                &self,
702                _dep_id: &str,
703                _method_idx: u32,
704                _args: Vec<u8>,
705            ) -> Result<Vec<u8>, HostedRpcError> {
706                Ok(Vec::new())
707            }
708        }
709
710        let factory = __test_r_make_hosted_both_rpc_factory::<BothFixture, BothStub>();
711        let transport: Arc<dyn HostedRpcTransport> = Arc::new(DummyTransport);
712        let channel = HostedRpcChannel::new("test::both_fixture".to_string(), transport);
713
714        let stub_any: Arc<dyn Any + Send + Sync> = (factory.build_stub)(channel);
715        let _stub: Arc<BothStub> = stub_any
716            .downcast::<BothStub>()
717            .expect("build_stub must produce the BothFixture::Stub type");
718    }
719}