rtb-config 0.6.5

Layered, typed configuration backed by figment. Part of the phpboyscout Rust toolkit.
Documentation
//! Hot-reload watcher behaviour against the ways files really get saved.

#![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");
}