feat: add library lifecycle utility in omrf
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
//! Integration tests for the `library_lifecycle` module.
|
||||
|
||||
use sarasacw_omrf::library_lifecycle::LibraryLifecycle;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
#[derive(Debug, TeError)]
|
||||
enum TestErr {
|
||||
#[error("boom")]
|
||||
Boom,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn paired_startup_shutdown() {
|
||||
let inits = AtomicUsize::new(0);
|
||||
let destroys = AtomicUsize::new(0);
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
|
||||
lc.startup(|| -> Result<State, TestErr> {
|
||||
inits.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(State { value: 42 })
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
lc.shutdown(|s| {
|
||||
destroys.fetch_add(1, Ordering::SeqCst);
|
||||
assert_eq!(s.value, 42);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(inits.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(destroys.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(lc.count(), 0);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn multiple_startups_shutdowns_run_only_on_boundaries() {
|
||||
let inits = AtomicUsize::new(0);
|
||||
let destroys = AtomicUsize::new(0);
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
|
||||
for _ in 0..3 {
|
||||
lc.startup(|| -> Result<State, TestErr> {
|
||||
inits.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(State { value: 7 })
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
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);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(inits.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(destroys.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(lc.count(), 0);
|
||||
}
|
||||
|
||||
/// Coverage: after a full cycle, a new cycle re-runs init and destroy (re-initialization works).
|
||||
#[test]
|
||||
fn reinitialization_runs_init_and_destroy_again() {
|
||||
let inits = AtomicUsize::new(0);
|
||||
let destroys = AtomicUsize::new(0);
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
|
||||
for _ in 0..2 {
|
||||
lc.startup(|| -> Result<State, TestErr> {
|
||||
inits.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(State { value: 1 })
|
||||
})
|
||||
.unwrap();
|
||||
lc.shutdown(|_s| {
|
||||
destroys.fetch_add(1, Ordering::SeqCst);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(inits.load(Ordering::SeqCst), 2);
|
||||
assert_eq!(destroys.load(Ordering::SeqCst), 2);
|
||||
assert_eq!(lc.count(), 0);
|
||||
}
|
||||
|
||||
/// Coverage: `shutdown` with the count already at zero is an error (unbalanced call).
|
||||
#[test]
|
||||
fn shutdown_when_idle_is_error() {
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
assert!(lc.shutdown(|_| ()).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `with_state` before any `startup` (and after the matching `shutdown`) is an error
|
||||
/// because there is no active state.
|
||||
#[test]
|
||||
fn with_state_when_not_active_is_error() {
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
assert!(lc.with_state(|_s| ()).is_err());
|
||||
|
||||
lc.startup(|| -> Result<State, TestErr> { Ok(State { value: 5 }) })
|
||||
.unwrap();
|
||||
lc.shutdown(|_| ()).unwrap();
|
||||
assert!(lc.with_state(|_s| ()).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: a failing init closure leaves the count at zero and the state absent, and the error is
|
||||
/// propagated (type-erased into `Error::Init`).
|
||||
#[test]
|
||||
fn init_failure_keeps_count_zero_and_propagates_error() {
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
let r = lc.startup(|| -> Result<State, TestErr> { Err(TestErr::Boom) });
|
||||
assert!(r.is_err());
|
||||
assert_eq!(lc.count(), 0);
|
||||
// State is absent, so accessing it errors.
|
||||
assert!(lc.with_state(|_s| ()).is_err());
|
||||
}
|
||||
|
||||
/// Coverage: `with_state` reads the stored state during an active lifecycle.
|
||||
#[test]
|
||||
fn with_state_reads_active_state() {
|
||||
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
||||
lc.startup(|| -> Result<State, TestErr> { Ok(State { value: 99 }) })
|
||||
.unwrap();
|
||||
|
||||
let observed = lc.with_state(|s| s.value).unwrap();
|
||||
assert_eq!(observed, 99);
|
||||
|
||||
lc.shutdown(|_| ()).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user