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}