//! select tests. Beyond the functional cases, the *_stays_precise tests are //! soundness probes for the consuming-wake protocol: they fire stale loser- //! arm wakes at an actor and then run a one-shot park (`sleep`, whose early //! return would be observable as a short elapsed time) to prove the stale //! wake died at its epoch CAS instead of corrupting the next wait. use smarm::{channel, run, select, spawn}; use std::sync::atomic::{AtomicI64, Ordering}; use std::time::{Duration, Instant}; static OUT: AtomicI64 = AtomicI64::new(0); #[test] fn ready_arm_returns_immediately_without_parking() { OUT.store(0, Ordering::SeqCst); run(|| { let (txa, rxa) = channel::(); let (_txb, rxb) = channel::(); txa.send(42).unwrap(); let i = select(&[&rxb, &rxa]); assert_eq!(i, 1); OUT.store(rxa.try_recv().unwrap().expect("ready arm must hold a message"), Ordering::SeqCst); }); assert_eq!(OUT.load(Ordering::SeqCst), 42); } #[test] fn lower_index_wins_when_several_arms_are_ready() { run(|| { let (txa, rxa) = channel::(); let (txb, rxb) = channel::(); txa.send(1).unwrap(); txb.send(2).unwrap(); // Documented priority order: index 0 first, regardless of send order. assert_eq!(select(&[&rxa, &rxb]), 0); assert_eq!(select(&[&rxb, &rxa]), 0); }); } #[test] fn parks_until_any_arm_fires() { OUT.store(0, Ordering::SeqCst); run(|| { let (txa, _rxa_keepalive) = (channel::().0, ()); let _hold = txa; // arm a: sender alive, never sends let (txa, rxa) = channel::(); let (txb, rxb) = channel::(); let _keep_a_open = txa; let h = spawn(move || { smarm::sleep(Duration::from_millis(10)); txb.send(7).unwrap(); }); let i = select(&[&rxa, &rxb]); assert_eq!(i, 1); OUT.store(rxb.try_recv().unwrap().unwrap(), Ordering::SeqCst); h.join().unwrap(); }); assert_eq!(OUT.load(Ordering::SeqCst), 7); } #[test] fn closed_arm_counts_as_ready() { run(|| { let (_keep, rxa) = channel::(); let (txb, rxb) = channel::(); drop(txb); let i = select(&[&rxa, &rxb]); assert_eq!(i, 1); // The caller observes the closure on the arm itself. assert!(rxb.try_recv().is_err()); }); } #[test] fn closure_while_parked_wakes_the_select() { run(|| { let (_keep, rxa) = channel::(); let (txb, rxb) = channel::(); let h = spawn(move || { smarm::sleep(Duration::from_millis(10)); drop(txb); }); assert_eq!(select(&[&rxa, &rxb]), 1); assert!(rxb.try_recv().is_err()); h.join().unwrap(); }); } #[test] fn loser_arm_wake_after_parked_select_stays_precise() { // Both arms registered; arm 0 wins the wake; arm 1's sender then fires // a wake stamped with the consumed epoch. The subsequent sleep is a // one-shot park: an early return would mean the stale wake landed. run(|| { let (txa, rxa) = channel::(); let (txb, rxb) = channel::(); let h = spawn(move || { smarm::sleep(Duration::from_millis(10)); txa.send(1).unwrap(); // wins txb.send(2).unwrap(); // loser arm: stale-epoch wake }); assert_eq!(select(&[&rxa, &rxb]), 0); assert_eq!(rxa.try_recv().unwrap(), Some(1)); let t0 = Instant::now(); smarm::sleep(Duration::from_millis(40)); assert!( t0.elapsed() >= Duration::from_millis(40), "one-shot park returned early: a stale loser-arm wake landed" ); // The loser's message was never lost. assert_eq!(select(&[&rxa, &rxb]), 1); assert_eq!(rxb.try_recv().unwrap(), Some(2)); h.join().unwrap(); }); } #[test] fn no_park_exit_retires_the_wait() { // Arm 1 is ready at registration time, so select returns WITHOUT // parking while arm 0 holds a live-epoch registration. Arm 0's sender // then fires. retire_wait must have invalidated/eaten that wake; // the sleep proves it. run(|| { let (txa, rxa) = channel::(); let (txb, rxb) = channel::(); txb.send(2).unwrap(); assert_eq!(select(&[&rxa, &rxb]), 1); assert_eq!(rxb.try_recv().unwrap(), Some(2)); let h = spawn(move || { txa.send(1).unwrap(); // fires at the (retired) select epoch }); h.join().unwrap(); let t0 = Instant::now(); smarm::sleep(Duration::from_millis(40)); assert!( t0.elapsed() >= Duration::from_millis(40), "one-shot park returned early: the no-park exit leaked a wake" ); assert_eq!(rxa.try_recv().unwrap(), Some(1)); }); } #[test] fn select_then_plain_recv_on_a_loser_arm() { // A leftover own-registration from a select must not trip the next // direct wait on that channel (it is overwritten, not asserted away). run(|| { let (txa, rxa) = channel::(); let (txb, rxb) = channel::(); txa.send(1).unwrap(); assert_eq!(select(&[&rxa, &rxb]), 0); // rxb may keep a registration assert_eq!(rxa.try_recv().unwrap(), Some(1)); let h = spawn(move || { smarm::sleep(Duration::from_millis(5)); txb.send(9).unwrap(); }); assert_eq!(rxb.recv().unwrap(), 9); h.join().unwrap(); }); } #[test] fn select_loop_drains_two_producers_completely() { const N: i64 = 200; OUT.store(0, Ordering::SeqCst); run(|| { let (txa, rxa) = channel::(); let (txb, rxb) = channel::(); let ha = spawn(move || { for v in 0..N { txa.send(v).unwrap(); if v % 7 == 0 { smarm::yield_now(); } } }); let hb = spawn(move || { for v in 0..N { txb.send(v).unwrap(); if v % 5 == 0 { smarm::yield_now(); } } }); // A closed arm stays permanently ready, so it must leave the arm // set once observed (keeping it would starve the other arm at // priority order) — select until the FIRST closure, then drain the // survivor with plain recv. let mut sum = 0i64; let survivor: &smarm::Receiver = loop { let i = select(&[&rxa, &rxb]); let arm: &smarm::Receiver = if i == 0 { &rxa } else { &rxb }; match arm.try_recv() { Ok(Some(v)) => sum += v, Ok(None) => {} // defensive: ready arm already drained Err(_) => break if i == 0 { &rxb } else { &rxa }, } }; loop { match survivor.recv() { Ok(v) => sum += v, Err(_) => break, } } OUT.store(sum, Ordering::SeqCst); ha.join().unwrap(); hb.join().unwrap(); }); assert_eq!(OUT.load(Ordering::SeqCst), 2 * (0..200i64).sum::()); } #[test] fn motivating_pattern_inbox_plus_monitor_down() { // The gen_server/handle_info shape select unlocks: a server waiting on // its request inbox AND a monitor Down channel in one park. use smarm::monitor; run(|| { let (req_tx, req_rx) = channel::(); let worker = spawn(|| { smarm::sleep(Duration::from_millis(10)); // worker exits -> Down fires }); let m = monitor(worker.pid()); let _keep_inbox_open = req_tx; let mut served = 0; loop { match select(&[&m.rx, &req_rx]) { 0 => { let _down = m.rx.try_recv().unwrap().expect("Down message"); break; } _ => { if let Ok(Some(_)) = req_rx.try_recv() { served += 1; } } } } assert_eq!(served, 0); worker.join().unwrap(); }); }