Files
RedBear-OS/local/recipes/system/redbear-threadtest/source/tests/watchdog.rs
T

221 lines
6.8 KiB
Rust

//! Host-runnable validation tests for the watchdog binary.
//!
//! Validates that the watchdog correctly:
//! - Detects clean exits (PASS)
//! - Detects non-zero exits (FAIL)
//! - Detects timeouts (HANG)
//! - Generates correct exit codes
use std::process::Command;
/// Helper: run the watchdog in host mode and return exit code + stderr text.
fn run_watchdog(args: &[&str], timeout_secs: u64) -> (i32, String) {
let mut cmd_args = vec!["host"];
cmd_args.extend(args);
// Add timeout.
let timeout_str = timeout_secs.to_string();
cmd_args.push("--timeout");
cmd_args.push(&timeout_str);
let output = Command::new(
std::env::current_dir()
.unwrap()
.join("target/debug/redbear-threadtest-watchdog"),
)
.args(&cmd_args)
.output();
match output {
Ok(o) => {
let code = o.status.code().unwrap_or(-1);
let stderr = String::from_utf8_lossy(&o.stderr).to_string();
(code, stderr)
}
Err(_e) => {
// Try cargo run as fallback.
let output = Command::new("cargo")
.args(["run", "--bin", "redbear-threadtest-watchdog", "--"])
.args(&cmd_args)
.output()
.expect("watchdog binary not found; build with `cargo build` first");
let code = output.status.code().unwrap_or(-1);
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(code, stderr)
}
}
}
/// Run the in-guest binary directly and capture its output for watchdog parsing.
fn run_threadtest(layer: &str) -> (i32, String, String) {
let output = Command::new(
std::env::current_dir()
.unwrap()
.join("target/debug/redbear-threadtest"),
)
.arg(layer)
.output();
match output {
Ok(o) => {
let code = o.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&o.stdout).to_string();
let stderr = String::from_utf8_lossy(&o.stderr).to_string();
(code, stdout, stderr)
}
Err(_e) => {
let output = Command::new("cargo")
.args(["run", "--bin", "redbear-threadtest", "--", layer])
.output()
.expect("threadtest binary not found; build with `cargo build` first");
let code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(code, stdout, stderr)
}
}
}
#[test]
fn watchdog_detects_clean_exit() {
let (code, stderr) = run_watchdog(&["true"], 10);
assert_eq!(code, 0, "watchdog should report PASS for exit 0: stderr={stderr}");
assert!(
stderr.contains("[WATCHDOG-PASS]"),
"watchdog should emit PASS marker: stderr={stderr}",
);
}
#[test]
fn watchdog_detects_non_zero_exit() {
let (code, stderr) = run_watchdog(&["false"], 10);
assert_eq!(code, 1, "watchdog should report FAIL for non-zero exit: stderr={stderr}");
assert!(
stderr.contains("[WATCHDOG-FAIL]"),
"watchdog should emit FAIL marker: stderr={stderr}",
);
}
#[test]
fn watchdog_detects_timeout_hang() {
// Use `sleep` to simulate a hanging process.
let (code, stderr) = run_watchdog(&["sleep", "30"], 2);
assert_eq!(code, 2, "watchdog exit code 2 = hang detected: stderr={stderr}");
assert!(
stderr.contains("[WATCHDOG-HANG]"),
"watchdog should emit HANG marker: stderr={stderr}",
);
assert!(
stderr.contains("[WATCHDOG-ATTRIBUTION]"),
"watchdog should emit ATTRIBUTION marker: stderr={stderr}",
);
}
#[test]
fn watchdog_reports_spawn_failure() {
let (code, stderr) = run_watchdog(&["/nonexistent/binary"], 10);
assert_eq!(code, 1, "watchdog should report FAIL for spawn failure");
assert!(
stderr.contains("[WATCHDOG-FAIL]"),
"watchdog should emit FAIL marker: stderr={stderr}",
);
assert!(
stderr.contains("spawn"),
"watchdog should report spawn error: stderr={stderr}",
);
}
#[test]
fn threadtest_l3_rwlock_produces_pass_marker() {
let (code, stdout, _stderr) = run_threadtest("l3");
assert_eq!(code, 0, "L3 RwLock host run must exit 0");
assert!(
stdout.contains("[THREADTEST-PASS]"),
"L3 must produce PASS marker: stdout={stdout}",
);
assert!(
stdout.contains("L3-rwlock"),
"L3 output must name the layer: stdout={stdout}",
);
}
#[test]
fn threadtest_l4_scope_produces_pass_marker() {
let (code, stdout, _stderr) = run_threadtest("l4");
assert_eq!(code, 0, "L4 scope host run must exit 0");
assert!(
stdout.contains("[THREADTEST-PASS]"),
"L4 must produce PASS marker: stdout={stdout}",
);
assert!(
stdout.contains("L4-scope"),
"L4 output must name the layer: stdout={stdout}",
);
}
#[test]
fn threadtest_l1_futex_produces_pass_marker() {
let (code, stdout, _stderr) = run_threadtest("l1");
assert_eq!(code, 0, "L1 futex host mock must exit 0");
assert!(
stdout.contains("[THREADTEST-PASS]"),
"L1 must produce PASS marker: stdout={stdout}",
);
}
#[test]
fn threadtest_l2_pthread_produces_pass_marker() {
let (code, stdout, _stderr) = run_threadtest("l2");
assert_eq!(code, 0, "L2 pthread host run must exit 0");
assert!(
stdout.contains("[THREADTEST-PASS]"),
"L2 must produce PASS marker: stdout={stdout}",
);
}
#[test]
fn threadtest_emits_begin_and_done_markers() {
let (code, stdout, _stderr) = run_threadtest("l3");
assert_eq!(code, 0);
assert!(
stdout.contains("[THREADTEST-BEGIN]"),
"must emit BEGIN marker"
);
// Single-layer runs don't emit DONE (only `all` mode does).
// That's fine — the watchdog reads per-layer PASS/FAIL markers.
}
#[test]
fn watchdog_can_supervise_threadtest_directly() {
// Full integration: watchdog watches threadtest directly.
// Use L4 scope which completes fast on host.
let output = Command::new("cargo")
.args([
"run",
"--bin",
"redbear-threadtest-watchdog",
"--",
"host",
"--timeout",
"60",
"cargo",
"run",
"--bin",
"redbear-threadtest",
"--",
"l4",
])
.output()
.expect("cargo run failed");
let stderr = String::from_utf8_lossy(&output.stderr);
let code = output.status.code().unwrap_or(-1);
assert_eq!(code, 0, "watchdog supervising L4 must PASS: stderr={stderr}");
assert!(
stderr.contains("[WATCHDOG-PASS]"),
"watchdog must emit PASS: stderr={stderr}",
);
}