notify-rs/notify

How to write unit tests that exercise a watcher?

Opened this issue · 3 comments

I'm working on a project that monitors two directories and synchronizes files between them based on specific metadata. I'm attempting to write unit tests that validate the expected behavior by starting a watcher on a temp directory, manipulating the temp directory, and validating that some action was correctly taken. I'm running into an issue where it appears that the watcher does not receive events for modifications performed by the parent process.

The following demonstrates what I'm hoping to achieve, however the test case never returns because the event is never received. I've verified that the thread returns if I create the file manually, or start another test using the same directory. I'm open to suggestions if this is the wrong approach, or if there is a way to configure the watcher to get events from the parent process as well.

fn watch_directories(dir1: &Path) {
    let (tx, rx) = std::sync::mpsc::channel();
    let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();

    watcher.watch(&dir1, RecursiveMode::Recursive).unwrap();

    for res in rx {
        match res {
            Ok(_event) => {
                println!("Hit an event!!!");
                return; // Early return so thread exits
            }
            Err(error) => println!("Error: {error:?}"),
        }
    }
}

#[cfg(test)]
mod tests {

    use crate::watch_directories;
    use std::fs::{self, File};
    use tempfile::tempdir;

    #[test]
    fn file_created() -> Result<(), std::io::Error> {
        let tmp_dir = tempdir()?;
        let tmp_dir_path = tmp_dir.into_path();
        let tmp_dir_path_thread = tmp_dir_path.clone();

        // Spawn a new thread to watch the temp directory
        let thread_1 = std::thread::spawn(move || {
            watch_directories(tmp_dir_path_thread.as_path());
        });

        // Create a file and verify thread returns
        let file_path = tmp_dir_path.as_path().join("my-temporary-note.txt");
        let tmp_file = File::create(file_path)?;

        // Explicitly drop the file to hopefully trigger an event
        drop(tmp_file);

        // Wait for thread to join
        thread_1.join().unwrap();

        // Cleanup tmp_dir
        fs::remove_dir_all(tmp_dir_path).unwrap();

        Ok(())
    }
}

What OS is this running on ?

Ubuntu Linux x86

I ran into something tangential to this that I'll note for future readers.

https://stackoverflow.com/questions/47648301/inotify-odd-behavior-with-directory-creations

The gist is that if you are creating directories and using any of the kernel event based backends, if you create a file in one of the just-created directories, you may miss the event. Each new dir has to be registered with the kernel as a place to watch for events. So by the time notify gets back to the top of the loop to register new dirs to watch, the file creation event has already passed.

I'm not sure about files specifically, but maybe something similar is happening in your test? You create and drop the file so fast that the file isn't registered as a thing to watch for removal?

if event.mask.contains(EventMask::CREATE) {