//! gen_server tests: call round-trip, cast, lifecycle callbacks, and the two //! server-down detection paths (reply-channel close vs. inbox-send failure). use smarm::gen_server::{start, CallError, GenServer}; use smarm::run; use std::sync::{Arc, Mutex}; // --------------------------------------------------------------------------- // A trivial counter server: casts mutate, calls read (or blow up). // --------------------------------------------------------------------------- struct Counter { n: i64, } enum Req { Get, Boom, } enum Op { Add(i64), } impl GenServer for Counter { type Call = Req; type Reply = i64; type Cast = Op; fn handle_call(&mut self, req: Req) -> i64 { match req { Req::Get => self.n, Req::Boom => panic!("boom"), } } fn handle_cast(&mut self, op: Op) { match op { Op::Add(x) => self.n += x, } } } // Casts are applied in order and a later call observes the accumulated state. #[test] fn cast_then_call_roundtrip() { let got = Arc::new(Mutex::new(0i64)); let got2 = got.clone(); run(move || { let server = start(Counter { n: 0 }); server.cast(Op::Add(5)).unwrap(); server.cast(Op::Add(3)).unwrap(); let n = server.call(Req::Get).unwrap(); *got2.lock().unwrap() = n; }); assert_eq!(*got.lock().unwrap(), 8); } // --------------------------------------------------------------------------- // Lifecycle: init runs before the first message, terminate on graceful exit. // --------------------------------------------------------------------------- struct Lifecycle { log: Arc>>, } impl GenServer for Lifecycle { type Call = (); type Reply = (); type Cast = (); fn init(&mut self) { self.log.lock().unwrap().push("init"); } fn handle_call(&mut self, _req: ()) { self.log.lock().unwrap().push("call"); } fn handle_cast(&mut self, _req: ()) {} fn terminate(&mut self) { self.log.lock().unwrap().push("terminate"); } } // init -> handle_call -> (drop last ref closes inbox) -> terminate. #[test] fn init_and_terminate_run() { let log = Arc::new(Mutex::new(Vec::new())); let log2 = log.clone(); run(move || { let server = start(Lifecycle { log: log2 }); server.call(()).unwrap(); // Dropping the only ref closes the inbox; the server breaks out of its // recv loop and runs terminate. run() will not return until it has. drop(server); }); assert_eq!(*log.lock().unwrap(), vec!["init", "call", "terminate"]); } // --------------------------------------------------------------------------- // Server-down detection. // --------------------------------------------------------------------------- // The server dies *while* a call is in flight (handler panics): the reply // sender drops on unwind, closing the reply channel, so the parked caller's // recv returns Err -> ServerDown. #[test] fn call_to_panicking_handler_is_server_down() { let got = Arc::new(Mutex::new(None)); let got2 = got.clone(); run(move || { let server = start(Counter { n: 0 }); let r = server.call(Req::Boom); *got2.lock().unwrap() = Some(r); }); assert_eq!(*got.lock().unwrap(), Some(Err(CallError::ServerDown))); } // A call issued *after* the server is already gone: the inbox is closed, so the // send itself fails -> ServerDown (the other detection path). #[test] fn call_after_server_gone_is_server_down() { let got = Arc::new(Mutex::new(None)); let got2 = got.clone(); run(move || { let server = start(Counter { n: 0 }); let server2 = server.clone(); // This kills the server (and is itself ServerDown). assert_eq!(server.call(Req::Boom), Err(CallError::ServerDown)); // Inbox now closed; a fresh call can't even be enqueued. let r = server2.call(Req::Get); *got2.lock().unwrap() = Some(r); }); assert_eq!(*got.lock().unwrap(), Some(Err(CallError::ServerDown))); }