use super::*;
use crate::editor::Editor;
use std::time::{Duration, Instant};
use strop_core::Buffer;
use strop_worker_protocol::{LeaseId, NotifyHint, NotifyKind};
const TIMEOUT: Duration = Duration::from_secs(30);
fn subscribed_editor(dir: &std::path::Path) -> Editor {
let mut editor = Editor::new_in(Buffer::from_text(""), dir.to_path_buf());
editor.start_notifications();
let deadline = Instant::now() + TIMEOUT;
while editor.notify.subscription.is_none() {
assert!(Instant::now() < deadline, "the subscribe job settles");
editor.drain_notify();
std::thread::sleep(Duration::from_millis(5));
}
assert!(
editor.notify.push_coverage(),
"test namespaces watch natively"
);
editor
}
fn hint(editor: &Editor, relative: &str, kind: NotifyKind) -> Event {
Event::Notify {
subscription: editor.notify.subscription.unwrap(),
sequence: 0,
hints: vec![NotifyHint {
path: relative.as_bytes().to_vec(),
kind,
}],
}
}
fn pump_until(editor: &mut Editor, mut condition: impl FnMut(&Editor) -> bool) {
let deadline = Instant::now() + TIMEOUT;
while !condition(editor) {
assert!(Instant::now() < deadline, "the condition settles");
editor.drain_notify();
let _ = editor.wait_io();
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn a_hint_reloads_a_clean_buffer() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("note.txt");
std::fs::write(&path, "one\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(&path).unwrap();
assert_eq!(editor.buf().text(), "one\n");
std::fs::write(&path, "two\n").unwrap();
pump_until(&mut editor, |e| e.buf().text() == "two\n");
assert!(!editor.buf().dirty);
assert!(!editor.cur().external_change);
}
#[test]
fn a_hint_preserves_a_dirty_buffer_with_external_change_state() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("note.txt");
std::fs::write(&path, "one\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(&path).unwrap();
editor.feed_text("imine <esc>");
assert!(editor.buf().dirty);
std::fs::write(&path, "theirs\n").unwrap();
pump_until(&mut editor, |e| e.cur().external_change);
assert_eq!(editor.buf().text(), "mine one\n");
assert!(editor.buf().dirty, "edits are preserved");
}
#[test]
fn overflow_rescans_conservatively() {
let dir = tempfile::tempdir().unwrap();
let clean = dir.path().join("clean.txt");
let dirty = dir.path().join("dirty.txt");
std::fs::write(&clean, "clean-one\n").unwrap();
std::fs::write(&dirty, "dirty-one\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(&clean).unwrap();
let clean_doc = editor.current();
editor.open_fixture(&dirty).unwrap();
let dirty_doc = editor.current();
editor.feed_text("imine <esc>");
assert!(editor.buf().dirty);
std::fs::write(&clean, "clean-two\n").unwrap();
std::fs::write(&dirty, "dirty-two\n").unwrap();
let subscription = editor.notify.subscription.unwrap();
editor.notify.queue.push_event(Event::NotifyOverflow {
subscription,
sequence: 1,
});
editor.handle_notify();
pump_until(&mut editor, |e| {
!e.notify.pending()
&& e.doc(clean_doc).buf.text() == "clean-two\n"
&& e.doc(dirty_doc).external_change
});
assert_eq!(
editor.doc(dirty_doc).buf.text(),
"mine dirty-one\n",
"the dirty buffer kept its edits through the rescan"
);
}
#[test]
fn a_dead_generation_never_acts() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("note.txt");
std::fs::write(&path, "one\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(&path).unwrap();
let current = editor.notify.subscription.unwrap();
let stale = Subscription {
id: current.id,
generation: current.generation + 1,
};
std::fs::write(&path, "two\n").unwrap();
editor.notify.queue.push_event(Event::Notify {
subscription: stale,
sequence: 99,
hints: vec![NotifyHint {
path: b"note.txt".to_vec(),
kind: NotifyKind::Modified,
}],
});
editor.handle_notify();
assert!(
!editor.notify.pending(),
"no reload was spawned for the stale generation"
);
assert_eq!(editor.buf().text(), "one\n", "stale events never act");
assert!(!editor.cur().external_change);
}
#[test]
fn a_hint_reobserves_an_open_directory() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("before.txt"), "x\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(dir.path()).unwrap();
assert!(editor.directory().is_some(), "a directory buffer opened");
std::fs::write(dir.path().join("after.txt"), "y\n").unwrap();
let event = hint(&editor, "after.txt", NotifyKind::Created);
editor.notify.queue.push_event(event);
editor.handle_notify();
pump_until(&mut editor, |e| {
e.directory().is_some() && e.buf().text().to_string().contains("after.txt")
});
}
#[test]
fn a_removed_file_preserves_the_buffer() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("note.txt");
std::fs::write(&path, "one\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(&path).unwrap();
std::fs::remove_file(&path).unwrap();
let event = hint(&editor, "note.txt", NotifyKind::Removed);
editor.notify.queue.push_event(event);
editor.handle_notify();
pump_until(&mut editor, |e| e.cur().external_change);
assert_eq!(editor.buf().text(), "one\n", "content preserved");
assert_eq!(
editor.buf().path.as_deref(),
Some(path.as_path()),
"the binding never relocated"
);
}
#[test]
fn close_retires_the_subscription() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("note.txt");
std::fs::write(&path, "one\n").unwrap();
let mut editor = subscribed_editor(dir.path());
editor.open_fixture(&path).unwrap();
editor.finish_background_work();
assert!(
editor.notify.subscription.is_none(),
"unsubscribed at close"
);
std::fs::write(&path, "two\n").unwrap();
editor.drain_notify();
let _ = editor.wait_io();
assert_eq!(editor.buf().text(), "one\n", "late events never apply");
}
#[test]
fn worker_loss_reestablishes_with_a_fresh_generation() {
let dir = tempfile::tempdir().unwrap();
let mut editor = subscribed_editor(dir.path());
let before = editor.notify.subscription.unwrap();
editor.notify.session = Some(Session {
incarnation: u64::MAX,
lease: LeaseId(u64::MAX),
});
editor.notify_observe_lease();
assert!(
editor.notify.subscription.is_none(),
"the dead session's subscription is dropped"
);
assert!(
editor.notify.subscribing || editor.notify.subscription.is_some(),
"coverage is reestablished"
);
let deadline = Instant::now() + TIMEOUT;
while editor.notify.subscription.is_none() {
assert!(Instant::now() < deadline, "the resubscribe settles");
editor.drain_notify();
std::thread::sleep(Duration::from_millis(5));
}
let after = editor.notify.subscription.unwrap();
assert_ne!(
before, after,
"reinstallation enters with a bumped identity"
);
}