2026-08-10 19:35:10 +08:00
|
|
|
//! 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
|
2026-08-10 22:17:19 +08:00
|
|
|
/// state produced by init, both report that they ran the closure, and the count returns to zero.
|
2026-08-10 19:35:10 +08:00
|
|
|
#[test]
|
|
|
|
|
fn paired_startup_shutdown() {
|
|
|
|
|
let inits = AtomicUsize::new(0);
|
|
|
|
|
let destroys = AtomicUsize::new(0);
|
|
|
|
|
let lc: LibraryLifecycle<State> = LibraryLifecycle::new();
|
|
|
|
|
|
2026-08-10 22:17:19 +08:00
|
|
|
let ran_init = lc
|
|
|
|
|
.startup(|| -> Result<State, TestErr> {
|
|
|
|
|
inits.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
Ok(State { value: 42 })
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(ran_init);
|
2026-08-10 19:35:10 +08:00
|
|
|
|
2026-08-10 22:17:19 +08:00
|
|
|
let ran_destroy = lc
|
|
|
|
|
.shutdown(|s| {
|
|
|
|
|
destroys.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
assert_eq!(s.value, 42);
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(ran_destroy);
|
2026-08-10 19:35:10 +08:00
|
|
|
|
|
|
|
|
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
|
2026-08-10 22:17:19 +08:00
|
|
|
/// dropped without being run, and the bool return reflects exactly which calls ran a closure.
|
2026-08-10 19:35:10 +08:00
|
|
|
#[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();
|
|
|
|
|
|
2026-08-10 22:17:19 +08:00
|
|
|
let startup_ran: Vec<bool> = (0..3)
|
|
|
|
|
.map(|_| {
|
|
|
|
|
lc.startup(|| -> Result<State, TestErr> {
|
|
|
|
|
inits.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
Ok(State { value: 7 })
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
2026-08-10 19:35:10 +08:00
|
|
|
})
|
2026-08-10 22:17:19 +08:00
|
|
|
.collect();
|
|
|
|
|
assert_eq!(startup_ran, vec![true, false, false]);
|
2026-08-10 19:35:10 +08:00
|
|
|
assert_eq!(lc.count(), 3);
|
|
|
|
|
assert_eq!(inits.load(Ordering::SeqCst), 1);
|
|
|
|
|
|
2026-08-10 22:17:19 +08:00
|
|
|
let shutdown_ran: Vec<bool> = (0..3)
|
|
|
|
|
.map(|_| {
|
|
|
|
|
lc.shutdown(|_s| {
|
|
|
|
|
destroys.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
2026-08-10 19:35:10 +08:00
|
|
|
})
|
2026-08-10 22:17:19 +08:00
|
|
|
.collect();
|
|
|
|
|
assert_eq!(shutdown_ran, vec![false, false, true]);
|
2026-08-10 19:35:10 +08:00
|
|
|
|
|
|
|
|
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 {
|
2026-08-10 22:17:19 +08:00
|
|
|
let ran_init = lc
|
|
|
|
|
.startup(|| -> Result<State, TestErr> {
|
|
|
|
|
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);
|
2026-08-10 19:35:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|