diff --git a/CHANGELOG.md b/CHANGELOG.md index a62d5d52..aaf6a1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,17 +14,20 @@ v4 commits split out to branch `v4_maintenance` starting with `4.0.16` ## notify 7.0.0 (unreleased) -- CHANGE: raise MSRV to 1.72 [#569] [#610] +- CHANGE: raise MSRV to 1.72 [#569] [#610] **breaking** - CHANGE: move event type to notify-types crate [#559] - CHANGE: flatten serialization of events and use camelCase [#558] -- CHANGE: remove internal use of crossbeam-channels [#569] [#610] +- CHANGE: remove internal use of crossbeam channels [#569] [#610] +- CHANGE: rename feature `crossbeam` to `crossbeam-channel` and disable it by default [#610] **breaking** - CHANGE: upgrade mio to 1.0 [#623] +- CHANGE: add log statements [#499] - FIX: prevent UB with illegal instruction for the windows backend [#604] [#607] - FIX: on Linux report deleted directories correctly [#545] - FEATURE: enable kqueue on iOS [#533] - MISC: various minor doc updates and fixes [#535] [#536] [#543] [#565] [#592] [#595] - MISC: update inotify to 0.10 [#547] +[#499]: https://github.com/notify-rs/notify/pull/499 [#533]: https://github.com/notify-rs/notify/pull/533 [#535]: https://github.com/notify-rs/notify/pull/535 [#536]: https://github.com/notify-rs/notify/pull/536 @@ -43,13 +46,16 @@ v4 commits split out to branch `v4_maintenance` starting with `4.0.16` ## notify-types 1.0.0 (unreleased) -New crate containing public type definitions for the notify and debouncer crates. +New crate containing public type definitions for the notify and debouncer crates. [#559] - CHANGE: the serialization format for events has been changed to be easier to use in environments like JavaScript; - the old behavior can be restored using the new feature flag `serialization-compat-6` [#558] [#568] + the old behavior can be restored using the new feature flag **breaking**`serialization-compat-6` [#558] [#568] +- CHANGE: use instant crate (which provides an `Instant` type that works in Wasm environments) [#570] [#558]: https://github.com/notify-rs/notify/pull/558 +[#559]: https://github.com/notify-rs/notify/pull/559 [#568]: https://github.com/notify-rs/notify/pull/568 +[#570]: https://github.com/notify-rs/notify/pull/570 ## debouncer-full 0.4.0 (unreleased) diff --git a/Cargo.toml b/Cargo.toml index bf013516..9a5f48d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ kqueue = "1.0.8" libc = "0.2.4" log = "0.4.17" mio = { version = "1.0", features = ["os-ext"] } -mock_instant = "0.3.0" instant = "0.1.12" nix = "0.27.0" notify = { path = "notify" } diff --git a/notify-debouncer-full/Cargo.toml b/notify-debouncer-full/Cargo.toml index 5e334cc5..7d0e914f 100644 --- a/notify-debouncer-full/Cargo.toml +++ b/notify-debouncer-full/Cargo.toml @@ -15,7 +15,6 @@ repository.workspace = true [features] default = ["macos_fsevent"] serde = ["notify-types/serde"] -mock_instant = ["dep:mock_instant", "notify-types/mock_instant"] crossbeam-channel = ["dep:crossbeam-channel", "notify/crossbeam-channel"] macos_fsevent = ["notify/macos_fsevent"] macos_kqueue = ["notify/macos_kqueue"] @@ -28,12 +27,11 @@ crossbeam-channel = { workspace = true, optional = true } file-id.workspace = true walkdir.workspace = true log.workspace = true -mock_instant = { workspace = true, optional = true } [dev-dependencies] -notify-debouncer-full = { workspace = true, features = ["mock_instant"] } pretty_assertions.workspace = true rstest.workspace = true serde.workspace = true deser-hjson.workspace = true rand.workspace = true +tempfile.workspace = true diff --git a/notify-debouncer-full/src/lib.rs b/notify-debouncer-full/src/lib.rs index 3ef448d7..8a6fb72a 100644 --- a/notify-debouncer-full/src/lib.rs +++ b/notify-debouncer-full/src/lib.rs @@ -49,7 +49,7 @@ //! //! The following crate features can be turned on or off in your cargo dependency config: //! -//! - `crossbeam` passed down to notify, off by default +//! - `crossbeam-channel` passed down to notify, off by default //! - `serialization-compat-6` passed down to notify, off by default //! //! # Caveats @@ -57,6 +57,7 @@ //! As all file events are sourced from notify, the [known problems](https://docs.rs/notify/latest/notify/#known-problems) section applies here too. mod cache; +mod time; #[cfg(test)] mod testing; @@ -69,9 +70,11 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, - time::Duration, + time::{Duration, Instant}, }; +use time::now; + pub use cache::{FileIdCache, FileIdMap, NoCache, RecommendedCache}; pub use file_id; @@ -84,12 +87,6 @@ use notify::{ Error, ErrorKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, WatcherKind, }; -#[cfg(feature = "mock_instant")] -use mock_instant::Instant; - -#[cfg(not(feature = "mock_instant"))] -use std::time::Instant; - /// The set of requirements for watcher debounce event handling functions. /// /// # Example implementation @@ -124,7 +121,7 @@ where } } -#[cfg(feature = "crossbeam")] +#[cfg(feature = "crossbeam-channel")] impl DebounceEventHandler for crossbeam_channel::Sender { fn handle_event(&mut self, event: DebounceEventResult) { let _ = self.send(event); @@ -198,7 +195,7 @@ impl DebounceDataInner { /// Retrieve a vec of debounced events, removing them if not continuous pub fn debounced_events(&mut self) -> Vec { - let now = Instant::now(); + let now = now(); let mut events_expired = Vec::with_capacity(self.queues.len()); let mut queues_remaining = HashMap::with_capacity(self.queues.len()); @@ -211,6 +208,7 @@ impl DebounceDataInner { } } + // drain the entire queue, then process the expired events and re-add the rest // TODO: perfect fit for drain_filter https://github.com/rust-lang/rust/issues/59618 for (path, mut queue) in self.queues.drain() { let mut kind_index = HashMap::new(); @@ -249,13 +247,13 @@ impl DebounceDataInner { /// Returns all currently stored errors pub fn errors(&mut self) -> Vec { - let mut v = Vec::new(); - std::mem::swap(&mut v, &mut self.errors); - v + std::mem::take(&mut self.errors) } /// Add an error entry to re-send later on pub fn add_error(&mut self, error: Error) { + log::trace!("raw error: {error:?}"); + self.errors.push(error); } @@ -265,7 +263,7 @@ impl DebounceDataInner { if event.need_rescan() { self.cache.rescan(&self.roots); - self.rescan_event = Some(event.into()); + self.rescan_event = Some(DebouncedEvent { event, time: now() }); return; } @@ -277,7 +275,7 @@ impl DebounceDataInner { self.cache.add_path(path, recursive_mode); - self.push_event(event, Instant::now()); + self.push_event(event, now()); } EventKind::Modify(ModifyKind::Name(rename_mode)) => { match rename_mode { @@ -303,7 +301,7 @@ impl DebounceDataInner { } } EventKind::Remove(_) => { - self.push_remove_event(event, Instant::now()); + self.push_remove_event(event, now()); } EventKind::Other => { // ignore meta events @@ -315,7 +313,7 @@ impl DebounceDataInner { self.cache.add_path(path, recursive_mode); } - self.push_event(event, Instant::now()); + self.push_event(event, now()); } } } @@ -334,7 +332,7 @@ impl DebounceDataInner { } fn handle_rename_from(&mut self, event: Event) { - let time = Instant::now(); + let time = now(); let path = &event.paths[0]; // store event @@ -382,7 +380,7 @@ impl DebounceDataInner { self.push_rename_event(path, event, time); } else { // move in - self.push_event(event, Instant::now()); + self.push_event(event, now()); } self.rename_event = None; @@ -753,10 +751,11 @@ mod tests { use super::*; - use mock_instant::MockClock; use pretty_assertions::assert_eq; use rstest::rstest; + use tempfile::tempdir; use testing::TestCase; + use time::MockTime; #[rstest] fn state( @@ -805,16 +804,19 @@ mod tests { fs::read_to_string(Path::new(&format!("./test_cases/{file_name}.hjson"))).unwrap(); let mut test_case = deser_hjson::from_str::(&file_content).unwrap(); - MockClock::set_time(Duration::default()); - - let time = Instant::now(); + let time = now(); + MockTime::set_time(time); let mut state = test_case.state.into_debounce_data_inner(time); state.roots = vec![(PathBuf::from("/"), RecursiveMode::Recursive)]; + let mut prev_event_time = Duration::default(); + for event in test_case.events { + let event_time = Duration::from_millis(event.time); let event = event.into_debounced_event(time, None); - MockClock::set_time(event.time - time); + MockTime::advance(event_time - prev_event_time); + prev_event_time = event_time; state.add_event(event.event); } @@ -856,21 +858,20 @@ mod tests { "errors not as expected" ); - let backup_time = Instant::now().duration_since(time); + let backup_time = now(); let backup_queues = state.queues.clone(); for (delay, events) in expected_events { - MockClock::set_time(backup_time); + MockTime::set_time(backup_time); state.queues = backup_queues.clone(); match delay.as_str() { "none" => {} - "short" => MockClock::advance(Duration::from_millis(10)), - "long" => MockClock::advance(Duration::from_millis(100)), + "short" => MockTime::advance(Duration::from_millis(10)), + "long" => MockTime::advance(Duration::from_millis(100)), _ => { if let Ok(ts) = delay.parse::() { - let ts = time + Duration::from_millis(ts); - MockClock::set_time(ts - time); + MockTime::set_time(time + Duration::from_millis(ts)); } } } @@ -887,4 +888,26 @@ mod tests { ); } } + + #[test] + fn integration() -> Result<(), Box> { + let dir = tempdir()?; + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut debouncer = new_debouncer(Duration::from_millis(10), None, tx)?; + + debouncer.watch(dir.path(), RecursiveMode::Recursive)?; + + fs::write(dir.path().join("file.txt"), b"Lorem ipsum")?; + + let events = rx + .recv_timeout(Duration::from_secs(10)) + .expect("no events received") + .expect("received an error"); + + assert!(!events.is_empty(), "received empty event list"); + + Ok(()) + } } diff --git a/notify-debouncer-full/src/testing.rs b/notify-debouncer-full/src/testing.rs index 55ae706e..c4b3245d 100644 --- a/notify-debouncer-full/src/testing.rs +++ b/notify-debouncer-full/src/testing.rs @@ -1,11 +1,10 @@ use std::{ collections::{HashMap, VecDeque}, path::{Path, PathBuf}, - time::Duration, + time::{Duration, Instant}, }; use file_id::FileId; -use mock_instant::Instant; use notify::{ event::{ AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind, diff --git a/notify-debouncer-full/src/time.rs b/notify-debouncer-full/src/time.rs new file mode 100644 index 00000000..772aa06f --- /dev/null +++ b/notify-debouncer-full/src/time.rs @@ -0,0 +1,47 @@ +#[cfg(not(test))] +mod build { + use std::time::Instant; + + pub fn now() -> Instant { + Instant::now() + } +} + +#[cfg(not(test))] +pub use build::*; + +#[cfg(test)] +mod test { + use std::{ + sync::Mutex, + time::{Duration, Instant}, + }; + + thread_local! { + static NOW: Mutex> = Mutex::new(None); + } + + pub fn now() -> Instant { + let time = NOW.with(|now| *now.lock().unwrap()); + time.unwrap_or_else(|| Instant::now()) + } + + pub struct MockTime; + + impl MockTime { + pub fn set_time(time: Instant) { + NOW.with(|now| *now.lock().unwrap() = Some(time)); + } + + pub fn advance(delta: Duration) { + NOW.with(|now| { + if let Some(n) = &mut *now.lock().unwrap() { + *n += delta; + } + }); + } + } +} + +#[cfg(test)] +pub use test::*; diff --git a/notify-debouncer-mini/Cargo.toml b/notify-debouncer-mini/Cargo.toml index 37ec4723..e1f70788 100644 --- a/notify-debouncer-mini/Cargo.toml +++ b/notify-debouncer-mini/Cargo.toml @@ -25,3 +25,4 @@ notify.workspace = true notify-types.workspace = true crossbeam-channel = { workspace = true, optional = true } log.workspace = true +tempfile.workspace = true diff --git a/notify-debouncer-mini/src/lib.rs b/notify-debouncer-mini/src/lib.rs index d4470b3f..2a165102 100644 --- a/notify-debouncer-mini/src/lib.rs +++ b/notify-debouncer-mini/src/lib.rs @@ -45,7 +45,7 @@ //! //! The following crate features can be turned on or off in your cargo dependency config: //! -//! - `crossbeam` passed down to notify, off by default +//! - `crossbeam-channel` passed down to notify, off by default //! - `serde` enables serde support for events, off by default //! - `serialization-compat-6` passed down to notify, off by default //! @@ -152,7 +152,7 @@ where } } -#[cfg(feature = "crossbeam")] +#[cfg(feature = "crossbeam-channel")] impl DebounceEventHandler for crossbeam_channel::Sender { fn handle_event(&mut self, event: DebounceEventResult) { let _ = self.send(event); @@ -396,3 +396,39 @@ pub fn new_debouncer( let config = Config::default().with_timeout(timeout); new_debouncer_opt::(config, event_handler) } + +#[cfg(test)] +mod tests { + use super::*; + use notify::RecursiveMode; + use std::fs; + use tempfile::tempdir; + + #[test] + fn integration() -> Result<(), Box> { + let dir = tempdir()?; + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; + + debouncer + .watcher() + .watch(dir.path(), RecursiveMode::Recursive)?; + + let file_path = dir.path().join("file.txt"); + fs::write(&file_path, b"Lorem ipsum")?; + + let events = rx + .recv_timeout(Duration::from_secs(10)) + .expect("no events received") + .expect("received an error"); + + assert_eq!( + events, + vec![DebouncedEvent::new(file_path, DebouncedEventKind::Any)] + ); + + Ok(()) + } +} diff --git a/notify-types/Cargo.toml b/notify-types/Cargo.toml index 90cee241..d24028fe 100644 --- a/notify-types/Cargo.toml +++ b/notify-types/Cargo.toml @@ -18,7 +18,6 @@ serialization-compat-6 = [] [dependencies] serde = { workspace = true, optional = true } -mock_instant = { workspace = true, optional = true } instant.workspace = true [dev-dependencies] diff --git a/notify-types/src/debouncer_full.rs b/notify-types/src/debouncer_full.rs index 8266ffe6..9b32a71b 100644 --- a/notify-types/src/debouncer_full.rs +++ b/notify-types/src/debouncer_full.rs @@ -1,9 +1,5 @@ use std::ops::{Deref, DerefMut}; -#[cfg(feature = "mock_instant")] -use mock_instant::Instant; - -#[cfg(not(feature = "mock_instant"))] use instant::Instant; use crate::event::Event; @@ -37,21 +33,3 @@ impl DerefMut for DebouncedEvent { &mut self.event } } - -impl Default for DebouncedEvent { - fn default() -> Self { - Self { - event: Default::default(), - time: Instant::now(), - } - } -} - -impl From for DebouncedEvent { - fn from(event: Event) -> Self { - Self { - event, - time: Instant::now(), - } - } -} diff --git a/notify/src/lib.rs b/notify/src/lib.rs index 30b3b50a..ef063730 100644 --- a/notify/src/lib.rs +++ b/notify/src/lib.rs @@ -383,6 +383,10 @@ where #[cfg(test)] mod tests { + use std::{fs, time::Duration}; + + use tempfile::tempdir; + use super::*; #[test] @@ -408,4 +412,27 @@ mod tests { assert_debug_impl!(RecursiveMode); assert_debug_impl!(WatcherKind); } + + #[test] + fn integration() -> std::result::Result<(), Box> { + let dir = tempdir()?; + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut watcher = RecommendedWatcher::new(tx, Config::default())?; + + watcher.watch(dir.path(), RecursiveMode::Recursive)?; + + let file_path = dir.path().join("file.txt"); + fs::write(&file_path, b"Lorem ipsum")?; + + let event = rx + .recv_timeout(Duration::from_secs(10)) + .expect("no events received") + .expect("received an error"); + + assert_eq!(event.paths, vec![file_path]); + + Ok(()) + } }