#[cfg(feature = "watch")]
mod drift {
use lockedenv::load;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
#[test]
fn watcher_detects_change() {
std::env::set_var("DRIFT_CHANGE", "1");
let _config = load! { DRIFT_CHANGE: String };
let seen = Arc::new(AtomicBool::new(false));
let seen2 = seen.clone();
let _handle = lockedenv::watch!(
keys = ["DRIFT_CHANGE"],
interval_ms = 10,
on_drift = move |key: &str, _old: &str, _new: &str| {
if key == "DRIFT_CHANGE" {
seen2.store(true, Ordering::SeqCst);
}
}
);
std::thread::sleep(std::time::Duration::from_millis(20));
std::env::set_var("DRIFT_CHANGE", "2");
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(
seen.load(Ordering::SeqCst),
"watcher should detect the changed variable"
);
}
#[test]
fn watcher_detects_removal() {
std::env::set_var("DRIFT_REMOVAL", "present");
let _config = load! { DRIFT_REMOVAL: String };
let removed = Arc::new(AtomicBool::new(false));
let removed2 = removed.clone();
let _handle = lockedenv::watch!(
keys = ["DRIFT_REMOVAL"],
interval_ms = 10,
on_drift = move |key: &str, _old: &str, new: &str| {
if key == "DRIFT_REMOVAL" && new == "<removed>" {
removed2.store(true, Ordering::SeqCst);
}
}
);
std::thread::sleep(std::time::Duration::from_millis(20));
std::env::remove_var("DRIFT_REMOVAL");
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(
removed.load(Ordering::SeqCst),
"watcher should fire when a variable is removed"
);
}
#[test]
fn watcher_stops_on_drop() {
std::env::set_var("DRIFT_STOP_TEST", "initial");
let _config = load! { DRIFT_STOP_TEST: String };
let call_count = Arc::new(AtomicUsize::new(0));
let cc2 = call_count.clone();
let handle = lockedenv::watch!(
keys = ["DRIFT_STOP_TEST"],
interval_ms = 10,
on_drift = move |key: &str, _old: &str, _new: &str| {
if key == "DRIFT_STOP_TEST" {
cc2.fetch_add(1, Ordering::SeqCst);
}
}
);
std::thread::sleep(std::time::Duration::from_millis(30));
drop(handle); std::thread::sleep(std::time::Duration::from_millis(20));
let count_before = call_count.load(Ordering::SeqCst);
std::env::set_var("DRIFT_STOP_TEST", "after_drop");
std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(
call_count.load(Ordering::SeqCst),
count_before,
"watcher must not fire after the handle is dropped",
);
}
#[test]
fn watcher_reports_multiple_changes() {
std::env::set_var("DRIFT_MULTI_A", "0");
std::env::set_var("DRIFT_MULTI_B", "0");
let _cfg = load! { DRIFT_MULTI_A: u32, DRIFT_MULTI_B: u32 };
let count = Arc::new(AtomicUsize::new(0));
let c2 = count.clone();
let _handle = lockedenv::watch!(
keys = ["DRIFT_MULTI_A", "DRIFT_MULTI_B"],
interval_ms = 10,
on_drift = move |_key: &str, _old: &str, _new: &str| {
c2.fetch_add(1, Ordering::SeqCst);
}
);
std::thread::sleep(std::time::Duration::from_millis(20));
std::env::set_var("DRIFT_MULTI_A", "1");
std::env::set_var("DRIFT_MULTI_B", "1");
std::thread::sleep(std::time::Duration::from_millis(60));
assert!(
count.load(Ordering::SeqCst) >= 2,
"expected at least 2 drift callbacks, got {}",
count.load(Ordering::SeqCst),
);
}
#[test]
fn watcher_detects_addition() {
std::env::remove_var("DRIFT_ADDITION");
let seen = Arc::new(AtomicBool::new(false));
let seen2 = seen.clone();
let _handle = lockedenv::watch!(
keys = ["DRIFT_ADDITION"],
interval_ms = 10,
on_drift = move |key: &str, old: &str, _new: &str| {
if key == "DRIFT_ADDITION" && old == "<missing>" {
seen2.store(true, Ordering::SeqCst);
}
}
);
std::thread::sleep(std::time::Duration::from_millis(20));
std::env::set_var("DRIFT_ADDITION", "appeared");
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(
seen.load(Ordering::SeqCst),
"watcher should detect a newly added variable"
);
}
#[test]
fn watcher_empty_keys_never_fires() {
let count = Arc::new(AtomicUsize::new(0));
let c2 = count.clone();
let _handle = lockedenv::watch!(
keys = [],
interval_ms = 10,
on_drift = move |_key: &str, _old: &str, _new: &str| {
c2.fetch_add(1, Ordering::SeqCst);
}
);
std::env::set_var("UNRELATED_VAR_XYZ", "value");
std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(
count.load(Ordering::SeqCst),
0,
"watcher with no keys must never fire",
);
}
#[test]
fn watcher_survives_callback_panic() {
std::env::set_var("DRIFT_PANIC_KEY", "v0");
let call_count = Arc::new(AtomicUsize::new(0));
let cc2 = call_count.clone();
let survived = Arc::new(AtomicBool::new(false));
let survived2 = survived.clone();
let _handle = lockedenv::watch!(
keys = ["DRIFT_PANIC_KEY"],
interval_ms = 10,
on_drift = move |_key: &str, _old: &str, _new: &str| {
let n = cc2.fetch_add(1, Ordering::SeqCst);
if n == 0 {
panic!("intentional callback panic on first drift");
}
survived2.store(true, Ordering::SeqCst);
}
);
std::thread::sleep(std::time::Duration::from_millis(20));
std::env::set_var("DRIFT_PANIC_KEY", "v1");
std::thread::sleep(std::time::Duration::from_millis(60));
std::env::set_var("DRIFT_PANIC_KEY", "v2");
std::thread::sleep(std::time::Duration::from_millis(60));
assert!(
survived.load(Ordering::SeqCst),
"watcher thread must survive a panicking on_drift callback",
);
}
}