#![cfg(feature = "watch")]
use std::fs;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
use confers::watcher::{
AdaptiveDebouncer, FsWatcher, MultiFsWatcher, WatcherConfig, WatcherConfigBuilder, WatcherGuard,
};
const DEFAULT_DEBOUNCE_MS: u64 = 200;
const DEFAULT_MIN_RELOAD_MS: u64 = 1000;
const DEFAULT_MAX_FAILURES: u32 = 5;
const DEFAULT_FAILURE_PAUSE_MS: u64 = 30000;
const CUSTOM_DEBOUNCE_MS: u64 = 500;
const CUSTOM_MIN_RELOAD_MS: u64 = 2000;
const CUSTOM_MAX_FAILURES: u32 = 10;
const CUSTOM_FAILURE_PAUSE_MS: u64 = 60000;
#[test]
#[allow(unused_imports)]
fn test_watcher_module_exists() {
use confers::watcher::*;
}
#[cfg(feature = "progressive-reload")]
#[test]
#[allow(unused_imports)]
fn test_progressive_reload_module_exists() {
use confers::watcher::{
HealthStatus, ProgressiveReloader, ProgressiveReloaderBuilder, ReloadHealthCheck,
ReloadOutcome, ReloadStrategy,
};
}
#[test]
fn test_watcher_config_default() {
let config = WatcherConfig::default();
assert_eq!(config.debounce_ms, DEFAULT_DEBOUNCE_MS);
assert_eq!(config.min_reload_interval_ms, DEFAULT_MIN_RELOAD_MS);
assert_eq!(config.max_consecutive_failures, DEFAULT_MAX_FAILURES);
assert_eq!(config.failure_pause_ms, DEFAULT_FAILURE_PAUSE_MS);
assert!(!config.rollback_on_validation_failure);
}
#[test]
fn test_watcher_config_new() {
let config = WatcherConfig::new();
assert_eq!(config.debounce_ms, DEFAULT_DEBOUNCE_MS);
assert_eq!(config.min_reload_interval_ms, DEFAULT_MIN_RELOAD_MS);
}
#[test]
fn test_watcher_config_builder() {
let config = WatcherConfigBuilder::new()
.debounce_ms(CUSTOM_DEBOUNCE_MS)
.min_reload_interval_ms(CUSTOM_MIN_RELOAD_MS)
.max_consecutive_failures(CUSTOM_MAX_FAILURES)
.failure_pause_ms(CUSTOM_FAILURE_PAUSE_MS)
.rollback_on_validation_failure(true)
.build();
assert_eq!(config.debounce_ms, CUSTOM_DEBOUNCE_MS);
assert_eq!(config.min_reload_interval_ms, CUSTOM_MIN_RELOAD_MS);
assert_eq!(config.max_consecutive_failures, CUSTOM_MAX_FAILURES);
assert_eq!(config.failure_pause_ms, CUSTOM_FAILURE_PAUSE_MS);
assert!(config.rollback_on_validation_failure);
}
#[test]
fn test_watcher_config_builder_partial() {
const PARTIAL_DEBOUNCE_MS: u64 = 100;
let config = WatcherConfigBuilder::new()
.debounce_ms(PARTIAL_DEBOUNCE_MS)
.build();
assert_eq!(config.debounce_ms, PARTIAL_DEBOUNCE_MS);
assert_eq!(config.min_reload_interval_ms, DEFAULT_MIN_RELOAD_MS); assert_eq!(config.max_consecutive_failures, DEFAULT_MAX_FAILURES); }
#[test]
fn test_watcher_config_with_debounce() {
const WITH_DEBOUNCE_MS: u64 = 300;
let config = WatcherConfig::new().with_debounce(WITH_DEBOUNCE_MS);
assert_eq!(config.debounce_ms, WITH_DEBOUNCE_MS);
assert_eq!(config.min_reload_interval_ms, DEFAULT_MIN_RELOAD_MS);
}
#[test]
fn test_watcher_config_with_min_reload_interval() {
const WITH_MIN_RELOAD_MS: u64 = 500;
let config = WatcherConfig::new().with_min_reload_interval(WITH_MIN_RELOAD_MS);
assert_eq!(config.min_reload_interval_ms, WITH_MIN_RELOAD_MS);
}
#[test]
fn test_watcher_config_with_max_failures() {
const WITH_MAX_FAILURES: u32 = 3;
let config = WatcherConfig::new().with_max_consecutive_failures(WITH_MAX_FAILURES);
assert_eq!(config.max_consecutive_failures, WITH_MAX_FAILURES);
}
#[test]
fn test_watcher_config_with_failure_pause() {
const WITH_FAILURE_PAUSE_MS: u64 = 10000;
let config = WatcherConfig::new().with_failure_pause(WITH_FAILURE_PAUSE_MS);
assert_eq!(config.failure_pause_ms, WITH_FAILURE_PAUSE_MS);
}
#[test]
fn test_watcher_config_with_rollback() {
let config = WatcherConfig::new().with_rollback_on_validation_failure(true);
assert!(config.rollback_on_validation_failure);
}
#[test]
fn test_watcher_guard_creation() {
let guard = WatcherGuard::new();
assert!(!guard.is_running());
}
#[test]
fn test_watcher_guard_start_stop() {
let guard = WatcherGuard::new();
assert!(!guard.is_running());
guard.start();
assert!(guard.is_running());
guard.stop();
assert!(!guard.is_running());
}
#[test]
fn test_watcher_guard_drop_stops_watcher() {
let flag = Arc::new(AtomicBool::new(false));
let flag_clone = Arc::clone(&flag);
{
let guard = WatcherGuard::new();
guard.start();
if guard.is_running() {
flag_clone.store(true, Ordering::SeqCst);
}
}
assert!(flag.load(Ordering::SeqCst));
}
#[test]
fn test_watcher_config_clone() {
let config = WatcherConfig::new();
let cloned = config.clone();
assert_eq!(config.debounce_ms, cloned.debounce_ms);
assert_eq!(config.min_reload_interval_ms, cloned.min_reload_interval_ms);
}
#[test]
fn test_watcher_config_debug() {
let config = WatcherConfig::new();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("WatcherConfig"));
assert!(debug_str.contains("debounce_ms"));
}
#[test]
fn test_adaptive_debouncer_default() {
let debouncer = AdaptiveDebouncer::new(100);
assert!(debouncer.should_process());
}
#[test]
fn test_adaptive_debouncer_should_not_process() {
let debouncer = AdaptiveDebouncer::new(1000);
assert!(debouncer.should_process());
assert!(!debouncer.should_process());
}
#[test]
fn test_adaptive_debouncer_after_window() {
let debouncer = AdaptiveDebouncer::new(50);
assert!(debouncer.should_process());
std::thread::sleep(Duration::from_millis(60));
assert!(debouncer.should_process());
}
#[test]
fn test_adaptive_debouncer_rapid_calls() {
let debouncer = AdaptiveDebouncer::new(100);
assert!(debouncer.should_process());
for _ in 0..9 {
assert!(!debouncer.should_process());
}
std::thread::sleep(Duration::from_millis(150));
assert!(debouncer.should_process());
}
#[test]
fn test_adaptive_debouncer_window_ms() {
let debouncer = AdaptiveDebouncer::new(500);
assert_eq!(debouncer.window_ms(), 500);
}
#[test]
fn test_adaptive_debouncer_reset() {
let debouncer = AdaptiveDebouncer::new(1000);
assert!(debouncer.should_process());
assert!(!debouncer.should_process());
debouncer.reset();
assert!(debouncer.should_process());
}
#[tokio::test]
async fn test_fs_watcher_nonexistent_path() {
let result = FsWatcher::new("/nonexistent/path/to/file.toml", 200).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_watcher_creation() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("config.toml");
fs::write(&file_path, "key = \"value\"").unwrap();
let watcher = FsWatcher::new(&file_path, 200).await;
assert!(watcher.is_ok());
if let Ok(mut watcher) = watcher {
assert!(watcher.is_running());
assert_eq!(watcher.watch_path(), file_path.as_path());
watcher.stop();
}
}
#[tokio::test]
async fn test_fs_watcher_stop() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("config.toml");
fs::write(&file_path, "key = \"value\"").unwrap();
let mut watcher = FsWatcher::new(&file_path, 200).await.unwrap();
assert!(watcher.is_running());
watcher.stop();
assert!(!watcher.is_running());
}
#[tokio::test]
async fn test_fs_watcher_file_creation_detection() {
let temp_dir = TempDir::new().unwrap();
let config_dir = temp_dir.path().join("configs");
fs::create_dir(&config_dir).unwrap();
let mut watcher = FsWatcher::new(&config_dir, 100).await.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
let new_file = config_dir.join("new_config.toml");
fs::write(&new_file, "new_key = \"new_value\"").unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
let event = tokio::time::timeout(Duration::from_millis(3000), watcher.recv()).await;
watcher.stop();
assert!(
event.is_ok(),
"file creation should be detected within 3s; \
timeout or channel error means watcher failed: {:?}",
event.err()
);
assert!(
event.unwrap().is_some(),
"recv() should deliver a Some(event), not None (channel closed)"
);
}
#[tokio::test]
async fn test_fs_watcher_file_modification_detection() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("config.toml");
fs::write(&config_file, "original = \"value\"").unwrap();
let mut watcher = FsWatcher::new(&config_file, 100).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
fs::write(&config_file, "modified = \"new_value\"").unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
let event = tokio::time::timeout(Duration::from_millis(500), watcher.recv()).await;
watcher.stop();
assert!(
event.is_ok(),
"file modification should be detected within 500ms: {:?}",
event.err()
);
assert!(
event.unwrap().is_some(),
"recv() should deliver a Some(event), not None (channel closed)"
);
}
#[tokio::test]
async fn test_fs_watcher_file_deletion_detection() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("config.toml");
fs::write(&config_file, "key = \"value\"").unwrap();
let mut watcher = FsWatcher::new(&config_file, 100).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
fs::remove_file(&config_file).unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
let event = tokio::time::timeout(Duration::from_millis(3000), watcher.recv()).await;
watcher.stop();
assert!(
event.is_ok(),
"file deletion should be detected within 3s: {:?}",
event.err()
);
assert!(
event.unwrap().is_some(),
"recv() should deliver a Some(event), not None (channel closed)"
);
}
#[tokio::test]
async fn test_multi_fs_watcher_empty_paths() {
let result = MultiFsWatcher::new(Vec::<String>::new(), 200).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_multi_fs_watcher_nonexistent_paths() {
let result = MultiFsWatcher::new(
vec!["/nonexistent/path1.toml", "/nonexistent/path2.toml"],
200,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_multi_fs_watcher_multiple_files() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("config1.toml");
let file2 = temp_dir.path().join("config2.toml");
fs::write(&file1, "key1 = \"value1\"").unwrap();
fs::write(&file2, "key2 = \"value2\"").unwrap();
let mut watcher = MultiFsWatcher::new(vec![&file1, &file2], 200).await;
assert!(watcher.is_ok());
if let Ok(ref mut watcher) = watcher {
assert!(watcher.is_running());
assert_eq!(watcher.watch_paths().len(), 2);
watcher.stop();
}
}
#[tokio::test]
async fn test_multi_fs_watcher_stop() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("config1.toml");
let file2 = temp_dir.path().join("config2.toml");
fs::write(&file1, "key1 = \"value1\"").unwrap();
fs::write(&file2, "key2 = \"value2\"").unwrap();
let mut watcher = MultiFsWatcher::new(vec![&file1, &file2], 200)
.await
.unwrap();
assert!(watcher.is_running());
watcher.stop();
assert!(!watcher.is_running());
}
#[tokio::test]
async fn test_multi_fs_watcher_recv_detects_modification() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("config1.toml");
let file2 = temp_dir.path().join("config2.toml");
fs::write(&file1, "key1 = \"value1\"").unwrap();
fs::write(&file2, "key2 = \"value2\"").unwrap();
let mut watcher = MultiFsWatcher::new(vec![&file1, &file2], 100)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
fs::write(&file1, "key1 = \"modified\"").unwrap();
tokio::time::sleep(Duration::from_millis(500)).await;
let event = tokio::time::timeout(Duration::from_millis(3000), watcher.recv()).await;
watcher.stop();
assert!(
event.is_ok(),
"file modification should be detected within 3s: {:?}",
event.err()
);
let path = event.unwrap().expect("recv() should deliver Some(event)");
assert!(
path == file1 || path == file2,
"event path should be one of the watched files, got {:?}",
path
);
}
#[tokio::test]
async fn test_multi_fs_watcher_drop_stops_watcher() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("config1.toml");
let file2 = temp_dir.path().join("config2.toml");
fs::write(&file1, "key1 = \"value1\"").unwrap();
fs::write(&file2, "key2 = \"value2\"").unwrap();
let running = {
let watcher = MultiFsWatcher::new(vec![&file1, &file2], 200)
.await
.unwrap();
assert!(watcher.is_running());
watcher.is_running()
};
assert!(running, "watcher should be running before drop");
}
#[tokio::test]
async fn test_fs_watcher_permission_handling() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("config.toml");
fs::write(&file_path, "key = \"value\"").unwrap();
assert!(file_path.exists());
let watcher = FsWatcher::new(&file_path, 200).await;
assert!(watcher.is_ok());
}
#[test]
fn test_watcher_config_validation() {
let config = WatcherConfigBuilder::new()
.debounce_ms(0) .failure_pause_ms(0)
.build();
assert_eq!(config.debounce_ms, 0);
assert_eq!(config.failure_pause_ms, 0);
}
#[test]
fn test_watcher_guard_from_running() {
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let guard = WatcherGuard::from_running(running.clone());
assert!(guard.is_running());
guard.stop();
assert!(!guard.is_running());
}
#[tokio::test]
async fn test_watcher_guard_shutdown() {
let guard = WatcherGuard::new();
guard.start();
assert!(guard.is_running());
let result = guard.shutdown(Duration::from_secs(1)).await;
assert!(result.is_ok());
assert!(!guard.is_running());
}
#[test]
fn test_watcher_config_builder_default() {
let builder = WatcherConfigBuilder::default();
let config = builder.build();
assert_eq!(config.debounce_ms, DEFAULT_DEBOUNCE_MS);
assert_eq!(config.min_reload_interval_ms, DEFAULT_MIN_RELOAD_MS);
assert_eq!(config.max_consecutive_failures, DEFAULT_MAX_FAILURES);
assert_eq!(config.failure_pause_ms, DEFAULT_FAILURE_PAUSE_MS);
}
#[test]
fn test_watcher_config_builder_chaining() {
let config = WatcherConfig::builder()
.debounce_ms(400)
.min_reload_interval_ms(3000)
.max_consecutive_failures(7)
.failure_pause_ms(45000)
.rollback_on_validation_failure(true)
.build();
assert_eq!(config.debounce_ms, 400);
assert_eq!(config.min_reload_interval_ms, 3000);
assert_eq!(config.max_consecutive_failures, 7);
assert_eq!(config.failure_pause_ms, 45000);
assert!(config.rollback_on_validation_failure);
}
#[cfg(feature = "progressive-reload")]
mod progressive_tests {
use super::*;
use async_trait::async_trait;
use confers::interface::ConfigProvider;
use confers::types::AnnotatedValue;
use confers::watcher::{
HealthStatus, ProgressiveReloader, ProgressiveReloaderBuilder, ReloadHealthCheck,
ReloadOutcome, ReloadStrategy,
};
use std::sync::Arc;
struct AlwaysHealthyCheck;
#[async_trait]
impl ReloadHealthCheck for AlwaysHealthyCheck {
async fn check(&self, _provider: Arc<dyn ConfigProvider>) -> HealthStatus {
HealthStatus::Healthy
}
}
#[allow(dead_code)]
struct AlwaysDegradedCheck;
#[async_trait]
impl ReloadHealthCheck for AlwaysDegradedCheck {
async fn check(&self, _provider: Arc<dyn ConfigProvider>) -> HealthStatus {
HealthStatus::Degraded {
reason: "degraded for testing".to_string(),
}
}
}
struct AlwaysCriticalCheck;
#[async_trait]
impl ReloadHealthCheck for AlwaysCriticalCheck {
async fn check(&self, _provider: Arc<dyn ConfigProvider>) -> HealthStatus {
HealthStatus::Critical {
reason: "critical failure".to_string(),
}
}
}
#[tokio::test]
async fn test_progressive_reload_immediate() {
let reloader = ProgressiveReloader::new(Arc::new(1i32), ReloadStrategy::Immediate);
struct MockProvider;
impl ConfigProvider for MockProvider {
fn get_raw(&self, _key: &str) -> Option<&AnnotatedValue> {
None
}
fn keys(&self) -> Vec<String> {
vec![]
}
}
let result = reloader
.begin_reload(Arc::new(2i32), Arc::new(MockProvider))
.await
.unwrap();
assert!(matches!(result, ReloadOutcome::Committed));
assert_eq!(*reloader.current(), 2);
}
#[tokio::test]
async fn test_progressive_reload_canary_healthy() {
let reloader = ProgressiveReloader::new(
Arc::new(1i32),
ReloadStrategy::Canary {
trial_duration: Duration::from_millis(50),
poll_interval: Duration::from_millis(10),
},
)
.with_health_check(Arc::new(AlwaysHealthyCheck));
struct MockProvider;
impl ConfigProvider for MockProvider {
fn get_raw(&self, _key: &str) -> Option<&AnnotatedValue> {
None
}
fn keys(&self) -> Vec<String> {
vec![]
}
}
let result = reloader
.begin_reload(Arc::new(2i32), Arc::new(MockProvider))
.await
.unwrap();
assert!(matches!(result, ReloadOutcome::Committed));
assert_eq!(*reloader.current(), 2);
}
#[tokio::test]
async fn test_progressive_reload_canary_critical_rollback() {
let reloader = ProgressiveReloader::new(
Arc::new(1i32),
ReloadStrategy::Canary {
trial_duration: Duration::from_millis(100),
poll_interval: Duration::from_millis(10),
},
)
.with_health_check(Arc::new(AlwaysCriticalCheck));
struct MockProvider;
impl ConfigProvider for MockProvider {
fn get_raw(&self, _key: &str) -> Option<&AnnotatedValue> {
None
}
fn keys(&self) -> Vec<String> {
vec![]
}
}
let result = reloader
.begin_reload(Arc::new(2i32), Arc::new(MockProvider))
.await;
assert!(result.is_err());
assert_eq!(*reloader.current(), 1);
}
#[tokio::test]
async fn test_progressive_reload_linear() {
let reloader = ProgressiveReloader::new(
Arc::new(1i32),
ReloadStrategy::Linear {
steps: 3,
interval: Duration::from_millis(10),
},
)
.with_health_check(Arc::new(AlwaysHealthyCheck));
struct MockProvider;
impl ConfigProvider for MockProvider {
fn get_raw(&self, _key: &str) -> Option<&AnnotatedValue> {
None
}
fn keys(&self) -> Vec<String> {
vec![]
}
}
let result = reloader
.begin_reload(Arc::new(2i32), Arc::new(MockProvider))
.await
.unwrap();
assert!(matches!(result, ReloadOutcome::Committed));
assert_eq!(*reloader.current(), 2);
}
#[tokio::test]
async fn test_progressive_reloader_builder() {
let reloader = ProgressiveReloaderBuilder::new()
.initial(Arc::new(42i32))
.strategy(ReloadStrategy::Immediate)
.build();
assert_eq!(*reloader.current(), 42);
}
#[tokio::test]
async fn test_progressive_reloader_clone() {
let reloader = ProgressiveReloader::new(Arc::new(1i32), ReloadStrategy::Immediate);
let cloned = reloader.clone();
assert_eq!(*cloned.current(), 1);
}
#[test]
fn test_reload_strategy_debug() {
let immediate = ReloadStrategy::Immediate;
assert!(format!("{:?}", immediate).contains("Immediate"));
let canary = ReloadStrategy::Canary {
trial_duration: Duration::from_secs(1),
poll_interval: Duration::from_secs(1),
};
assert!(format!("{:?}", canary).contains("Canary"));
let linear = ReloadStrategy::Linear {
steps: 5,
interval: Duration::from_secs(1),
};
assert!(format!("{:?}", linear).contains("Linear"));
}
#[test]
fn test_health_status_debug() {
let healthy = HealthStatus::Healthy;
assert!(format!("{:?}", healthy).contains("Healthy"));
let degraded = HealthStatus::Degraded {
reason: "test".to_string(),
};
assert!(format!("{:?}", degraded).contains("Degraded"));
let critical = HealthStatus::Critical {
reason: "test".to_string(),
};
assert!(format!("{:?}", critical).contains("Critical"));
}
#[test]
fn test_reload_outcome_debug() {
let committed = ReloadOutcome::Committed;
assert!(format!("{:?}", committed).contains("Committed"));
let rolled_back = ReloadOutcome::RolledBack {
reason: "test reason".to_string(),
};
assert!(format!("{:?}", rolled_back).contains("RolledBack"));
}
}