diff --git a/omrf/src/library_lifecycle.rs b/omrf/src/library_lifecycle.rs index a69c979..13f69a7 100644 --- a/omrf/src/library_lifecycle.rs +++ b/omrf/src/library_lifecycle.rs @@ -82,15 +82,16 @@ impl LibraryLifecycle { /// Increment the reference count. /// - /// On the `0 -> 1` transition the supplied `init` closure runs and its produced `S` is stored. - /// If `init` fails, neither the count nor the state is changed and an error is returned. + /// On the `0 -> 1` transition the supplied `init` closure runs and its produced `S` is stored; + /// the call returns `Ok(true)`. If `init` fails, neither the count nor the state is changed and + /// an error is returned. /// - /// On any other call (count already `> 0`) the supplied `init` is **dropped without being run** - /// and the count is simply incremented. + /// On any other call (count already `> 0`) the supplied `init` is **dropped without being run**, + /// the count is simply incremented, and the call returns `Ok(false)`. /// /// The init closure runs under the write lock and must not re-enter this lifecycle. Its error /// type `E` is type-erased. - pub fn startup(&self, init: impl FnOnce() -> Result) -> Result<(), Error> + pub fn startup(&self, init: impl FnOnce() -> Result) -> Result where E: std::error::Error + Send + Sync + 'static, { @@ -99,22 +100,24 @@ impl LibraryLifecycle { let s = init().map_err(|e| Error::Init(Box::new(e)))?; inner.state = Some(s); inner.count = 1; + Ok(true) } else { inner.count += 1; + Ok(false) } - Ok(()) } /// Decrement the reference count. /// /// On the `-> 0` transition the stored `S` is taken out and handed to the supplied `destroy` - /// closure, which must never fail (destructor semantics). On any other call (count stays `> 0`) - /// the supplied `destroy` is **dropped without being run** and the count is simply decremented. + /// closure, which must never fail (destructor semantics), and the call returns `Ok(true)`. On + /// any other call (count stays `> 0`) the supplied `destroy` is **dropped without being run**, + /// the count is simply decremented, and the call returns `Ok(false)`. /// /// Returns an error if the count was already zero. /// /// The destroy closure runs under the write lock and must not re-enter this lifecycle. - pub fn shutdown(&self, destroy: impl FnOnce(S)) -> Result<(), Error> { + pub fn shutdown(&self, destroy: impl FnOnce(S)) -> Result { let mut inner = self.inner.write().expect("unexpected poison lock"); if inner.count == 0 { return Err(Error::Underflow); @@ -124,8 +127,10 @@ impl LibraryLifecycle { // Invariant: state is Some whenever count > 0. let s = inner.state.take().expect("state present while count > 0"); destroy(s); + Ok(true) + } else { + Ok(false) } - Ok(()) } /// Access the stored state by shared reference under a read lock. diff --git a/omrf/tests/library_lifecycle.rs b/omrf/tests/library_lifecycle.rs index ea51df5..4db03be 100644 --- a/omrf/tests/library_lifecycle.rs +++ b/omrf/tests/library_lifecycle.rs @@ -16,24 +16,28 @@ struct State { } /// Coverage: a single paired startup/shutdown runs init once and destroy once, destroy receives the -/// state produced by init, and the count returns to zero. +/// state produced by init, both report that they ran the closure, and the count returns to zero. #[test] fn paired_startup_shutdown() { let inits = AtomicUsize::new(0); let destroys = AtomicUsize::new(0); let lc: LibraryLifecycle = LibraryLifecycle::new(); - lc.startup(|| -> Result { - inits.fetch_add(1, Ordering::SeqCst); - Ok(State { value: 42 }) - }) - .unwrap(); + let ran_init = lc + .startup(|| -> Result { + inits.fetch_add(1, Ordering::SeqCst); + Ok(State { value: 42 }) + }) + .unwrap(); + assert!(ran_init); - lc.shutdown(|s| { - destroys.fetch_add(1, Ordering::SeqCst); - assert_eq!(s.value, 42); - }) - .unwrap(); + let ran_destroy = lc + .shutdown(|s| { + destroys.fetch_add(1, Ordering::SeqCst); + assert_eq!(s.value, 42); + }) + .unwrap(); + assert!(ran_destroy); assert_eq!(inits.load(Ordering::SeqCst), 1); assert_eq!(destroys.load(Ordering::SeqCst), 1); @@ -42,29 +46,35 @@ fn paired_startup_shutdown() { /// Coverage: multiple startups followed by matching shutdowns run init and destroy only on the /// boundary transitions (once each); the init/destroy closures passed on non-transition calls are -/// dropped without being run. +/// dropped without being run, and the bool return reflects exactly which calls ran a closure. #[test] fn multiple_startups_shutdowns_run_only_on_boundaries() { let inits = AtomicUsize::new(0); let destroys = AtomicUsize::new(0); let lc: LibraryLifecycle = LibraryLifecycle::new(); - for _ in 0..3 { - lc.startup(|| -> Result { - inits.fetch_add(1, Ordering::SeqCst); - Ok(State { value: 7 }) + let startup_ran: Vec = (0..3) + .map(|_| { + lc.startup(|| -> Result { + inits.fetch_add(1, Ordering::SeqCst); + Ok(State { value: 7 }) + }) + .unwrap() }) - .unwrap(); - } + .collect(); + assert_eq!(startup_ran, vec![true, false, false]); assert_eq!(lc.count(), 3); assert_eq!(inits.load(Ordering::SeqCst), 1); - for _ in 0..3 { - lc.shutdown(|_s| { - destroys.fetch_add(1, Ordering::SeqCst); + let shutdown_ran: Vec = (0..3) + .map(|_| { + lc.shutdown(|_s| { + destroys.fetch_add(1, Ordering::SeqCst); + }) + .unwrap() }) - .unwrap(); - } + .collect(); + assert_eq!(shutdown_ran, vec![false, false, true]); assert_eq!(inits.load(Ordering::SeqCst), 1); assert_eq!(destroys.load(Ordering::SeqCst), 1); @@ -79,15 +89,19 @@ fn reinitialization_runs_init_and_destroy_again() { let lc: LibraryLifecycle = LibraryLifecycle::new(); for _ in 0..2 { - lc.startup(|| -> Result { - inits.fetch_add(1, Ordering::SeqCst); - Ok(State { value: 1 }) - }) - .unwrap(); - lc.shutdown(|_s| { - destroys.fetch_add(1, Ordering::SeqCst); - }) - .unwrap(); + let ran_init = lc + .startup(|| -> Result { + inits.fetch_add(1, Ordering::SeqCst); + Ok(State { value: 1 }) + }) + .unwrap(); + assert!(ran_init); + let ran_destroy = lc + .shutdown(|_s| { + destroys.fetch_add(1, Ordering::SeqCst); + }) + .unwrap(); + assert!(ran_destroy); } assert_eq!(inits.load(Ordering::SeqCst), 2);