use mwdg_ffi::*;
use core::ptr;
use core::sync::atomic::{AtomicU32, Ordering};
fn safe_mwdg_init() {
unsafe {
mwdg_init();
}
}
fn safe_mwdg_add(wdg: *mut mwdg_node, timeout_ms: u32) {
unsafe {
mwdg_add(wdg, timeout_ms);
}
}
static MOCK_TIME: AtomicU32 = AtomicU32::new(0);
extern "C" fn mock_get_time_ms() -> u32 {
MOCK_TIME.load(Ordering::Relaxed)
}
extern "C" fn mock_enter_critical() {
}
extern "C" fn mock_exit_critical() {
}
#[unsafe(no_mangle)]
pub extern "C" fn mwdg_get_time_milliseconds() -> u32 {
mock_get_time_ms()
}
#[unsafe(no_mangle)]
pub extern "C" fn mwdg_enter_critical() {
mock_enter_critical();
}
#[unsafe(no_mangle)]
pub extern "C" fn mwdg_exit_critical() {
mock_exit_critical();
}
fn set_time(ms: u32) {
MOCK_TIME.store(ms, Ordering::Relaxed);
}
fn reset() {
set_time(0);
safe_mwdg_init();
}
fn new_wdg() -> mwdg_node {
Default::default()
}
#[test]
fn test_check_no_watchdogs() {
reset();
assert_eq!(unsafe { mwdg_check() }, 0, "Empty list should be healthy");
}
#[test]
fn test_check_add_null() {
reset();
safe_mwdg_add(ptr::null_mut(), 100);
safe_mwdg_add(ptr::null_mut(), 200);
safe_mwdg_add(ptr::null_mut(), 300);
assert_eq!(unsafe { mwdg_check() }, 0, "Empty list should be healthy");
}
#[test]
fn test_check_add_with_remove() {
reset();
let mut wdg = new_wdg();
safe_mwdg_add(&mut wdg, 100);
set_time(200);
unsafe {
mwdg_remove(&mut wdg);
}
assert_eq!(
unsafe { mwdg_check() },
0,
"Removed expired WDG should not trigger failure"
);
}
#[test]
fn test_check_add_multiple_with_remove() {
reset();
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
safe_mwdg_add(&mut wdg1, 100);
safe_mwdg_add(&mut wdg2, 300);
safe_mwdg_add(&mut wdg3, 199);
set_time(200);
unsafe {
mwdg_remove(&mut wdg1);
mwdg_remove(&mut wdg3);
}
assert_eq!(
unsafe { mwdg_check() },
0,
"Removed expired WDG should not trigger failure"
);
}
#[test]
fn test_check_add_with_remove_and_add_again() {
reset();
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
let mut wdg4 = new_wdg();
safe_mwdg_init();
safe_mwdg_add(&mut wdg1, 100);
safe_mwdg_add(&mut wdg2, 300);
safe_mwdg_add(&mut wdg3, 199);
set_time(200);
unsafe {
mwdg_remove(&mut wdg1);
mwdg_remove(&mut wdg3);
}
assert_eq!(
unsafe { mwdg_check() },
0,
"Removed expired WDG should not trigger failure"
);
unsafe {
mwdg_add(&mut wdg4, 400);
mwdg_remove(&mut wdg2);
}
set_time(350);
assert_eq!(
unsafe { mwdg_check() },
0,
"Removed expired WDG should not trigger failure"
);
}
#[test]
fn test_check_remove_null() {
reset();
unsafe {
mwdg_remove(ptr::null_mut());
}
assert_eq!(unsafe { mwdg_check() }, 0, "Empty list should be healthy");
}
#[test]
fn test_register_single_and_check_ok() {
reset();
set_time(1000);
let mut wdg = new_wdg();
safe_mwdg_add(&mut wdg, 100);
assert_eq!(unsafe { mwdg_check() }, 0);
}
#[test]
fn test_single_expired() {
reset();
set_time(1000);
let mut wdg = new_wdg();
safe_mwdg_add(&mut wdg, 100);
set_time(1150);
assert_eq!(unsafe { mwdg_check() }, 1, "Should detect expired watchdog");
}
#[test]
fn test_feed_resets_timer() {
reset();
set_time(1000);
let mut wdg = new_wdg();
safe_mwdg_add(&mut wdg, 100);
set_time(1080);
unsafe {
mwdg_feed(&mut wdg);
}
set_time(1160);
assert_eq!(
unsafe { mwdg_check() },
0,
"Should be OK because we fed at 1080"
);
}
#[test]
fn test_multiple_all_ok() {
reset();
set_time(500);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
safe_mwdg_add(&mut wdg1, 100);
safe_mwdg_add(&mut wdg2, 200);
safe_mwdg_add(&mut wdg3, 300);
assert_eq!(unsafe { mwdg_check() }, 0);
}
#[test]
fn test_multiple_one_expired() {
reset();
set_time(500);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
safe_mwdg_add(&mut wdg1, 100);
safe_mwdg_add(&mut wdg2, 200);
safe_mwdg_add(&mut wdg3, 300);
set_time(650);
assert_eq!(unsafe { mwdg_check() }, 1, "wdg1 should be expired");
}
#[test]
fn test_wrapping_no_expire() {
reset();
let near_max = u32::MAX - 50;
set_time(near_max);
let mut wdg = new_wdg();
safe_mwdg_add(&mut wdg, 100);
set_time(near_max.wrapping_add(80));
assert_eq!(
unsafe { mwdg_check() },
0,
"80ms elapsed < 100ms timeout, should be OK across wrap"
);
}
#[test]
fn test_wrapping_expired() {
reset();
let near_max = u32::MAX - 50;
set_time(near_max);
let mut wdg = new_wdg();
safe_mwdg_add(&mut wdg, 100);
set_time(near_max.wrapping_add(150));
assert_eq!(
unsafe { mwdg_check() },
1,
"150ms elapsed > 100ms timeout, should be expired across wrap"
);
}
#[test]
fn test_once_expired_always_expired() {
reset();
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
set_time(0);
unsafe {
mwdg_add(&mut wdg1, 1);
mwdg_add(&mut wdg2, 5);
}
set_time(2);
assert_eq!(1, unsafe { mwdg_check() }, "WDG1 should be already expired");
unsafe {
mwdg_remove(&mut wdg1);
}
assert_eq!(
1,
unsafe { mwdg_check() },
"Once expired should be always expired"
);
}
#[test]
fn test_multiple_add_of_the_same_node() {
reset();
let mut wdg = new_wdg();
set_time(0);
unsafe {
mwdg_add(&mut wdg, 1);
set_time(2);
mwdg_add(&mut wdg, 1);
set_time(4);
mwdg_add(&mut wdg, 1);
}
assert_eq!(0, unsafe { mwdg_check() }, "Multiple add works as a feed");
}
#[test]
fn test_assign_id_before_add() {
reset();
set_time(0);
let mut wdg = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg, 42);
mwdg_add(&mut wdg, 100);
}
assert_eq!(unsafe { mwdg_check() }, 0);
}
#[test]
fn test_assign_id_after_add() {
reset();
set_time(0);
let mut wdg = new_wdg();
unsafe {
mwdg_add(&mut wdg, 100);
mwdg_assign_id(&mut wdg, 55);
}
assert_eq!(unsafe { mwdg_check() }, 0);
}
#[test]
fn test_assign_id_null_safe() {
reset();
unsafe {
mwdg_assign_id(ptr::null_mut(), 99);
}
assert_eq!(unsafe { mwdg_check() }, 0);
}
fn collect_expired_ids() -> Vec<u32> {
let mut ids = Vec::new();
let mut cursor: *mut mwdg_node = ptr::null_mut();
let mut id: u32 = 0;
while unsafe { mwdg_get_next_expired(&mut cursor, &mut id) } == 1 {
ids.push(id);
}
ids
}
#[test]
fn test_get_next_expired_empty_list() {
reset();
let ids = collect_expired_ids();
assert!(ids.is_empty(), "No expired nodes when list is empty");
}
#[test]
fn test_get_next_expired_none_expired() {
reset();
set_time(0);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg1, 1);
mwdg_assign_id(&mut wdg2, 2);
mwdg_add(&mut wdg1, 100);
mwdg_add(&mut wdg2, 200);
}
let ids = collect_expired_ids();
assert!(ids.is_empty(), "No expired nodes when all are healthy");
}
#[test]
fn test_get_next_expired_one_expired() {
reset();
set_time(0);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg1, 1);
mwdg_assign_id(&mut wdg2, 2);
mwdg_assign_id(&mut wdg3, 3);
mwdg_add(&mut wdg1, 100);
mwdg_add(&mut wdg2, 200);
mwdg_add(&mut wdg3, 300);
}
set_time(150);
assert_eq!(
unsafe { mwdg_check() },
1,
"mwdg_check must detect expiration"
);
let ids = collect_expired_ids();
assert_eq!(ids.len(), 1, "Exactly one node should be expired");
assert_eq!(ids[0], 1, "The expired node should be wdg1 (id=1)");
}
#[test]
fn test_get_next_expired_multiple_expired() {
reset();
set_time(0);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg1, 10);
mwdg_assign_id(&mut wdg2, 20);
mwdg_assign_id(&mut wdg3, 30);
mwdg_add(&mut wdg1, 100);
mwdg_add(&mut wdg2, 200);
mwdg_add(&mut wdg3, 300);
}
set_time(250);
assert_eq!(
unsafe { mwdg_check() },
1,
"mwdg_check must detect expiration"
);
let ids = collect_expired_ids();
assert_eq!(ids.len(), 2, "Two nodes should be expired");
assert!(ids.contains(&10), "wdg1 (id=10) should be expired");
assert!(ids.contains(&20), "wdg2 (id=20) should be expired");
}
#[test]
fn test_get_next_expired_all_expired() {
reset();
set_time(0);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
let mut wdg3 = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg1, 100);
mwdg_assign_id(&mut wdg2, 200);
mwdg_assign_id(&mut wdg3, 300);
mwdg_add(&mut wdg1, 50);
mwdg_add(&mut wdg2, 60);
mwdg_add(&mut wdg3, 70);
}
set_time(100); assert_eq!(
unsafe { mwdg_check() },
1,
"mwdg_check must detect expiration"
);
let ids = collect_expired_ids();
assert_eq!(ids.len(), 3, "All three nodes should be expired");
assert!(ids.contains(&100));
assert!(ids.contains(&200));
assert!(ids.contains(&300));
}
#[test]
fn test_get_next_expired_default_id_zero() {
reset();
set_time(0);
let mut wdg = new_wdg();
unsafe {
mwdg_add(&mut wdg, 50);
}
set_time(100);
assert_eq!(
unsafe { mwdg_check() },
1,
"mwdg_check must detect expiration"
);
let ids = collect_expired_ids();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], 0, "Default id should be 0");
}
#[test]
fn test_get_next_expired_null_cursor() {
reset();
let mut id: u32 = 0;
let result = unsafe { mwdg_get_next_expired(ptr::null_mut(), &mut id) };
assert_eq!(result, 0, "Null cursor should return 0");
}
#[test]
fn test_get_next_expired_null_out_id() {
reset();
let mut cursor: *mut mwdg_node = ptr::null_mut();
let result = unsafe { mwdg_get_next_expired(&mut cursor, ptr::null_mut()) };
assert_eq!(result, 0, "Null out_id should return 0");
}
#[test]
fn test_get_next_expired_both_null() {
reset();
let result = unsafe { mwdg_get_next_expired(ptr::null_mut(), ptr::null_mut()) };
assert_eq!(result, 0, "Both params null should return 0");
}
#[test]
fn test_get_next_expired_after_feed() {
reset();
set_time(0);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg1, 1);
mwdg_assign_id(&mut wdg2, 2);
mwdg_add(&mut wdg1, 100);
mwdg_add(&mut wdg2, 100);
}
set_time(80);
unsafe {
mwdg_feed(&mut wdg1);
} set_time(150);
assert_eq!(
unsafe { mwdg_check() },
1,
"mwdg_check must detect expiration"
);
let ids = collect_expired_ids();
assert_eq!(ids.len(), 1, "Only unfed wdg2 should expire");
assert_eq!(ids[0], 2);
}
#[test]
fn test_get_next_expired_wrapping_time() {
reset();
let near_max = u32::MAX - 50;
set_time(near_max);
let mut wdg = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg, 77);
mwdg_add(&mut wdg, 100);
}
set_time(near_max.wrapping_add(150));
assert_eq!(
unsafe { mwdg_check() },
1,
"mwdg_check must detect expiration"
);
let ids = collect_expired_ids();
assert_eq!(ids.len(), 1);
assert_eq!(ids[0], 77);
}
#[test]
fn test_get_next_expired_without_prior_check() {
reset();
set_time(0);
let mut wdg = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg, 5);
mwdg_add(&mut wdg, 50);
}
set_time(100);
let ids = collect_expired_ids();
assert!(
ids.is_empty(),
"Iterator must return nothing when mwdg_check has not detected expiration"
);
}
#[test]
fn test_get_next_expired_after_feed_race() {
reset();
set_time(0);
let mut wdg = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg, 42);
mwdg_add(&mut wdg, 100);
}
set_time(200);
assert_eq!(
unsafe { mwdg_check() },
1,
"Should detect expiration at t=200"
);
set_time(201);
unsafe {
mwdg_feed(&mut wdg);
}
let ids = collect_expired_ids();
assert_eq!(
ids.len(),
0,
"Node fed after snapshot must not appear in expired list"
);
}
#[test]
fn test_get_next_expired_feed_race_does_not_falsely_report_healthy_node() {
reset();
set_time(0);
let mut wdg1 = new_wdg();
let mut wdg2 = new_wdg();
unsafe {
mwdg_assign_id(&mut wdg1, 1);
mwdg_assign_id(&mut wdg2, 2);
mwdg_add(&mut wdg1, 100); mwdg_add(&mut wdg2, 200); }
set_time(350);
unsafe {
mwdg_feed(&mut wdg2);
}
set_time(450);
assert_eq!(unsafe { mwdg_check() }, 1, "Should detect wdg1 expiration");
set_time(460);
unsafe {
mwdg_feed(&mut wdg2);
}
let ids = collect_expired_ids();
assert_eq!(ids, vec![1], "Only wdg1 should be expired");
}