#![cfg(feature = "hot-reload")]
use std::io::Write;
use std::path::Path;
use std::time::{Duration, Instant};
use rtb_config::Config;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize)]
struct Port {
#[serde(default)]
port: u16,
}
const SETTLE: Duration = Duration::from_secs(5);
fn write_in_place(path: &Path, port: u16) {
let mut f = std::fs::File::create(path).unwrap();
writeln!(f, "port: {port}").unwrap();
f.sync_all().unwrap();
}
fn write_rename_replace(path: &Path, port: u16) {
let tmp = path.with_extension("tmp");
write_in_place(&tmp, port);
std::fs::rename(&tmp, path).unwrap();
}
fn wait_for_port(cfg: &Config<Port>, want: u16) {
let deadline = Instant::now() + SETTLE;
while Instant::now() < deadline {
if cfg.get().port == want {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("port stayed at {} after {SETTLE:?}; file says {want}", cfg.get().port);
}
#[test]
fn rename_replace_saves_keep_the_watcher_attached() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.yaml");
write_in_place(&path, 1);
let cfg = Config::<Port>::builder().user_file(&path).build().unwrap();
let _handle = cfg.watch_files().unwrap();
write_in_place(&path, 2);
wait_for_port(&cfg, 2);
for port in [3, 4, 5] {
write_rename_replace(&path, port);
wait_for_port(&cfg, port);
}
for port in [6, 7] {
write_in_place(&path, port);
wait_for_port(&cfg, port);
}
}
#[test]
fn a_file_created_after_the_watch_starts_is_picked_up() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.yaml");
let cfg = Config::<Port>::builder().user_file(&path).build().unwrap();
let _handle = cfg.watch_files().unwrap();
write_rename_replace(&path, 9);
wait_for_port(&cfg, 9);
}
#[cfg(unix)]
#[test]
fn a_kubernetes_style_symlink_swap_is_picked_up() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir(root.join("..v1")).unwrap();
write_in_place(&root.join("..v1/c.yaml"), 1);
symlink("..v1", root.join("..data")).unwrap();
symlink("..data/c.yaml", root.join("c.yaml")).unwrap();
let path = root.join("c.yaml");
let cfg = Config::<Port>::builder().user_file(&path).build().unwrap();
let _handle = cfg.watch_files().unwrap();
for (old, new, port) in [("..v1", "..v2", 2), ("..v2", "..v3", 3)] {
std::fs::create_dir(root.join(new)).unwrap();
write_in_place(&root.join(new).join("c.yaml"), port);
symlink(new, root.join("..data_tmp")).unwrap();
std::fs::rename(root.join("..data_tmp"), root.join("..data")).unwrap();
std::fs::remove_dir_all(root.join(old)).unwrap();
wait_for_port(&cfg, port);
}
}
#[cfg(unix)]
#[test]
fn an_in_place_edit_behind_a_symlink_is_picked_up() {
let link_dir = tempfile::tempdir().unwrap();
let real_dir = tempfile::tempdir().unwrap();
let real = real_dir.path().join("c.yaml");
write_in_place(&real, 1);
let path = link_dir.path().join("c.yaml");
std::os::unix::fs::symlink(&real, &path).unwrap();
let cfg = Config::<Port>::builder().user_file(&path).build().unwrap();
let _handle = cfg.watch_files().unwrap();
write_in_place(&real, 2);
wait_for_port(&cfg, 2);
}
#[test]
fn an_idle_file_does_not_reload_itself() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("c.yaml");
write_in_place(&path, 1);
let cfg = Config::<Port>::builder().user_file(&path).build().unwrap();
let mut rx = cfg.subscribe();
let _handle = cfg.watch_files().unwrap();
write_in_place(&path, 2);
wait_for_port(&cfg, 2);
std::thread::sleep(Duration::from_millis(750));
rx.mark_unchanged();
std::thread::sleep(Duration::from_millis(1500));
assert!(!rx.has_changed().unwrap(), "the watcher reloaded with no write to the file");
}