use crossbeam_channel;
use dampen_dev::watcher::{FileWatcher, FileWatcherConfig};
use std::fs;
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant};
use tempfile::TempDir;
fn setup_test_dir() -> TempDir {
TempDir::new().expect("Failed to create temp directory")
}
fn create_dampen_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let file_path = dir.path().join(name);
fs::write(&file_path, content).expect("Failed to write file");
file_path
}
fn modify_dampen_file(path: &PathBuf, content: &str) {
fs::write(path, content).expect("Failed to modify file");
}
pub struct TestTiming {
pub debounce_duration: Duration,
pub wait_multiplier: f64,
pub test_timeout: Duration,
pub poll_interval: Duration,
}
impl Default for TestTiming {
fn default() -> Self {
Self {
debounce_duration: Duration::from_millis(100),
wait_multiplier: 1.5,
test_timeout: Duration::from_millis(500),
poll_interval: Duration::from_millis(5),
}
}
}
impl TestTiming {
pub fn wait_for_debounce(&self) -> Duration {
self.debounce_duration.mul_f64(self.wait_multiplier)
}
}
fn wait_for_events<T>(receiver: &crossbeam_channel::Receiver<T>, timeout: Duration) -> Vec<T> {
let start = Instant::now();
let mut events = Vec::new();
while start.elapsed() < timeout {
while let Ok(event) = receiver.try_recv() {
events.push(event);
}
if start.elapsed() < timeout {
thread::sleep(Duration::from_millis(5));
}
}
events
}
fn wait_for_debounce() {
let timing = TestTiming::default();
thread::sleep(timing.wait_for_debounce());
}
#[test]
fn test_file_creation_detection() {
let temp_dir = setup_test_dir();
let config = FileWatcherConfig {
watch_paths: vec![temp_dir.path().to_path_buf()],
debounce_ms: 100,
extension_filter: ".dampen".to_string(),
recursive: true,
};
let mut watcher = FileWatcher::new(config).expect("Failed to create watcher");
watcher
.watch(temp_dir.path().to_path_buf())
.expect("Failed to watch directory");
thread::sleep(Duration::from_millis(50));
let test_file = temp_dir.path().join("test.dampen");
fs::write(
&test_file,
r#"<dampen version="1.1" encoding="utf-8"><text value="Hello" /></dampen>"#,
)
.expect("Failed to create file");
wait_for_debounce();
let receiver = watcher.receiver();
let mut received_events = Vec::new();
while let Ok(path) = receiver.try_recv() {
received_events.push(path);
}
assert!(
!received_events.is_empty(),
"Expected to receive file creation event, but got none"
);
assert!(
received_events.iter().any(|p| p == &test_file),
"Expected event for {:?}, but received events for: {:?}",
test_file,
received_events
);
}
#[test]
fn test_file_modification_detection() {
let temp_dir = setup_test_dir();
let test_file = create_dampen_file(
&temp_dir,
"existing.dampen",
r#"<dampen version="1.1" encoding="utf-8"><text value="Original" /></dampen>"#,
);
thread::sleep(Duration::from_millis(50));
let config = FileWatcherConfig {
watch_paths: vec![temp_dir.path().to_path_buf()],
debounce_ms: 100,
extension_filter: ".dampen".to_string(),
recursive: true,
};
let mut watcher = FileWatcher::new(config).expect("Failed to create watcher");
watcher
.watch(temp_dir.path().to_path_buf())
.expect("Failed to watch directory");
thread::sleep(Duration::from_millis(50));
let receiver = watcher.receiver();
while receiver.try_recv().is_ok() {}
modify_dampen_file(
&test_file,
r#"<dampen version="1.1" encoding="utf-8"><text value="Modified" /></dampen>"#,
);
wait_for_debounce();
let mut received_events = Vec::new();
while let Ok(path) = receiver.try_recv() {
received_events.push(path);
}
assert!(
!received_events.is_empty(),
"Expected to receive file modification event, but got none"
);
assert!(
received_events.iter().any(|p| p == &test_file),
"Expected event for {:?}, but received events for: {:?}",
test_file,
received_events
);
}
#[test]
fn test_debouncing_behavior() {
let temp_dir = setup_test_dir();
let config = FileWatcherConfig {
watch_paths: vec![temp_dir.path().to_path_buf()],
debounce_ms: 100,
extension_filter: ".dampen".to_string(),
recursive: true,
};
let mut watcher = FileWatcher::new(config).expect("Failed to create watcher");
watcher
.watch(temp_dir.path().to_path_buf())
.expect("Failed to watch directory");
thread::sleep(Duration::from_millis(100));
let test_file = temp_dir.path().join("debounce_test.dampen");
fs::write(
&test_file,
r#"<dampen version="1.1" encoding="utf-8"><text value="Original" /></dampen>"#,
)
.expect("Failed to create file");
thread::sleep(Duration::from_millis(150));
let receiver = watcher.receiver();
while receiver.try_recv().is_ok() {}
const NUM_MODIFICATIONS: usize = 10;
for i in 0..NUM_MODIFICATIONS {
modify_dampen_file(
&test_file,
&format!(
r#"<dampen version="1.1" encoding="utf-8"><text value="Change {}" /></dampen>"#,
i
),
);
thread::sleep(Duration::from_millis(5));
}
let timing = TestTiming::default();
thread::sleep(timing.wait_for_debounce());
let received_events = wait_for_events(receiver, timing.test_timeout);
assert!(
!received_events.is_empty(),
"Expected to receive at least one debounced event, but got none"
);
assert!(
received_events.len() < NUM_MODIFICATIONS,
"Expected debouncing to reduce {} modifications to fewer events, but got {} events. \
Debouncing may not be working correctly.",
NUM_MODIFICATIONS,
received_events.len()
);
let reduction_percent =
(1.0 - (received_events.len() as f64 / NUM_MODIFICATIONS as f64)) * 100.0;
assert!(
reduction_percent > 20.0,
"Expected at least 20% reduction from debouncing, but only got {:.1}% \
({} events from {} modifications). Debouncing may be variable due to OS timing.",
reduction_percent,
received_events.len(),
NUM_MODIFICATIONS
);
for event_path in &received_events {
assert_eq!(
event_path, &test_file,
"Received unexpected event for {:?}",
event_path
);
}
println!(
"✓ Debouncing working: {} modifications resulted in {} events (reduction: {:.1}%)",
NUM_MODIFICATIONS,
received_events.len(),
(1.0 - (received_events.len() as f64 / NUM_MODIFICATIONS as f64)) * 100.0
);
}
#[test]
fn test_extension_filtering() {
let temp_dir = setup_test_dir();
let config = FileWatcherConfig {
watch_paths: vec![temp_dir.path().to_path_buf()],
debounce_ms: 100,
extension_filter: ".dampen".to_string(),
recursive: true,
};
let mut watcher = FileWatcher::new(config).expect("Failed to create watcher");
watcher
.watch(temp_dir.path().to_path_buf())
.expect("Failed to watch directory");
thread::sleep(Duration::from_millis(50));
let receiver = watcher.receiver();
while receiver.try_recv().is_ok() {}
let dampen_file = temp_dir.path().join("should_detect.dampen");
fs::write(&dampen_file, "<dampen />").expect("Failed to create .dampen file");
let txt_file = temp_dir.path().join("should_ignore.txt");
fs::write(&txt_file, "Some text").expect("Failed to create .txt file");
wait_for_debounce();
let mut received_events = Vec::new();
while let Ok(path) = receiver.try_recv() {
received_events.push(path);
}
assert!(
received_events.iter().any(|p| p == &dampen_file),
"Expected to receive event for .dampen file"
);
assert!(
!received_events.iter().any(|p| p == &txt_file),
"Should not receive event for .txt file (should be filtered)"
);
}
#[test]
fn test_deleted_file_handling() {
let temp_dir = setup_test_dir();
let test_file = create_dampen_file(
&temp_dir,
"to_delete.dampen",
r#"<dampen version="1.1" encoding="utf-8"><text value="Will be deleted" /></dampen>"#,
);
thread::sleep(Duration::from_millis(50));
let config = FileWatcherConfig {
watch_paths: vec![temp_dir.path().to_path_buf()],
debounce_ms: 100,
extension_filter: ".dampen".to_string(),
recursive: true,
};
let mut watcher = FileWatcher::new(config).expect("Failed to create watcher");
watcher
.watch(temp_dir.path().to_path_buf())
.expect("Failed to watch directory");
thread::sleep(Duration::from_millis(50));
let receiver = watcher.receiver();
while receiver.try_recv().is_ok() {}
fs::remove_file(&test_file).expect("Failed to delete file");
wait_for_debounce();
let received_events: Vec<PathBuf> = receiver.try_iter().collect();
println!(
"✓ File deletion handled gracefully: {} events received for deleted file",
received_events.iter().filter(|p| p == &&test_file).count()
);
}
#[test]
fn test_file_change_detection_latency() {
let temp_dir = setup_test_dir();
let config = FileWatcherConfig {
watch_paths: vec![temp_dir.path().to_path_buf()],
debounce_ms: 10, extension_filter: ".dampen".to_string(),
recursive: true,
};
let mut watcher = FileWatcher::new(config).expect("Failed to create watcher");
watcher
.watch(temp_dir.path().to_path_buf())
.expect("Failed to watch directory");
thread::sleep(Duration::from_millis(100));
let test_file = temp_dir.path().join("latency_test.dampen");
fs::write(
&test_file,
r#"<dampen version="1.1" encoding="utf-8"><text value="Initial" /></dampen>"#,
)
.expect("Failed to create file");
thread::sleep(Duration::from_millis(50));
let receiver = watcher.receiver();
while receiver.try_recv().is_ok() {}
const NUM_MEASUREMENTS: usize = 5;
let mut latencies = Vec::new();
for i in 0..NUM_MEASUREMENTS {
let start = Instant::now();
modify_dampen_file(
&test_file,
&format!(
r#"<dampen version="1.1" encoding="utf-8"><text value="Measurement {}" /></dampen>"#,
i
),
);
let timeout = Duration::from_millis(200);
let mut received = false;
loop {
if let Ok(_path) = receiver.try_recv() {
let latency = start.elapsed();
latencies.push(latency);
received = true;
break;
}
if start.elapsed() > timeout {
break;
}
thread::sleep(Duration::from_millis(1));
}
assert!(
received,
"Measurement {}: Did not receive event within {}ms",
i,
timeout.as_millis()
);
thread::sleep(Duration::from_millis(50));
while receiver.try_recv().is_ok() {}
}
let average_latency = latencies.iter().sum::<Duration>() / latencies.len() as u32;
let min_latency = latencies.iter().min().unwrap();
let max_latency = latencies.iter().max().unwrap();
println!("\n=== File Change Detection Latency (FR-010, SC-003) ===");
println!("Measurements: {}", NUM_MEASUREMENTS);
println!(
"Average latency: {:.2}ms",
average_latency.as_secs_f64() * 1000.0
);
println!("Min latency: {:.2}ms", min_latency.as_secs_f64() * 1000.0);
println!("Max latency: {:.2}ms", max_latency.as_secs_f64() * 1000.0);
println!(
"All latencies: {:?}",
latencies
.iter()
.map(|d| format!("{:.2}ms", d.as_secs_f64() * 1000.0))
.collect::<Vec<_>>()
);
assert!(
average_latency.as_millis() < 100,
"FAILED: Average file change detection latency {:.2}ms exceeds 100ms requirement (FR-010, SC-003)",
average_latency.as_secs_f64() * 1000.0
);
assert!(
max_latency.as_millis() < 150,
"FAILED: Maximum latency {:.2}ms is too high (should be < 150ms)",
max_latency.as_secs_f64() * 1000.0
);
println!("✓ PASSED: File change detection latency meets <100ms requirement");
}