Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to write unit tests that exercise a watcher? #532

Open
christianheussy opened this issue Sep 11, 2023 · 3 comments
Open

How to write unit tests that exercise a watcher? #532

christianheussy opened this issue Sep 11, 2023 · 3 comments

Comments

@christianheussy
Copy link

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(())
    }
}
@0xpr03
Copy link
Member

0xpr03 commented Oct 7, 2023

What OS is this running on ?

@christianheussy
Copy link
Author

Ubuntu Linux x86

@sstadick
Copy link

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) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants