#![deny(warnings)]
#[cfg(feature = "stream")]
use inotify::StreamExt;
use inotify::{EventMask, Inotify, WatchMask};
use std::fs::File;
use std::io::{ErrorKind, Write};
#[cfg(feature = "stream")]
use std::mem;
use std::os::fd::AsFd;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::path::PathBuf;
use tempfile::TempDir;
#[cfg(feature = "stream")]
use futures_util::FutureExt;
#[cfg(feature = "stream")]
use maplit::hashmap;
#[cfg(feature = "stream")]
use rand::{prelude::SliceRandom, thread_rng};
#[cfg(feature = "stream")]
use std::sync::{Arc, Mutex};
#[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();
let mut watches = inotify.watches();
let watch = watches.add(&path, WatchMask::MODIFY).unwrap();
write_to(&mut file);
let mut buffer = [0; 1024];
let events = inotify
.into_event_stream(&mut buffer[..])
.unwrap()
.take(1)
.collect::<Vec<_>>()
.await;
let mut num_events = 0;
for event in events.into_iter().flatten() {
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];
let stream = inotify.into_event_stream(&mut buffer[..]).unwrap();
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.into_iter().flatten() {
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();
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();
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();
assert!(wd_1 != wd_2);
inotify_2.close().unwrap();
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();
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();
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);
}
#[test]
fn watch_descriptor_equality_should_work_for_multiple_fds_of_same_instance() {
let mut testdir = TestDir::new();
let (path, _) = testdir.new_file();
let inotify = Inotify::init().unwrap();
let second_inotify_reference = Inotify::from(
inotify
.as_fd()
.try_clone_to_owned()
.expect("failed to clone fd of inotify"),
);
let first_watch = inotify.watches().add(&path, WatchMask::MODIFY).unwrap();
let second_watch = second_inotify_reference
.watches()
.add(&path, WatchMask::MODIFY)
.unwrap();
assert_eq!(
first_watch, second_watch,
"first and second watch descriptors should be equal"
);
}
#[cfg(feature = "stream")]
#[tokio::test]
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");
let (path_1, _) = testdir.new_file_with_name("file_a");
testdir.new_directory_with_name("another_dir");
let (path_2, _) = testdir.new_file_in_directory_with_name("another_dir", "file_a");
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();
}
#[cfg(feature = "stream")]
#[tokio::test]
async fn it_should_yield_all_events_with_small_buffer() {
let testdir = TestDir::new();
let dir_path = testdir.dir.path().to_owned();
let inotify = Inotify::init().expect("Failed to initialize inotify instance");
inotify
.watches()
.add(&dir_path, WatchMask::CREATE)
.expect("Failed to add watch");
let num_files = 3usize;
for i in 0..num_files {
let file_path = dir_path.join(format!("{}", i));
File::create(&file_path).expect("Failed to create file");
}
let event_struct_size = mem::size_of::<inotify_sys::inotify_event>();
let name_len_padded = 4; let single_event_size = event_struct_size + name_len_padded;
let buffer_size = single_event_size + (single_event_size - 1); let mut buffer = vec![0u8; buffer_size];
let mut stream = inotify.into_event_stream(&mut buffer[..]).unwrap();
let first_event = stream.next().await.unwrap().unwrap();
assert!(first_event.mask.contains(EventMask::CREATE));
let mut events = vec![first_event];
while let Some(result) = stream.next().now_or_never() {
match result {
Some(Ok(event)) => {
assert!(event.mask.contains(EventMask::CREATE));
events.push(event);
}
Some(Err(e)) => panic!("Error reading event: {}", e),
None => break, }
}
assert_eq!(
events.len(),
num_files,
"All events should yield with now_or_never"
);
}
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)
}
}
#[test]
fn it_should_receive_delete_event_when_file_is_deleted() {
let mut testdir = TestDir::new();
let (path, _file) = testdir.new_file();
let mut inotify = Inotify::init().unwrap();
let _watch = inotify
.watches()
.add(path.parent().unwrap(), WatchMask::DELETE)
.unwrap();
std::fs::remove_file(&path).unwrap();
let mut buffer = [0; 1024];
let mut events = inotify.read_events_blocking(&mut buffer).unwrap();
match events.next() {
Some(event) => assert_eq!(event.mask, EventMask::DELETE),
None => panic!("Expected event, got none."),
}
}
#[test]
fn it_should_receive_delete_event_watchee_is_deleted() {
let testdir = TestDir::new();
let path = testdir.dir.path();
let mut inotify = Inotify::init().unwrap();
let _watch = inotify.watches().add(path, WatchMask::DELETE_SELF).unwrap();
std::fs::remove_dir(path).unwrap();
let mut buffer = [0; 1024];
let mut events = inotify.read_events_blocking(&mut buffer).unwrap();
match events.next() {
Some(event) => assert_eq!(event.mask, EventMask::DELETE_SELF),
None => panic!("Expected event, got none."),
}
}
#[cfg(feature = "stream")]
#[tokio::test]
async fn it_should_receive_delete_event_when_file_is_deleted_async() {
use std::time::Duration;
use tokio::time::timeout;
let mut testdir = TestDir::new();
let (path, _file) = testdir.new_file();
let inotify = Inotify::init().unwrap();
let _watch = inotify
.watches()
.add(path.parent().unwrap(), WatchMask::DELETE)
.unwrap();
let mut buffer = [0; 1024];
let mut stream = inotify.into_event_stream(&mut buffer).unwrap();
std::fs::remove_file(&path).unwrap();
let event = timeout(Duration::from_secs(2), stream.next())
.await
.unwrap() .unwrap() .unwrap(); assert_eq!(event.mask, EventMask::DELETE);
}
fn write_to(file: &mut File) {
file.write_all(b"This should trigger an inotify event.")
.unwrap_or_else(|error| panic!("Failed to write to file: {}", error));
}