mod common;
use atap::{
Runtime, RuntimeError, TaskHandle,
fs::{Change, File},
};
use common::{TestPath, next_run, until_started, within};
use std::{
fs,
path::PathBuf,
thread,
time::{Duration, Instant},
};
const PATIENCE: Duration = Duration::from_secs(10);
fn until_parked<T>(handle: &TaskHandle<T>) {
until_started(handle, PATIENCE);
thread::sleep(Duration::from_millis(10));
}
fn touch(path: &PathBuf, bytes: &[u8]) {
use std::io::Write;
let mut file = fs::OpenOptions::new()
.append(true)
.open(path)
.expect("could not open the watched file");
file.write_all(bytes)
.expect("could not write the watched file");
}
#[test]
fn a_write_wakes_the_watch() {
let _ = Runtime::init();
let file = TestPath::new("written");
fs::write(file.path(), b"before").unwrap();
let handle = Runtime::task(File::watch(file.path())).spawn();
until_parked(&handle);
assert!(handle.is_running(), "the watch is waiting on the file");
touch(file.path(), b" and after");
let change = handle
.take_with_timeout(PATIENCE)
.expect("the watch never woke")
.expect("the watch failed");
println!("a write reported {:?}", change);
assert!(change.written(), "a write must report as a write");
assert!(change.extended(), "the file got longer");
assert!(!change.removed(), "the file is still there");
}
#[test]
fn a_watch_gives_up_when_asked_to() {
let _ = Runtime::init();
let file = TestPath::new("untouched");
fs::write(file.path(), b"still").unwrap();
let started = Instant::now();
let change = within(File::watch(file.path()), Duration::from_millis(200));
println!(
"giving up took {:?} and gave {:?}",
started.elapsed(),
change
);
assert_eq!(
change,
Err(RuntimeError::TimedOut),
"nothing touched the file"
);
assert!(started.elapsed() < PATIENCE, "giving up took too long");
}
#[test]
fn a_repeating_watch_reports_every_change() {
let _ = Runtime::init();
let file = TestPath::new("repeated");
fs::write(file.path(), b"one").unwrap();
let handle = Runtime::task(File::watch(file.path()))
.repeat()
.every(Duration::from_millis(50))
.count(3)
.spawn();
until_parked(&handle);
for round in 0..3 {
touch(file.path(), b" more");
let change = next_run(&handle, PATIENCE)
.unwrap_or_else(|| panic!("run {} never reported", round))
.expect("the watch failed");
println!("run {} reported {:?}", round, change);
assert!(change.written(), "run {} saw a write", round);
}
}
#[test]
fn a_change_between_runs_is_not_lost() {
let _ = Runtime::init();
let file = TestPath::new("between");
fs::write(file.path(), b"one").unwrap();
let gap = Duration::from_millis(400);
let handle = Runtime::task(File::watch(file.path()))
.repeat()
.every(gap)
.count(2)
.spawn();
until_parked(&handle);
touch(file.path(), b" two");
let first = next_run(&handle, PATIENCE)
.expect("the first run never reported")
.expect("the watch failed");
assert!(first.written(), "the first run saw its write");
touch(file.path(), b" three");
let second = next_run(&handle, PATIENCE)
.expect("the change between runs was lost")
.expect("the watch failed");
println!("the change between runs reported {:?}", second);
assert!(second.written(), "the second run found the write it missed");
}
#[test]
fn a_removed_file_reads_as_removed() {
let _ = Runtime::init();
let file = TestPath::new("removed");
fs::write(file.path(), b"here for now").unwrap();
let handle = Runtime::task(File::watch(file.path())).spawn();
until_parked(&handle);
fs::remove_file(file.path()).expect("could not remove the watched file");
let change = handle
.take_with_timeout(PATIENCE)
.expect("the watch never woke")
.expect("the watch failed");
println!("a removal reported {:?}", change);
assert!(change.removed(), "the last name the file had went");
assert!(!change.renamed(), "a removal is not a rename");
}
#[test]
fn a_repeating_watch_reports_a_removal_once() {
let _ = Runtime::init();
let file = TestPath::new("removed-once");
fs::write(file.path(), b"here for now").unwrap();
let handle = Runtime::task(File::watch(file.path()))
.repeat()
.every(Duration::from_millis(20))
.spawn();
until_parked(&handle);
fs::remove_file(file.path()).expect("could not remove the watched file");
let change = next_run(&handle, PATIENCE)
.expect("the removal was never reported")
.expect("the watch failed");
assert!(change.removed(), "the last name the file had went");
thread::sleep(Duration::from_millis(400));
assert_eq!(
handle.try_take(),
Err(RuntimeError::NotReady),
"a file that is still gone has not changed again",
);
handle.cancel();
}
#[test]
fn a_renamed_file_reads_as_renamed() {
let _ = Runtime::init();
let file = TestPath::new("renamed");
let moved = TestPath::new("renamed-to");
fs::write(file.path(), b"about to move").unwrap();
let handle = Runtime::task(File::watch(file.path())).spawn();
until_parked(&handle);
fs::rename(file.path(), moved.path()).expect("could not rename the watched file");
let change = handle
.take_with_timeout(PATIENCE)
.expect("the watch never woke")
.expect("the watch failed");
println!("a rename reported {:?}", change);
assert!(change.renamed(), "the path stopped leading to the file");
assert!(!change.removed(), "the file itself is still there");
}
#[test]
fn a_new_entry_wakes_a_directory_watch() {
let _ = Runtime::init();
let dir = TestPath::new("directory");
fs::create_dir(dir.path()).expect("could not make the watched directory");
let handle = Runtime::task(File::watch(dir.path())).spawn();
until_parked(&handle);
fs::write(dir.path().join("arrived.txt"), b"new").unwrap();
let change = handle
.take_with_timeout(PATIENCE)
.expect("the watch never woke")
.expect("the watch failed");
println!("a new entry reported {:?}", change);
assert!(
change.written(),
"an entry coming is a write to the directory"
);
}
#[test]
fn a_narrowed_watch_ignores_what_it_did_not_ask_for() {
let _ = Runtime::init();
let file = TestPath::new("narrowed");
fs::write(file.path(), b"before").unwrap();
let watch = File::watch(file.path()).only(Change::REMOVED);
let handle = Runtime::task(watch).spawn();
until_parked(&handle);
touch(file.path(), b" written to");
thread::sleep(Duration::from_millis(200));
assert!(
handle.is_running(),
"a write is not what this watch asked for"
);
fs::remove_file(file.path()).expect("could not remove the watched file");
let change = handle
.take_with_timeout(PATIENCE)
.expect("the watch never woke")
.expect("the watch failed");
println!("the narrowed watch reported {:?}", change);
assert_eq!(change, Change::REMOVED, "only the removal was asked for");
}
#[test]
fn a_path_that_is_not_there_says_so() {
let _ = Runtime::init();
let file = TestPath::new("never-made");
let change = Runtime::block(File::watch(file.path()));
println!("a missing path gave {:?}", change);
assert_eq!(
change,
Err(RuntimeError::CheckError(Some(libc::ENOENT))),
"there has to be something to watch",
);
}
#[test]
fn a_blocking_watch_waits_for_its_change() {
let _ = Runtime::init();
let file = TestPath::new("blocking");
fs::write(file.path(), b"before").unwrap();
let path = file.path().clone();
let writer = thread::spawn(move || {
thread::sleep(Duration::from_millis(150));
touch(&path, b" and after");
});
let change = within(File::watch(file.path()), PATIENCE).expect("the blocking watch failed");
writer.join().expect("the writing thread panicked");
println!("a blocking watch reported {:?}", change);
assert!(change.written(), "the write the other thread made");
}
#[test]
fn a_cancelled_watch_settles() {
let _ = Runtime::init();
let file = TestPath::new("cancelled");
fs::write(file.path(), b"never touched again").unwrap();
let handle = Runtime::task(File::watch(file.path())).spawn();
until_parked(&handle);
assert!(handle.is_running(), "the watch is parked");
let watching = handle.clone();
handle.cancel();
let deadline = Instant::now() + PATIENCE;
while !watching.settled() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(1));
}
println!("the cancelled watch is {:?}", watching.state());
assert!(watching.is_cancelled(), "a cancelled watch has to settle");
assert_eq!(
watching.try_take(),
Err(RuntimeError::Cancelled),
"a cancelled watch reports the cancel",
);
}
#[test]
fn a_watch_can_wait_for_a_path_to_appear() {
let _ = Runtime::init();
let file = TestPath::new("appearing");
let handle = Runtime::task(File::watch(file.path()).appear()).spawn();
until_parked(&handle);
assert!(
handle.is_running(),
"a missing path settled before it appeared"
);
fs::write(file.path(), b"here now").unwrap();
let change = handle
.take_with_timeout(PATIENCE)
.expect("the watch must settle")
.expect("the appearance must be reported");
assert!(change.created(), "the change was {change:?}");
assert!(!change.written());
}
#[test]
fn an_appeared_path_is_watched_after() {
let _ = Runtime::init();
let file = TestPath::new("appeared-then");
let handle = Runtime::task(File::watch(file.path()).appear())
.repeat()
.every(Duration::from_millis(200))
.spawn();
until_parked(&handle);
fs::write(file.path(), b"first").unwrap();
let created = next_run(&handle, PATIENCE)
.expect("the appearance")
.unwrap();
assert!(created.created());
thread::sleep(Duration::from_millis(300));
touch(file.path(), b" and more");
let written = next_run(&handle, PATIENCE).expect("the write").unwrap();
assert!(written.written(), "the change was {written:?}");
assert!(!written.created());
handle.cancel();
}
#[test]
fn appear_on_a_path_already_there_watches_it() {
let _ = Runtime::init();
let file = TestPath::new("already-there");
fs::write(file.path(), b"was here").unwrap();
let handle = Runtime::task(File::watch(file.path()).appear()).spawn();
until_parked(&handle);
touch(file.path(), b"!");
let change = handle.take_with_timeout(PATIENCE).unwrap().unwrap();
assert!(change.written());
assert!(!change.created());
}
#[test]
fn appear_needs_the_directory() {
let _ = Runtime::init();
let file = TestPath::new("no-parent");
assert_eq!(
within(File::watch(file.path().join("child")).appear(), PATIENCE),
Err(RuntimeError::CheckError(Some(libc::ENOENT)))
);
}