use super::*;
use std::sync::Mutex;
use std::sync::atomic::AtomicUsize;
use std::time::Instant;
use tempfile::{TempDir, tempdir};
fn workspace() -> (TempDir, PathBuf) {
let dir = tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
(dir, root)
}
fn wait_until<T>(timeout: Duration, mut probe: impl FnMut() -> Option<T>) -> Option<T> {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if let Some(v) = probe() {
return Some(v);
}
std::thread::sleep(Duration::from_millis(25));
}
probe()
}
const DETECT: Duration = Duration::from_secs(5);
#[test]
fn wait_until_times_out_to_none() {
assert!(wait_until(Duration::from_millis(20), || None::<()>).is_none());
}
#[derive(Clone)]
struct CountingRepaint(Arc<AtomicUsize>);
impl Repaint for CountingRepaint {
fn request(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn reconcile_creates_then_drops_watchers() {
let (_dir, root) = workspace();
let mut set = WatchSet::new();
assert!(set.is_empty());
set.reconcile(&[(root.clone(), RootKind::Workspace)]);
assert_eq!(set.len(), 1);
assert!(set.watches(&root, RootKind::Workspace));
set.reconcile(&[]);
assert!(set.is_empty());
assert!(!set.watches(&root, RootKind::Workspace));
}
#[test]
fn reconcile_keeps_surviving_watcher_instance() {
let (_dir, a) = workspace();
let (_dir_b, b) = workspace();
let mut set = WatchSet::new();
set.reconcile(&[(a.clone(), RootKind::Workspace)]);
set.reconcile(&[
(a.clone(), RootKind::Workspace),
(b.clone(), RootKind::Workspace),
]);
assert_eq!(set.len(), 2);
assert!(set.watches(&a, RootKind::Workspace));
assert!(set.watches(&b, RootKind::Workspace));
}
#[test]
fn reconcile_skips_absent_root_and_retries_later() {
let dir = tempdir().unwrap();
let missing = dir.path().join("not-yet");
let mut set = WatchSet::new();
set.reconcile(&[(missing.clone(), RootKind::Workspace)]);
assert!(set.is_empty());
std::fs::create_dir_all(&missing).unwrap();
set.reconcile(&[(missing.clone(), RootKind::Workspace)]);
assert_eq!(set.len(), 1);
}
#[test]
fn drain_dirty_reports_the_changed_root() {
let (_dir, root) = workspace();
let mut set = WatchSet::new();
set.reconcile(&[(root.clone(), RootKind::Workspace)]);
std::fs::create_dir_all(root.join("steps/abc/001")).unwrap();
let _ = set.drain_dirty(); std::fs::write(root.join("steps/abc/001/request.json"), b"{}").unwrap();
let dirty = wait_until(DETECT, || {
let d = set.drain_dirty();
(!d.is_empty()).then_some(d)
})
.expect("a change surfaces");
assert!(dirty.contains(&root));
}
#[test]
fn dirty_set_marks_and_drains() {
let dirty = DirtySet::default();
assert!(dirty.is_empty());
dirty.mark_all([PathBuf::from("/w/a"), PathBuf::from("/w/b")]);
assert!(!dirty.is_empty());
let drained = dirty.drain();
assert_eq!(drained.len(), 2);
assert!(dirty.is_empty(), "drain clears the set");
}
#[test]
fn pump_is_false_without_a_change_and_marks_on_change() {
let (_dir, root) = workspace();
let mut set = WatchSet::new();
set.reconcile(&[(root.clone(), RootKind::Workspace)]);
let watchset = Arc::new(Mutex::new(set));
let dirty = DirtySet::default();
let _ = watchset.lock().unwrap().drain_dirty();
assert!(!pump(&watchset, &dirty));
assert!(dirty.is_empty());
std::fs::create_dir_all(root.join("inbox/a")).unwrap();
std::fs::write(root.join("inbox/a/user-001.md"), b"hi").unwrap();
let marked = wait_until(DETECT, || pump(&watchset, &dirty).then_some(()));
assert!(marked.is_some());
assert!(dirty.drain().contains(&root));
}
#[test]
fn bridge_detects_a_disk_change_and_requests_repaint() {
let (_dir, root) = workspace();
let mut set = WatchSet::new();
set.reconcile(&[(root.clone(), RootKind::Workspace)]);
let watchset = Arc::new(Mutex::new(set));
let dirty = DirtySet::default();
let count = Arc::new(AtomicUsize::new(0));
let bridge = Bridge::spawn(
Arc::clone(&watchset),
dirty.clone(),
CountingRepaint(Arc::clone(&count)),
);
std::fs::create_dir_all(root.join("steps/abc/001")).unwrap();
std::fs::write(root.join("steps/abc/001/request.json"), b"{}").unwrap();
let seen = wait_until(DETECT, || (count.load(Ordering::Relaxed) > 0).then_some(()));
assert!(seen.is_some(), "the bridge requested a repaint");
assert!(!dirty.is_empty(), "the bridge marked the root dirty");
drop(bridge); }
#[test]
fn egui_repaint_requests_without_panicking() {
EguiRepaint(egui::Context::default()).request();
}