476 lines
14 KiB
Rust
476 lines
14 KiB
Rust
#![deny(warnings)]
|
|
|
|
|
|
// This test suite is incomplete and doesn't cover all available functionality.
|
|
// Contributions to improve test coverage would be highly appreciated!
|
|
|
|
use inotify::{
|
|
Inotify,
|
|
WatchMask
|
|
};
|
|
use std::fs::File;
|
|
use std::io::{
|
|
Write,
|
|
ErrorKind,
|
|
};
|
|
use std::os::unix::io::{
|
|
AsRawFd,
|
|
FromRawFd,
|
|
IntoRawFd,
|
|
};
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
#[cfg(feature = "stream")]
|
|
use maplit::hashmap;
|
|
#[cfg(feature = "stream")]
|
|
use inotify::EventMask;
|
|
#[cfg(feature = "stream")]
|
|
use rand::{thread_rng, prelude::SliceRandom};
|
|
#[cfg(feature = "stream")]
|
|
use std::sync::{Mutex, Arc};
|
|
#[cfg(feature = "stream")]
|
|
use futures_util::StreamExt;
|
|
|
|
|
|
#[test]
|
|
fn it_should_watch_a_file() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let mut inotify = Inotify::init().unwrap();
|
|
let watch = inotify.watches().add(&path, WatchMask::MODIFY).unwrap();
|
|
|
|
write_to(&mut file);
|
|
|
|
let mut buffer = [0; 1024];
|
|
let events = inotify.read_events_blocking(&mut buffer).unwrap();
|
|
|
|
let mut num_events = 0;
|
|
for event in events {
|
|
assert_eq!(watch, event.wd);
|
|
num_events += 1;
|
|
}
|
|
assert!(num_events > 0);
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
#[tokio::test]
|
|
async fn it_should_watch_a_file_async() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let inotify = Inotify::init().unwrap();
|
|
|
|
// Hold ownership of `watches` for this test, so that the underlying file descriptor has
|
|
// at least one reference to keep it alive, and we can inspect the WatchDescriptors below.
|
|
// Otherwise the `Weak<FdGuard>` contained in the WatchDescriptors will be invalidated
|
|
// when `inotify` is consumed by `into_event_stream()` and the EventStream is dropped
|
|
// during `await`.
|
|
let mut watches = inotify.watches();
|
|
|
|
let watch = watches.add(&path, WatchMask::MODIFY).unwrap();
|
|
|
|
write_to(&mut file);
|
|
|
|
let mut buffer = [0; 1024];
|
|
|
|
use futures_util::StreamExt;
|
|
let events = inotify
|
|
.into_event_stream(&mut buffer[..])
|
|
.unwrap()
|
|
.take(1)
|
|
.collect::<Vec<_>>()
|
|
.await;
|
|
|
|
let mut num_events = 0;
|
|
for event in events {
|
|
if let Ok(event) = event {
|
|
assert_eq!(watch, event.wd);
|
|
num_events += 1;
|
|
}
|
|
}
|
|
assert!(num_events > 0);
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
#[tokio::test]
|
|
async fn it_should_watch_a_file_from_eventstream_watches() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let inotify = Inotify::init().unwrap();
|
|
|
|
let mut buffer = [0; 1024];
|
|
|
|
use futures_util::StreamExt;
|
|
let stream = inotify.into_event_stream(&mut buffer[..]).unwrap();
|
|
|
|
// Hold ownership of `watches` for this test, so that the underlying file descriptor has
|
|
// at least one reference to keep it alive, and we can inspect the WatchDescriptors below.
|
|
// Otherwise the `Weak<FdGuard>` contained in the WatchDescriptors will be invalidated
|
|
// when `stream` is dropped during `await`.
|
|
let mut watches = stream.watches();
|
|
|
|
let watch = watches.add(&path, WatchMask::MODIFY).unwrap();
|
|
write_to(&mut file);
|
|
|
|
let events = stream
|
|
.take(1)
|
|
.collect::<Vec<_>>()
|
|
.await;
|
|
|
|
let mut num_events = 0;
|
|
for event in events {
|
|
if let Ok(event) = event {
|
|
assert_eq!(watch, event.wd);
|
|
num_events += 1;
|
|
}
|
|
}
|
|
assert!(num_events > 0);
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
#[tokio::test]
|
|
async fn it_should_watch_a_file_after_converting_back_from_eventstream() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let inotify = Inotify::init().unwrap();
|
|
|
|
let mut buffer = [0; 1024];
|
|
let stream = inotify.into_event_stream(&mut buffer[..]).unwrap();
|
|
let mut inotify = stream.into_inotify();
|
|
|
|
let watch = inotify.watches().add(&path, WatchMask::MODIFY).unwrap();
|
|
|
|
write_to(&mut file);
|
|
|
|
let events = inotify.read_events_blocking(&mut buffer).unwrap();
|
|
|
|
let mut num_events = 0;
|
|
for event in events {
|
|
assert_eq!(watch, event.wd);
|
|
num_events += 1;
|
|
}
|
|
assert!(num_events > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn it_should_return_immediately_if_no_events_are_available() {
|
|
let mut inotify = Inotify::init().unwrap();
|
|
|
|
let mut buffer = [0; 1024];
|
|
assert_eq!(inotify.read_events(&mut buffer).unwrap_err().kind(), ErrorKind::WouldBlock);
|
|
}
|
|
|
|
#[test]
|
|
fn it_should_convert_the_name_into_an_os_str() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let mut inotify = Inotify::init().unwrap();
|
|
inotify.watches().add(&path.parent().unwrap(), WatchMask::MODIFY).unwrap();
|
|
|
|
write_to(&mut file);
|
|
|
|
let mut buffer = [0; 1024];
|
|
let mut events = inotify.read_events_blocking(&mut buffer).unwrap();
|
|
|
|
if let Some(event) = events.next() {
|
|
assert_eq!(path.file_name(), event.name);
|
|
}
|
|
else {
|
|
panic!("Expected inotify event");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn it_should_set_name_to_none_if_it_is_empty() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let mut inotify = Inotify::init().unwrap();
|
|
inotify.watches().add(&path, WatchMask::MODIFY).unwrap();
|
|
|
|
write_to(&mut file);
|
|
|
|
let mut buffer = [0; 1024];
|
|
let mut events = inotify.read_events_blocking(&mut buffer).unwrap();
|
|
|
|
if let Some(event) = events.next() {
|
|
assert_eq!(event.name, None);
|
|
}
|
|
else {
|
|
panic!("Expected inotify event");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn it_should_not_accept_watchdescriptors_from_other_instances() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, _) = testdir.new_file();
|
|
|
|
let inotify = Inotify::init().unwrap();
|
|
let _ = inotify.watches().add(&path, WatchMask::ACCESS).unwrap();
|
|
|
|
let second_inotify = Inotify::init().unwrap();
|
|
let wd2 = second_inotify.watches().add(&path, WatchMask::ACCESS).unwrap();
|
|
|
|
assert_eq!(inotify.watches().remove(wd2).unwrap_err().kind(), ErrorKind::InvalidInput);
|
|
}
|
|
|
|
#[test]
|
|
fn watch_descriptors_from_different_inotify_instances_should_not_be_equal() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, _) = testdir.new_file();
|
|
|
|
let inotify_1 = Inotify::init()
|
|
.unwrap();
|
|
let inotify_2 = Inotify::init()
|
|
.unwrap();
|
|
|
|
let wd_1 = inotify_1
|
|
.watches()
|
|
.add(&path, WatchMask::ACCESS)
|
|
.unwrap();
|
|
let wd_2 = inotify_2
|
|
.watches()
|
|
.add(&path, WatchMask::ACCESS)
|
|
.unwrap();
|
|
|
|
// As far as inotify is concerned, watch descriptors are just integers that
|
|
// are scoped per inotify instance. This means that multiple instances will
|
|
// produce the same watch descriptor number, a case we want inotify-rs to
|
|
// detect.
|
|
assert!(wd_1 != wd_2);
|
|
}
|
|
|
|
#[test]
|
|
fn watch_descriptor_equality_should_not_be_confused_by_reused_fds() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, _) = testdir.new_file();
|
|
|
|
// When a new inotify instance is created directly after closing another
|
|
// one, it is possible that the file descriptor is reused immediately, and
|
|
// we end up with a new instance that has the same file descriptor as the
|
|
// old one.
|
|
// This is quite likely, but it doesn't happen every time. Therefore we may
|
|
// need a few tries until we find two instances where that is the case.
|
|
let (wd_1, inotify_2) = loop {
|
|
let inotify_1 = Inotify::init()
|
|
.unwrap();
|
|
|
|
let wd_1 = inotify_1
|
|
.watches()
|
|
.add(&path, WatchMask::ACCESS)
|
|
.unwrap();
|
|
let fd_1 = inotify_1.as_raw_fd();
|
|
|
|
inotify_1
|
|
.close()
|
|
.unwrap();
|
|
let inotify_2 = Inotify::init()
|
|
.unwrap();
|
|
|
|
if fd_1 == inotify_2.as_raw_fd() {
|
|
break (wd_1, inotify_2);
|
|
}
|
|
};
|
|
|
|
let wd_2 = inotify_2
|
|
.watches()
|
|
.add(&path, WatchMask::ACCESS)
|
|
.unwrap();
|
|
|
|
// The way we engineered this situation, both `WatchDescriptor` instances
|
|
// have the same fields. They still come from different inotify instances
|
|
// though, so they shouldn't be equal.
|
|
assert!(wd_1 != wd_2);
|
|
|
|
inotify_2
|
|
.close()
|
|
.unwrap();
|
|
|
|
// A little extra gotcha: If both inotify instances are closed, and the `Eq`
|
|
// implementation naively compares the weak pointers, both will be `None`,
|
|
// making them equal. Let's make sure this isn't the case.
|
|
assert!(wd_1 != wd_2);
|
|
}
|
|
|
|
#[test]
|
|
fn it_should_implement_raw_fd_traits_correctly() {
|
|
let fd = Inotify::init()
|
|
.expect("Failed to initialize inotify instance")
|
|
.into_raw_fd();
|
|
|
|
// If `IntoRawFd` has been implemented naively, `Inotify`'s `Drop`
|
|
// implementation will have closed the inotify instance at this point. Let's
|
|
// make sure this didn't happen.
|
|
let mut inotify = unsafe { <Inotify as FromRawFd>::from_raw_fd(fd) };
|
|
|
|
let mut buffer = [0; 1024];
|
|
if let Err(error) = inotify.read_events(&mut buffer) {
|
|
if error.kind() != ErrorKind::WouldBlock {
|
|
panic!("Failed to add watch: {}", error);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn it_should_watch_correctly_with_a_watches_clone() {
|
|
let mut testdir = TestDir::new();
|
|
let (path, mut file) = testdir.new_file();
|
|
|
|
let mut inotify = Inotify::init().unwrap();
|
|
let mut watches1 = inotify.watches();
|
|
let mut watches2 = watches1.clone();
|
|
let watch1 = watches1.add(&path, WatchMask::MODIFY).unwrap();
|
|
let watch2 = watches2.add(&path, WatchMask::MODIFY).unwrap();
|
|
|
|
// same path and same Inotify should return same descriptor
|
|
assert_eq!(watch1, watch2);
|
|
|
|
write_to(&mut file);
|
|
|
|
let mut buffer = [0; 1024];
|
|
let events = inotify.read_events_blocking(&mut buffer).unwrap();
|
|
|
|
let mut num_events = 0;
|
|
for event in events {
|
|
assert_eq!(watch2, event.wd);
|
|
num_events += 1;
|
|
}
|
|
assert!(num_events > 0);
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
#[tokio::test]
|
|
/// Testing if two files with the same name but different directories
|
|
/// (e.g. "file_a" and "another_dir/file_a") are distinguished when _randomly_
|
|
/// triggering a DELETE_SELF for the two files.
|
|
async fn it_should_distinguish_event_for_files_with_same_name() {
|
|
let mut testdir = TestDir::new();
|
|
let testdir_path = testdir.dir.path().to_owned();
|
|
let file_order = Arc::new(Mutex::new(vec!["file_a", "another_dir/file_a"]));
|
|
file_order.lock().unwrap().shuffle(&mut thread_rng());
|
|
let file_order_clone = file_order.clone();
|
|
|
|
let inotify = Inotify::init().expect("Failed to initialize inotify instance");
|
|
|
|
// creating file_a inside `TestDir.dir`
|
|
let (path_1, _) = testdir.new_file_with_name("file_a");
|
|
// creating a directory inside `TestDir.dir`
|
|
testdir.new_directory_with_name("another_dir");
|
|
// creating a file inside `TestDir.dir/another_dir`
|
|
let (path_2, _) = testdir.new_file_in_directory_with_name("another_dir", "file_a");
|
|
|
|
// watching both files for `DELETE_SELF`
|
|
let wd_1 = inotify.watches().add(&path_1, WatchMask::DELETE_SELF).unwrap();
|
|
let wd_2 = inotify.watches().add(&path_2, WatchMask::DELETE_SELF).unwrap();
|
|
|
|
let expected_ids = hashmap! {
|
|
wd_1.get_watch_descriptor_id() => "file_a",
|
|
wd_2.get_watch_descriptor_id() => "another_dir/file_a"
|
|
};
|
|
let mut buffer = [0; 1024];
|
|
|
|
let file_removal_handler = tokio::spawn(async move {
|
|
for file in file_order.lock().unwrap().iter() {
|
|
testdir.delete_file(file);
|
|
}
|
|
});
|
|
|
|
let event_handle = tokio::spawn(async move {
|
|
let mut events = inotify.into_event_stream(&mut buffer).unwrap();
|
|
while let Some(Ok(event)) = events.next().await {
|
|
if event.mask == EventMask::DELETE_SELF {
|
|
let id = event.wd.get_watch_descriptor_id();
|
|
let file = expected_ids.get(&id).unwrap();
|
|
let full_path = testdir_path.join(*file);
|
|
println!("file {:?} was deleted", full_path);
|
|
file_order_clone.lock().unwrap().retain(|&x| x != *file);
|
|
|
|
if file_order_clone.lock().unwrap().is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let () = event_handle.await.unwrap();
|
|
let () = file_removal_handler.await.unwrap();
|
|
}
|
|
|
|
struct TestDir {
|
|
dir: TempDir,
|
|
counter: u32,
|
|
}
|
|
|
|
impl TestDir {
|
|
fn new() -> TestDir {
|
|
TestDir {
|
|
dir: TempDir::new().unwrap(),
|
|
counter: 0,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
fn new_file_with_name(&mut self, file_name: &str) -> (PathBuf, File) {
|
|
self.counter += 1;
|
|
|
|
let path = self.dir.path().join(file_name);
|
|
let file = File::create(&path)
|
|
.unwrap_or_else(|error| panic!("Failed to create temporary file: {}", error));
|
|
|
|
(path, file)
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
fn delete_file(&mut self, relative_path_to_file: &str) {
|
|
let path = &self.dir.path().join(relative_path_to_file);
|
|
std::fs::remove_file(path).unwrap();
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
fn new_file_in_directory_with_name(
|
|
&mut self,
|
|
dir_name: &str,
|
|
file_name: &str,
|
|
) -> (PathBuf, File) {
|
|
self.counter += 1;
|
|
|
|
let path = self.dir.path().join(dir_name).join(file_name);
|
|
let file = File::create(&path)
|
|
.unwrap_or_else(|error| panic!("Failed to create temporary file: {}", error));
|
|
|
|
(path, file)
|
|
}
|
|
|
|
#[cfg(feature = "stream")]
|
|
fn new_directory_with_name(&mut self, dir_name: &str) -> PathBuf {
|
|
let path = self.dir.path().join(dir_name);
|
|
let () = std::fs::create_dir(&path).unwrap();
|
|
path.to_path_buf()
|
|
}
|
|
|
|
fn new_file(&mut self) -> (PathBuf, File) {
|
|
let id = self.counter;
|
|
self.counter += 1;
|
|
|
|
let path = self.dir.path().join("file-".to_string() + &id.to_string());
|
|
let file = File::create(&path)
|
|
.unwrap_or_else(|error| panic!("Failed to create temporary file: {}", error));
|
|
|
|
(path, file)
|
|
}
|
|
}
|
|
|
|
fn write_to(file: &mut File) {
|
|
file
|
|
.write(b"This should trigger an inotify event.")
|
|
.unwrap_or_else(|error|
|
|
panic!("Failed to write to file: {}", error)
|
|
);
|
|
}
|