//! RFC 019 commit 2 — the `SpawnOpts` surface. //! //! Covers: per-spawn stack shape overrides on every spawn surface, the //! `None ⇒ Config default` resolution, the pool rule from the outside //! (obligation 4: a custom-shaped stack never enters the pool), and that a //! big reserve behaviorally takes effect (deep recursion completes). use smarm::runtime::{Config, DEFAULT_STACK_GUARD, DEFAULT_STACK_RESERVE}; use smarm::{self_pid, spawn, spawn_under_with, spawn_with, GenServerBuilder, SpawnOpts}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; fn rt1() -> smarm::runtime::Runtime { smarm::runtime::init(Config::exact(1)) } #[test] fn default_spawn_has_default_shape() { rt1().run(|| { let h = spawn(|| { let shape = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD)); }); h.join().unwrap(); }); } #[test] fn spawn_with_overrides_reserve_and_guard() { rt1().run(|| { let opts = SpawnOpts { stack_reserve: Some(1024 * 1024), guard_size: Some(256 * 1024), }; let h = spawn_with(opts, || { let shape = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(shape, (1024 * 1024, 256 * 1024)); }); h.join().unwrap(); }); } #[test] fn spawn_with_partial_override_keeps_config_default_for_the_rest() { rt1().run(|| { let opts = SpawnOpts { stack_reserve: Some(1024 * 1024), ..SpawnOpts::default() }; let h = spawn_with(opts, || { let shape = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(shape, (1024 * 1024, DEFAULT_STACK_GUARD)); }); h.join().unwrap(); }); } #[test] fn spawn_with_rounds_to_pages() { rt1().run(|| { let opts = SpawnOpts { stack_reserve: Some(64 * 1024 + 1), guard_size: Some(4097), }; let h = spawn_with(opts, || { let (reserve, guard) = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(reserve % 4096, 0); assert_eq!(guard % 4096, 0); assert!(reserve >= 64 * 1024 + 1); assert!(guard >= 4097); }); h.join().unwrap(); }); } #[test] fn spawn_under_with_takes_opts() { rt1().run(|| { let me = self_pid(); let opts = SpawnOpts { stack_reserve: Some(128 * 1024), ..SpawnOpts::default() }; let h = spawn_under_with(me, opts, || { let (reserve, _) = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(reserve, 128 * 1024); }); h.join().unwrap(); }); } /// Obligation 4, from the outside: a dead custom stack must not be handed to /// the next default spawn. The pool is LIFO, so if the custom stack had been /// (wrongly) pushed at death, the very next default-shaped spawn on this /// single-threaded runtime would pop it and report a custom shape. #[test] fn custom_stack_never_enters_the_pool() { rt1().run(|| { spawn_with( SpawnOpts { stack_reserve: Some(512 * 1024), guard_size: Some(128 * 1024), }, || {}, ) .join() .unwrap(); let h = spawn(|| { let shape = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD)); }); h.join().unwrap(); }); } /// The reverse direction of the pool rule: a default-shaped stack IS pooled /// and reused (cap = threads × 4 ≥ 1 here, pool empty at start). #[test] fn default_stack_is_recycled() { rt1().run(|| { spawn(|| {}).join().unwrap(); let h = spawn(|| { let shape = smarm::introspect::stack_shape(self_pid()).unwrap(); assert_eq!(shape, (DEFAULT_STACK_RESERVE, DEFAULT_STACK_GUARD)); }); h.join().unwrap(); }); } /// Burn ~`frames` × 4 KiB of stack (see tests/runtime.rs twin). #[inline(never)] fn burn_stack(frames: usize) -> u64 { let mut local = [0u8; 4096]; local[0] = frames as u8; let below = if frames == 0 { 0 } else { burn_stack(frames - 1) }; std::hint::black_box(&mut local); below.wrapping_add(local[0] as u64) } #[test] fn big_reserve_behaviorally_takes_effect() { // ~1 MiB deep on an 8 MiB per-spawn reserve, runtime default untouched. rt1().run(|| { let done = Arc::new(AtomicBool::new(false)); let done2 = done.clone(); spawn_with( SpawnOpts { stack_reserve: Some(8 * 1024 * 1024), ..SpawnOpts::default() }, move || { std::hint::black_box(burn_stack(256)); done2.store(true, Ordering::SeqCst); }, ) .join() .unwrap(); assert!(done.load(Ordering::SeqCst)); }); } // --------------------------------------------------------------------------- // Builder surfaces // --------------------------------------------------------------------------- struct Echo; impl smarm::GenServer for Echo { type Call = (); type Reply = (usize, usize); type Cast = (); type Info = (); type Timer = (); fn handle_call(&mut self, _c: ()) -> (usize, usize) { smarm::introspect::stack_shape(self_pid()).unwrap() } fn handle_cast(&mut self, _c: ()) {} } #[test] fn gen_server_builder_stack_opts() { rt1().run(|| { let server = GenServerBuilder::new(Echo) .stack_opts(SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() }) .start(); let (reserve, guard) = server.call(()).unwrap(); assert_eq!(reserve, 256 * 1024); assert_eq!(guard, DEFAULT_STACK_GUARD); server.shutdown(); }); } struct Probe; impl smarm::Machine for Probe { type Ev = smarm::channel::Sender<(usize, usize)>; fn state_timeout_ev() -> Self::Ev { unreachable!("no timers in this test") } fn timeout_ev(_name: &'static str) -> Self::Ev { unreachable!("no timers in this test") } fn on_start(&mut self, _cx: &mut smarm::Cx) {} fn handle( &mut self, ev: Self::Ev, _cx: &mut smarm::Cx, ) -> smarm::gen_statem::Step { let _ = ev.send(smarm::introspect::stack_shape(self_pid()).unwrap()); smarm::gen_statem::Step::Stayed } } #[test] fn gen_statem_spawn_with_stack_opts() { rt1().run(|| { let m = smarm::gen_statem::spawn_with( SpawnOpts { stack_reserve: Some(256 * 1024), ..SpawnOpts::default() }, Probe, ); let (tx, rx) = smarm::channel::channel(); m.send(tx).unwrap(); let (reserve, guard) = rx.recv().unwrap(); assert_eq!(reserve, 256 * 1024); assert_eq!(guard, DEFAULT_STACK_GUARD); }); }