//! Test-only utilities shared by unit tests across the crate. //! //! Background (issue #23): [`crate::device::Device`] is the entire N64 machine //! state — cpu, pif, cart, memory, rsp, rdp, rdram, mi, pi, vi, ai, si, ri, //! vru, transfer paks, cheats — 307968 bytes (~301 KiB) in a debug build. //! `Device::new()` materialises the struct literal and its nested field //! literals on the stack before moving them into the box, and //! `postcard::from_bytes::` rebuilds the whole value on the stack — //! so a netplay-savestate round trip (construct, serialize, restore, clone) //! peaks at several times that size. libtest runs each test on a spawned //! thread whose default stack is 2 MiB (not the 8 MiB main-thread stack), so //! such a test aborts the whole test binary with //! `fatal runtime error: stack overflow` — taking every other test in the run //! down with it. //! //! [`with_big_stack`] runs a test body on an explicitly sized thread so tests //! are independent of the ambient stack, on any runner, with no env var. /// Run `f` on a thread with an 8 MiB stack — the same budget the project /// already standardises on for the main thread (`.cargo/config.toml` ships /// `/STACK:8388608` on Windows). Panics inside `f` are propagated to the /// caller, so a failing test still fails normally. pub(crate) fn with_big_stack(f: F) { std::thread::Builder::new() .stack_size(8 * 1024 * 1024) .spawn(f) .unwrap() .join() .unwrap(); } #[cfg(test)] mod tests { use super::with_big_stack; use crate::device::Device; /// Issue #23 acceptance-criterion 5 tripwire: if `Device` ever grows past /// ~700 KiB the 8 MiB test-stack budget chosen here has to be revisited. const MAX_DEVICE_SIZE: usize = 700 * 1024; /// Regression test for issue #23: a netplay-savestate-style round trip — /// construct a `Device`, serialize it, rebuild it from bytes (postcard /// deserialises the ~301 KiB struct on the stack), clone its state back — /// needs more than the 2 MiB libtest thread stack in a debug build. /// /// Measured with this exact body unwrapped on the ambient libtest thread: /// `thread '...' has overflowed its stack` / `fatal runtime error: stack /// overflow, aborting` (SIGABRT); the same body passes with an 8 MiB /// stack. This body is therefore run on an 8 MiB thread; if the wrapping /// is ever removed the test binary aborts instead of reporting a failure, /// so the mitigation cannot silently regress once step 3 of the gate has /// real content. #[test] fn device_round_trip_fits_under_the_8_mib_test_stack_budget() { with_big_stack(|| { let device = Device::new(false); let bytes = postcard::to_stdvec(&device).unwrap(); let mut restored: Device = postcard::from_bytes(&bytes).unwrap(); restored.clone_state(&device); // Keep both devices alive so nothing is dropped early and the // peak stack use is actually measured. std::hint::black_box((device, restored)); }); assert!( std::mem::size_of::() < MAX_DEVICE_SIZE, "Device is now {} bytes; the 8 MiB test-stack budget in with_big_stack must be revisited", std::mem::size_of::() ); } }