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}