use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use skadoosh::watch::{WatchConfig, WatchEvent, WatchManager};
use tokio_util::sync::CancellationToken;
fn temp_file(label: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut p = std::env::temp_dir();
p.push(format!(
"skadoosh-watch-test-{}-{}-{}.txt",
std::process::id(),
label,
n,
));
p
}
#[test]
fn watch_event_renders_notification_text() {
let file = WatchEvent::FileChanged(PathBuf::from("Cargo.toml"));
assert_eq!(file.message(), "The file Cargo.toml has changed.");
let proc_ = WatchEvent::ProcessExited(4242);
assert_eq!(proc_.message(), "Process 4242 has exited.");
let timer = WatchEvent::TimerElapsed("30".to_string());
assert_eq!(timer.message(), "Your 30-second timer is up.");
}
#[tokio::test]
async fn timer_fires_timer_elapsed_event() {
let shutdown = CancellationToken::new();
let cfg = WatchConfig {
files: vec![],
processes: vec![],
timers: vec![1],
};
let (manager, mut rx) = WatchManager::start(&cfg, shutdown.clone());
let ev = tokio::time::timeout(Duration::from_secs(3), rx.recv())
.await
.expect("timer should fire within 3s")
.expect("watch channel closed before the timer fired");
assert_eq!(ev, WatchEvent::TimerElapsed("1".to_string()));
assert_eq!(ev.message(), "Your 1-second timer is up.");
let closed = tokio::time::timeout(Duration::from_millis(200), rx.recv())
.await
.expect("channel should close promptly after the timer fires");
assert!(closed.is_none(), "timer must not fire more than once");
manager.shutdown().await;
}
#[tokio::test]
async fn file_watcher_emits_file_changed() {
let path = temp_file("file");
std::fs::write(&path, b"initial").unwrap();
let shutdown = CancellationToken::new();
let cfg = WatchConfig {
files: vec![path.clone()],
processes: vec![],
timers: vec![],
};
let (manager, mut rx) = WatchManager::start(&cfg, shutdown.clone());
tokio::time::sleep(Duration::from_millis(200)).await;
std::fs::write(&path, b"modified").unwrap();
let ev = tokio::time::timeout(Duration::from_secs(3), rx.recv())
.await
.expect("file change should be detected within 3s")
.expect("watch channel closed before the file change arrived");
assert_eq!(ev, WatchEvent::FileChanged(path.clone()));
assert!(ev.message().contains("has changed"));
let _ = std::fs::remove_file(&path);
manager.shutdown().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn process_watcher_emits_process_exited() {
if !PathBuf::from("/proc").exists() {
eprintln!("skipping: /proc not present (non-Linux)");
return;
}
let mut child = std::process::Command::new("sleep")
.arg("2")
.spawn()
.expect("spawn `sleep`");
let pid = child.id();
let shutdown = CancellationToken::new();
let cfg = WatchConfig {
files: vec![],
processes: vec![pid],
timers: vec![],
};
let (manager, mut rx) = WatchManager::start(&cfg, shutdown.clone());
let _ = child.wait().expect("wait for child");
let ev = tokio::time::timeout(Duration::from_secs(5), rx.recv())
.await
.expect("process exit should be detected within 5s")
.expect("watch channel closed before the process exit arrived");
assert_eq!(ev, WatchEvent::ProcessExited(pid));
assert!(ev.message().contains("has exited"));
manager.shutdown().await;
}
#[tokio::test]
async fn empty_config_emits_no_events() {
let shutdown = CancellationToken::new();
let cfg = WatchConfig::default();
assert!(cfg.is_empty());
let (manager, mut rx) = WatchManager::start(&cfg, shutdown.clone());
let res = tokio::time::timeout(Duration::from_millis(200), rx.recv()).await;
assert!(
!matches!(res, Ok(Some(_))),
"no events should fire for an empty config, got {res:?}",
);
manager.shutdown().await;
}
#[tokio::test]
async fn shutdown_cancels_pending_timer() {
let shutdown = CancellationToken::new();
let cfg = WatchConfig {
files: vec![],
processes: vec![],
timers: vec![30],
};
let (manager, mut rx) = WatchManager::start(&cfg, shutdown.clone());
manager.shutdown().await;
let res = tokio::time::timeout(Duration::from_millis(300), rx.recv()).await;
assert!(
!matches!(res, Ok(Some(_))),
"a cancelled timer must not fire, got {res:?}",
);
}