Files
sarasacw-omrf/omrf/tests/library_lifecycle.rs
T

142 lines
4.3 KiB
Rust
Raw Normal View History

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
/// 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();
}