notify icon indicating copy to clipboard operation
notify copied to clipboard

How to write unit tests that exercise a watcher?

Open christianheussy opened this issue 1 year ago • 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(())
    }
}

christianheussy avatar Sep 11 '23 15:09 christianheussy

What OS is this running on ?

0xpr03 avatar Oct 07 '23 18:10 0xpr03

Ubuntu Linux x86

christianheussy avatar Oct 07 '23 19:10 christianheussy

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? https://github.com/notify-rs/notify/blob/c3929ed114fbb0bc7457a9a498260461596b00ca/notify/src/inotify.rs#L273

sstadick avatar Jan 19 '24 02:01 sstadick