copy tests from notify-debouncer-full as well (verbatim)

While at it, remove the docs as they are referring to the original source code.
Tests should help to understand how it is supposed to work.

Note that the copy is verbatim and modifications will happen in a separate commit,
along with the necessary license declarations.
This commit is contained in:
Sebastian Thiel 2024-05-24 13:30:21 +02:00
parent 86edb4627d
commit edd8d54950
No known key found for this signature in database
GPG Key ID: 9CB5EE7895E8268B
40 changed files with 1430 additions and 85 deletions

63
Cargo.lock generated
View File

@ -1228,6 +1228,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "deser-hjson"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e1ab99fef4d11b2de312a0650bbf312fb48aa11a00084f35b27bf8c57d4cad"
dependencies = [
"serde",
]
[[package]]
name = "diff"
version = "0.1.13"
@ -1847,6 +1856,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.30"
@ -2211,13 +2226,20 @@ dependencies = [
"anyhow",
"backoff",
"crossbeam-channel",
"deser-hjson",
"file-id",
"futures",
"gitbutler-core",
"gitbutler-watcher",
"gix",
"itertools 0.12.1",
"mock_instant",
"notify",
"parking_lot 0.12.1",
"pretty_assertions",
"rand 0.8.5",
"rstest",
"serde",
"thiserror",
"tokio",
"tokio-util",
@ -3986,6 +4008,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mock_instant"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9366861eb2a2c436c20b12c8dbec5f798cea6b47ad99216be0282942e2c81ea0"
[[package]]
name = "native-tls"
version = "0.2.11"
@ -5183,6 +5211,12 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rend"
version = "0.4.2"
@ -5369,6 +5403,35 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rstest"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605"
dependencies = [
"cfg-if",
"glob",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn 2.0.58",
"unicode-ident",
]
[[package]]
name = "rusqlite"
version = "0.29.0"

View File

@ -7,6 +7,9 @@ publish = false
[lib]
doctest = false
[features]
mock_instant = ["dep:mock_instant"]
[dependencies]
gitbutler-core.workspace = true
thiserror.workspace = true
@ -25,6 +28,16 @@ walkdir = "2.2.2"
crossbeam-channel = "0.5.12"
itertools = "0.12"
mock_instant = { version = "0.3.0", optional = true }
[dev-dependencies]
gitbutler-watcher = { path = ".", features = ["mock_instant"] }
pretty_assertions = "1.3.0"
rstest = "0.18"
serde = { version = "1.0.89", features = ["derive"] }
deser-hjson = "1.1.1"
rand = "0.8.5"
[lints.clippy]
all = "deny"
perf = "deny"

View File

@ -29,6 +29,10 @@
// DEALINGS IN THE SOFTWARE.
use std::ops::{Deref, DerefMut};
#[cfg(feature = "mock_instant")]
use mock_instant::Instant;
#[cfg(not(feature = "mock_instant"))]
use std::time::Instant;
use notify::Event;

View File

@ -27,70 +27,6 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//! A debouncer for [notify] that is optimized for ease of use.
//!
//! * Only emits a single `Rename` event if the rename `From` and `To` events can be matched
//! * Merges multiple `Rename` events
//! * Takes `Rename` events into account and updates paths for events that occurred before the rename event, but which haven't been emitted, yet
//! * Optionally keeps track of the file system IDs all files and stiches rename events together (FSevents, Windows)
//! * Emits only one `Remove` event when deleting a directory (inotify)
//! * Doesn't emit duplicate create events
//! * Doesn't emit `Modify` events after a `Create` event
//!
//! # Installation
//!
//! ```toml
//! [dependencies]
//! notify-debouncer-full = "0.3.1"
//! ```
//!
//! In case you want to select specific features of notify,
//! specify notify as dependency explicitly in your dependencies.
//! Otherwise you can just use the re-export of notify from debouncer-full.
//!
//! ```toml
//! notify-debouncer-full = "0.3.1"
//! notify = { version = "..", features = [".."] }
//! ```
//!
//! # Examples
//!
//! ```rust,no_run
//! # use std::path::Path;
//! # use std::time::Duration;
//! use notify_debouncer_full::{notify::*, new_debouncer, DebounceEventResult};
//!
//! // Select recommended watcher for debouncer.
//! // Using a callback here, could also be a channel.
//! let mut debouncer = new_debouncer(Duration::from_secs(2), None, |result: DebounceEventResult| {
//! match result {
//! Ok(events) => events.iter().for_each(|event| println!("{event:?}")),
//! Err(errors) => errors.iter().for_each(|error| println!("{error:?}")),
//! }
//! }).unwrap();
//!
//! // Add a path to be watched. All files and directories at that path and
//! // below will be monitored for changes.
//! debouncer.watcher().watch(Path::new("."), RecursiveMode::Recursive).unwrap();
//!
//! // Add the same path to the file ID cache. The cache uses unique file IDs
//! // provided by the file system and is used to stich together rename events
//! // in case the notification back-end doesn't emit rename cookies.
//! debouncer.cache().add_root(Path::new("."), RecursiveMode::Recursive);
//! ```
//!
//! # Features
//!
//! The following crate features can be turned on or off in your cargo dependency config:
//!
//! - `crossbeam` enabled by default, adds [`DebounceEventHandler`](DebounceEventHandler) support for crossbeam channels.
//! Also enables crossbeam-channel in the re-exported notify. You may want to disable this when using the tokio async runtime.
//! - `serde` enables serde support for events.
//!
//! # Caveats
//!
//! As all file events are sourced from notify, the [known problems](https://docs.rs/notify/latest/notify/#known-problems) section applies here too.
use std::{
collections::{HashMap, VecDeque},
path::PathBuf,
@ -98,9 +34,15 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::{Duration, Instant},
time::Duration,
};
#[cfg(feature = "mock_instant")]
use mock_instant::Instant;
#[cfg(not(feature = "mock_instant"))]
use std::time::Instant;
use file_id::FileId;
use notify::{
event::{ModifyKind, RemoveKind, RenameMode},
@ -114,26 +56,6 @@ mod event;
use cache::{FileIdCache, FileIdMap};
use event::DebouncedEvent;
/// The set of requirements for watcher debounce event handling functions.
///
/// # Example implementation
///
/// ```rust,no_run
/// # use notify::{Event, Result, EventHandler};
/// # use notify_debouncer_full::{DebounceEventHandler, DebounceEventResult};
///
/// /// Prints received events
/// struct EventPrinter;
///
/// impl DebounceEventHandler for EventPrinter {
/// fn handle_event(&mut self, result: DebounceEventResult) {
/// match result {
/// Ok(events) => events.iter().for_each(|event| println!("{event:?}")),
/// Err(errors) => errors.iter().for_each(|error| println!("{error:?}")),
/// }
/// }
/// }
/// ```
pub trait DebounceEventHandler: Send + 'static {
/// Handles an event.
fn handle_event(&mut self, event: DebounceEventResult);

View File

@ -0,0 +1,451 @@
use std::{
collections::{HashMap, VecDeque},
path::{Path, PathBuf},
time::Duration,
};
use file_id::FileId;
use mock_instant::Instant;
use notify::{
event::{
AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind,
RenameMode,
},
Error, ErrorKind, Event, EventKind, RecursiveMode,
};
use crate::{DebounceDataInner, DebouncedEvent, FileIdCache, Queue};
pub(crate) use schema::TestCase;
mod schema {
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct Error {
/// The error kind is parsed by `into_notify_error`
pub kind: String,
/// The error paths
#[serde(default)]
pub paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct Event {
/// The timestamp the event occurred
#[serde(default)]
pub time: u64,
/// The event kind is parsed by `into_notify_event`
pub kind: String,
/// The event paths
#[serde(default)]
pub paths: Vec<String>,
/// The event flags
#[serde(default)]
pub flags: Vec<String>,
/// The event tracker
pub tracker: Option<usize>,
/// The event info
pub info: Option<String>,
/// The file id for the file associated with the event
///
/// Only used for the rename event.
pub file_id: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct Queue {
pub events: Vec<Event>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct State {
/// Timeout for the debouncer
///
/// Only used for the initial state.
pub timeout: Option<u64>,
/// The event queues for each file
#[serde(default)]
pub queues: HashMap<String, Queue>,
/// Cached file ids
#[serde(default)]
pub cache: HashMap<String, u64>,
/// A map of file ids, used instead of accessing the file system
#[serde(default)]
pub file_system: HashMap<String, u64>,
/// Current rename event
pub rename_event: Option<Event>,
/// Current rescan event
pub rescan_event: Option<Event>,
/// Debounced events
///
/// Only used for the expected state.
#[serde(default)]
pub events: HashMap<String, Vec<Event>>,
/// Errors
#[serde(default)]
pub errors: Vec<Error>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct TestCase {
/// Initial state
pub state: State,
/// Events that are added during the test
#[serde(default)]
pub events: Vec<Event>,
/// Errors that are added during the test
#[serde(default)]
pub errors: Vec<Error>,
/// Expected state after the test
pub expected: State,
}
}
impl schema::Error {
pub fn into_notify_error(self) -> Error {
let kind = match &*self.kind {
"path-not-found" => ErrorKind::PathNotFound,
"watch-not-found" => ErrorKind::WatchNotFound,
"max-files-watch" => ErrorKind::MaxFilesWatch,
_ => panic!("unknown error type `{}`", self.kind),
};
let mut error = Error::new(kind);
for p in self.paths {
error = error.add_path(PathBuf::from(p));
}
error
}
}
impl schema::Event {
#[rustfmt::skip]
pub fn into_debounced_event(self, time: Instant, path: Option<&str>) -> DebouncedEvent {
let kind = match &*self.kind {
"any" => EventKind::Any,
"other" => EventKind::Other,
"access-any" => EventKind::Access(AccessKind::Any),
"access-read" => EventKind::Access(AccessKind::Read),
"access-open-any" => EventKind::Access(AccessKind::Open(AccessMode::Any)),
"access-open-execute" => EventKind::Access(AccessKind::Open(AccessMode::Execute)),
"access-open-read" => EventKind::Access(AccessKind::Open(AccessMode::Read)),
"access-open-write" => EventKind::Access(AccessKind::Open(AccessMode::Write)),
"access-open-other" => EventKind::Access(AccessKind::Open(AccessMode::Other)),
"access-close-any" => EventKind::Access(AccessKind::Close(AccessMode::Any)),
"access-close-execute" => EventKind::Access(AccessKind::Close(AccessMode::Execute)),
"access-close-read" => EventKind::Access(AccessKind::Close(AccessMode::Read)),
"access-close-write" => EventKind::Access(AccessKind::Close(AccessMode::Write)),
"access-close-other" => EventKind::Access(AccessKind::Close(AccessMode::Other)),
"access-other" => EventKind::Access(AccessKind::Other),
"create-any" => EventKind::Create(CreateKind::Any),
"create-file" => EventKind::Create(CreateKind::File),
"create-folder" => EventKind::Create(CreateKind::Folder),
"create-other" => EventKind::Create(CreateKind::Other),
"modify-any" => EventKind::Modify(ModifyKind::Any),
"modify-other" => EventKind::Modify(ModifyKind::Other),
"modify-data-any" => EventKind::Modify(ModifyKind::Data(DataChange::Any)),
"modify-data-size" => EventKind::Modify(ModifyKind::Data(DataChange::Size)),
"modify-data-content" => EventKind::Modify(ModifyKind::Data(DataChange::Content)),
"modify-data-other" => EventKind::Modify(ModifyKind::Data(DataChange::Other)),
"modify-metadata-any" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)),
"modify-metadata-access-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)),
"modify-metadata-write-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)),
"modify-metadata-permissions" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)),
"modify-metadata-ownership" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)),
"modify-metadata-extended" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)),
"modify-metadata-other" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other)),
"rename-any" => EventKind::Modify(ModifyKind::Name(RenameMode::Any)),
"rename-from" => EventKind::Modify(ModifyKind::Name(RenameMode::From)),
"rename-to" => EventKind::Modify(ModifyKind::Name(RenameMode::To)),
"rename-both" => EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
"rename-other" => EventKind::Modify(ModifyKind::Name(RenameMode::Other)),
"remove-any" => EventKind::Remove(RemoveKind::Any),
"remove-file" => EventKind::Remove(RemoveKind::File),
"remove-folder" => EventKind::Remove(RemoveKind::Folder),
"remove-other" => EventKind::Remove(RemoveKind::Other),
_ => panic!("unknown event type `{}`", self.kind),
};
let mut event = Event::new(kind);
for p in self.paths {
event = event.add_path(if p == "*" {
PathBuf::from(path.expect("cannot replace `*`"))
} else {
PathBuf::from(p)
});
if let Some(tracker) = self.tracker {
event = event.set_tracker(tracker);
}
if let Some(info) = &self.info {
event = event.set_info(info.as_str());
}
}
for f in self.flags {
let flag = match &*f {
"rescan" => Flag::Rescan,
_ => panic!("unknown event flag `{f}`"),
};
event = event.set_flag(flag);
}
DebouncedEvent { event, time: time + Duration::from_millis(self.time) }
}
}
impl schema::State {
pub(crate) fn into_debounce_data_inner(self, time: Instant) -> DebounceDataInner<TestCache> {
let queues = self
.queues
.into_iter()
.map(|(path, queue)| {
let queue = Queue {
events: queue
.events
.into_iter()
.map(|event| event.into_debounced_event(time, Some(&path)))
.collect::<VecDeque<_>>(),
};
(path.into(), queue)
})
.collect();
let cache = self
.cache
.into_iter()
.map(|(path, id)| {
let path = PathBuf::from(path);
let id = FileId::new_inode(id, id);
(path, id)
})
.collect::<HashMap<_, _>>();
let file_system = self
.file_system
.into_iter()
.map(|(path, id)| {
let path = PathBuf::from(path);
let id = FileId::new_inode(id, id);
(path, id)
})
.collect::<HashMap<_, _>>();
let cache = TestCache::new(cache, file_system);
let rename_event = self.rename_event.map(|e| {
let file_id = e.file_id.map(|id| FileId::new_inode(id, id));
let event = e.into_debounced_event(time, None);
(event, file_id)
});
let rescan_event = self
.rescan_event
.map(|e| e.into_debounced_event(time, None));
DebounceDataInner {
queues,
roots: Vec::new(),
cache,
rename_event,
rescan_event,
errors: Vec::new(),
timeout: Duration::from_millis(self.timeout.unwrap_or(50)),
}
}
}
#[derive(Debug, Clone)]
pub struct TestCache {
pub paths: HashMap<PathBuf, FileId>,
pub file_system: HashMap<PathBuf, FileId>,
}
impl TestCache {
pub fn new(paths: HashMap<PathBuf, FileId>, file_system: HashMap<PathBuf, FileId>) -> Self {
Self { paths, file_system }
}
}
impl FileIdCache for TestCache {
fn cached_file_id(&self, path: &Path) -> Option<&FileId> {
self.paths.get(path)
}
fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode) {
for (file_path, file_id) in &self.file_system {
if file_path == path
|| (file_path.starts_with(path) && recursive_mode == RecursiveMode::Recursive)
{
self.paths.insert(file_path.clone(), *file_id);
}
}
}
fn remove_path(&mut self, path: &Path) {
self.paths.remove(path);
}
}
#[cfg(test)]
mod tests {
use std::{fs, path::Path};
use super::*;
use mock_instant::MockClock;
use pretty_assertions::assert_eq;
use rstest::rstest;
use testing::TestCase;
#[rstest]
fn state(
#[values(
"add_create_event",
"add_create_event_after_remove_event",
"add_create_dir_event_twice",
"add_modify_content_event_after_create_event",
"add_rename_from_event",
"add_rename_from_event_after_create_event",
"add_rename_from_event_after_modify_event",
"add_rename_from_event_after_create_and_modify_event",
"add_rename_from_event_after_rename_from_event",
"add_rename_to_event",
"add_rename_to_dir_event",
"add_rename_from_and_to_event",
"add_rename_from_and_to_event_after_create",
"add_rename_from_and_to_event_after_rename",
"add_rename_from_and_to_event_after_modify_content",
"add_rename_from_and_to_event_override_created",
"add_rename_from_and_to_event_override_modified",
"add_rename_from_and_to_event_override_removed",
"add_rename_from_and_to_event_with_file_ids",
"add_rename_from_and_to_event_with_different_file_ids",
"add_rename_from_and_to_event_with_different_tracker",
"add_rename_both_event",
"add_remove_event",
"add_remove_event_after_create_event",
"add_remove_event_after_modify_event",
"add_remove_event_after_create_and_modify_event",
"add_remove_parent_event_after_remove_child_event",
"add_errors",
"emit_continuous_modify_content_events",
"emit_events_in_chronological_order",
"emit_events_with_a_prepended_rename_event",
"emit_close_events_only_once",
"emit_modify_event_after_close_event",
"emit_needs_rescan_event",
"read_file_id_without_create_event"
)]
file_name: &str,
) {
let file_content =
fs::read_to_string(Path::new(&format!("./test_cases/{file_name}.hjson"))).unwrap();
let mut test_case = deser_hjson::from_str::<TestCase>(&file_content).unwrap();
MockClock::set_time(Duration::default());
let time = Instant::now();
let mut state = test_case.state.into_debounce_data_inner(time);
state.roots = vec![(PathBuf::from("/"), RecursiveMode::Recursive)];
for event in test_case.events {
let event = event.into_debounced_event(time, None);
MockClock::set_time(event.time - time);
state.add_event(event.event);
}
for error in test_case.errors {
let error = error.into_notify_error();
state.add_error(error);
}
let expected_errors = std::mem::take(&mut test_case.expected.errors);
let expected_events = std::mem::take(&mut test_case.expected.events);
let expected_state = test_case.expected.into_debounce_data_inner(time);
assert_eq!(
state.queues, expected_state.queues,
"queues not as expected"
);
assert_eq!(
state.rename_event, expected_state.rename_event,
"rename event not as expected"
);
assert_eq!(
state.rescan_event, expected_state.rescan_event,
"rescan event not as expected"
);
assert_eq!(
state.cache.paths, expected_state.cache.paths,
"cache not as expected"
);
assert_eq!(
state
.errors
.iter()
.map(|e| format!("{:?}", e))
.collect::<Vec<_>>(),
expected_errors
.iter()
.map(|e| format!("{:?}", e.clone().into_notify_error()))
.collect::<Vec<_>>(),
"errors not as expected"
);
let backup_time = Instant::now().duration_since(time);
let backup_queues = state.queues.clone();
for (delay, events) in expected_events {
MockClock::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)),
_ => {
if let Ok(ts) = delay.parse::<u64>() {
let ts = time + Duration::from_millis(ts);
MockClock::set_time(ts - time);
}
}
}
let events = events
.into_iter()
.map(|event| event.into_debounced_event(time, None))
.collect::<Vec<_>>();
assert_eq!(
state.debounced_events(),
events,
"debounced events after a `{delay}` delay"
);
}
}
}

View File

@ -0,0 +1,20 @@
// https://github.com/spacedriveapp/spacedrive/blob/90a350946914be7f91ba692887ca03db659d530a/core/src/location/manager/watcher/macos.rs
//
// This is a MacOS specific event that happens when a folder is created trough Finder.
// It creates a folder but 2 events are triggered in FSEvents.
{
state: {}
events: [
{ kind: "create-folder", paths: ["/watch/dir"] }
{ kind: "create-folder", paths: ["/watch/dir"] }
]
expected: {
queues: {
/watch/dir: {
events: [
{ kind: "create-folder", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,29 @@
{
state: {
file_system: {
/watch/file: 1
}
}
events: [
{ kind: "create-any", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
]
}
}
cache: {
/watch/file: 1
}
events: {
none: []
short: []
long: [
{ kind: "create-any", paths: ["/watch/file"] }
]
}
}
}

View File

@ -0,0 +1,24 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "remove-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "create-any", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "remove-any", paths: ["*"] }
{ kind: "create-any", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,13 @@
{
state: {}
errors: [
{ kind: "max-files-watch" }
{ kind: "path-not-found", paths: ["/watch/file"] }
]
expected: {
errors: [
{ kind: "max-files-watch" }
{ kind: "path-not-found", paths: ["/watch/file"] }
]
}
}

View File

@ -0,0 +1,26 @@
// https://github.com/spacedriveapp/spacedrive/blob/90a350946914be7f91ba692887ca03db659d530a/core/src/location/manager/watcher/macos.rs
// MacOS emits a Create File and then an Update Content event when a file is created.
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "create-file", paths: ["*"] }
]
}
}
}
events: [
{ kind: "modify-data-content", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "create-file", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,15 @@
{
state: {}
events: [
{ kind: "remove-any", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "remove-any", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,16 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
{ kind: "modify-data-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "remove-any", paths: ["/watch/file"] }
]
expected: {}
}

View File

@ -0,0 +1,15 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "remove-any", paths: ["/watch/file"] }
]
expected: {}
}

View File

@ -0,0 +1,23 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "remove-any", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "remove-any", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,25 @@
{
state: {
cache: {
/watch: 1
/watch/parent: 2
/watch/parent/child: 3
}
}
events: [
{ kind: "remove-any", paths: ["/watch/parent/child"] }
{ kind: "remove-any", paths: ["/watch/parent"] }
]
expected: {
queues: {
/watch/parent: {
events: [
{ kind: "remove-any", paths: ["*"] }
]
}
}
cache: {
/watch: 1
}
}
}

View File

@ -0,0 +1,7 @@
{
state: {}
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"] }
]
expected: {}
}

View File

@ -0,0 +1,16 @@
{
state: {}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 }
]
}
}
}
}

View File

@ -0,0 +1,24 @@
{
state: {
queues: {
/watch/source: {
events: [
{ kind: "create-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "create-any", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,25 @@
{
state: {
queues: {
/watch/source: {
events: [
{ kind: "modify-data-content", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 }
{ kind: "modify-data-content", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,24 @@
{
state: {
queues: {
/watch/temp: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/temp"], tracker: 1, time: 1 }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/temp"], tracker: 2, time: 2 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 2, time: 3 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 2, time: 1 }
]
}
}
}
}

View File

@ -0,0 +1,24 @@
{
state: {
queues: {
/watch/target: {
events: [
{ kind: "create-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 }
]
}
}
}
}

View File

@ -0,0 +1,35 @@
{
state: {
queues: {
/watch/target: {
events: [
{ kind: "modify-data-any", paths: ["*"] }
]
}
}
cache: {
/watch/target: 1
/watch/source: 2
}
file_system: {
/watch/target: 2
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "remove-any", paths: ["*"], info: "override" }
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 }
]
}
}
cache: {
/watch/target: 2
}
}
}

View File

@ -0,0 +1,25 @@
{
state: {
queues: {
/watch/target: {
events: [
{ kind: "remove-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "remove-any", paths: ["*"] }
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 }
]
}
}
}
}

View File

@ -0,0 +1,31 @@
{
state: {
cache: {
/watch/source: 1
}
file_system: {
/watch/target: 2
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"] }
{ kind: "rename-to", paths: ["/watch/target"] }
]
expected: {
queues: {
/watch/source: {
events: [
{ kind: "rename-from", paths: ["*"] }
]
}
/watch/target: {
events: [
{ kind: "rename-to", paths: ["*"] }
]
}
}
cache: {
/watch/target: 2
}
}
}

View File

@ -0,0 +1,21 @@
{
state: {}
events: [
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 2 }
]
expected: {
queues: {
/watch/source: {
events: [
{ kind: "rename-from", paths: ["*"], tracker: 1 }
]
}
/watch/target: {
events: [
{ kind: "rename-to", paths: ["*"], tracker: 2 }
]
}
}
}
}

View File

@ -0,0 +1,26 @@
{
state: {
cache: {
/watch/source: 1
}
file_system: {
/watch/target: 1
}
}
events: [
{ kind: "rename-from", paths: ["/watch/source"] }
{ kind: "rename-to", paths: ["/watch/target"] }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"] }
]
}
}
cache: {
/watch/target: 1
}
}
}

View File

@ -0,0 +1,16 @@
{
state: {}
events: [
{ kind: "rename-from", paths: ["/watch/source"] }
]
expected: {
queues: {
/watch/source: {
events: [
{ kind: "rename-from", paths: ["*"] }
]
}
}
rename_event: { kind: "rename-from", paths: ["/watch/source"] }
}
}

View File

@ -0,0 +1,27 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
{ kind: "modify-data-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
{ kind: "modify-data-any", paths: ["*"] }
{ kind: "rename-from", paths: ["*"] }
]
}
}
rename_event: { kind: "rename-from", paths: ["/watch/file"] }
}
}

View File

@ -0,0 +1,25 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "create-any", paths: ["*"] }
{ kind: "rename-from", paths: ["*"] }
]
}
}
rename_event: { kind: "rename-from", paths: ["/watch/file"] }
}
}

View File

@ -0,0 +1,25 @@
{
state: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-any", paths: ["*"] }
]
}
}
}
events: [
{ kind: "rename-from", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-any", paths: ["*"] }
{ kind: "rename-from", paths: ["*"] }
]
}
}
rename_event: { kind: "rename-from", paths: ["/watch/file"] }
}
}

View File

@ -0,0 +1,30 @@
{
state: {
queues: {
/watch/file-a: {
events: [
{ kind: "rename-from", paths: ["*"] }
]
}
}
rename_event: { kind: "rename-from", paths: ["/watch/file-a"] }
}
events: [
{ kind: "rename-from", paths: ["/watch/file-b"] }
]
expected: {
queues: {
/watch/file-a: {
events: [
{ kind: "rename-from", paths: ["*"] }
]
}
/watch/file-b: {
events: [
{ kind: "rename-from", paths: ["*"] }
]
}
}
rename_event: { kind: "rename-from", paths: ["/watch/file-b"] }
}
}

View File

@ -0,0 +1,24 @@
{
state: {
file_system: {
/watch/parent: 1
/watch/parent/child: 2
}
}
events: [
{ kind: "rename-to", paths: ["/watch/parent"] }
]
expected: {
queues: {
/watch/parent: {
events: [
{ kind: "rename-to", paths: ["*"] }
]
}
}
cache: {
/watch/parent: 1
/watch/parent/child: 2
}
}
}

View File

@ -0,0 +1,15 @@
{
state: {}
events: [
{ kind: "rename-to", paths: ["/watch/target"] }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-to", paths: ["*"] }
]
}
}
}
}

View File

@ -0,0 +1,28 @@
{
state: {}
events: [
{ kind: "modify-data-any", paths: ["/watch/file"], time: 1 }
{ kind: "access-close-write", paths: ["/watch/file"], time: 2 }
{ kind: "modify-data-any", paths: ["/watch/file"], time: 3 }
{ kind: "access-close-write", paths: ["/watch/file"], time: 4 }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-any", paths: ["*"], time: 1 }
{ kind: "access-close-write", paths: ["*"], time: 2 }
{ kind: "modify-data-any", paths: ["*"], time: 3 }
{ kind: "access-close-write", paths: ["*"], time: 4 }
]
}
}
events: {
short: []
long: [
{ kind: "modify-data-any", paths: ["/watch/file"], time: 3 }
{ kind: "access-close-write", paths: ["/watch/file"], time: 4 }
]
}
}
}

View File

@ -0,0 +1,49 @@
{
state: {
timeout: 5
}
events: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 1 }
{ kind: "modify-data-content", paths: ["/watch/file"], time: 2 }
{ kind: "modify-data-content", paths: ["/watch/file"], time: 3 }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-content", paths: ["*"], time: 1 }
{ kind: "modify-data-content", paths: ["*"], time: 2 }
{ kind: "modify-data-content", paths: ["*"], time: 3 }
]
}
}
events: {
1: []
2: []
3: []
4: []
5: []
6: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 1 }
]
7: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 2 }
]
8: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 3 }
]
9: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 3 }
]
10: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 3 }
]
100: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 3 }
]
1000: [
{ kind: "modify-data-content", paths: ["/watch/file"], time: 3 }
]
}
}
}

View File

@ -0,0 +1,45 @@
{
state: {
timeout: 5
}
events: [
{ kind: "modify-data-content", paths: ["/watch/file-a"], time: 1 }
{ kind: "modify-data-content", paths: ["/watch/file-b"], time: 3 }
{ kind: "modify-data-content", paths: ["/watch/file-c"], time: 4 }
{ kind: "modify-metadata-write-time", paths: ["/watch/file-b"], time: 7 }
{ kind: "modify-metadata-write-time", paths: ["/watch/file-c"], time: 8 }
{ kind: "modify-metadata-write-time", paths: ["/watch/file-a"], time: 9 }
]
expected: {
queues: {
/watch/file-a: {
events: [
{ kind: "modify-data-content", paths: ["*"], time: 1 }
{ kind: "modify-metadata-write-time", paths: ["*"], time: 9 }
]
}
/watch/file-b: {
events: [
{ kind: "modify-data-content", paths: ["*"], time: 3 }
{ kind: "modify-metadata-write-time", paths: ["*"], time: 7 }
]
}
/watch/file-c: {
events: [
{ kind: "modify-data-content", paths: ["*"], time: 4 }
{ kind: "modify-metadata-write-time", paths: ["*"], time: 8 }
]
}
}
events: {
long: [
{ kind: "modify-data-content", paths: ["/watch/file-a"], time: 1 }
{ kind: "modify-data-content", paths: ["/watch/file-b"], time: 3 }
{ kind: "modify-data-content", paths: ["/watch/file-c"], time: 4 }
{ kind: "modify-metadata-write-time", paths: ["/watch/file-b"], time: 7 }
{ kind: "modify-metadata-write-time", paths: ["/watch/file-c"], time: 8 }
{ kind: "modify-metadata-write-time", paths: ["/watch/file-a"], time: 9 }
]
}
}
}

View File

@ -0,0 +1,36 @@
{
state: {
timeout: 5
}
events: [
{ kind: "modify-data-content", paths: ["/watch/source"], time: 1 }
{ kind: "modify-data-content", paths: ["/watch/source"], time: 4 }
{ kind: "rename-from", paths: ["/watch/source"], tracker: 1, time: 7 }
{ kind: "rename-to", paths: ["/watch/target"], tracker: 1, time: 8 }
{ kind: "modify-metadata-write-time", paths: ["/watch/target"], time: 9 }
]
expected: {
queues: {
/watch/target: {
events: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1, time: 7 }
{ kind: "modify-data-content", paths: ["*"], time: 1 }
{ kind: "modify-data-content", paths: ["*"], time: 4 }
{ kind: "modify-metadata-write-time", paths: ["*"], time: 9 }
]
}
}
events: {
11: []
12: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1, time: 7 }
{ kind: "modify-data-content", paths: ["/watch/target"], time: 4 }
]
14: [
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1, time: 7 }
{ kind: "modify-data-content", paths: ["/watch/target"], time: 4 }
{ kind: "modify-metadata-write-time", paths: ["/watch/target"], time: 9 }
]
}
}
}

View File

@ -0,0 +1,30 @@
{
state: {}
events: [
{ kind: "modify-data-any", paths: ["/watch/file"], time: 1 }
{ kind: "access-close-write", paths: ["/watch/file"], time: 2 }
{ kind: "modify-data-any", paths: ["/watch/file"], time: 3 }
{ kind: "access-close-write", paths: ["/watch/file"], time: 4 }
{ kind: "modify-data-any", paths: ["/watch/file"], time: 5 }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-any", paths: ["*"], time: 1 }
{ kind: "access-close-write", paths: ["*"], time: 2 }
{ kind: "modify-data-any", paths: ["*"], time: 3 }
{ kind: "access-close-write", paths: ["*"], time: 4 }
{ kind: "modify-data-any", paths: ["*"], time: 5 }
]
}
}
events: {
short: []
long: [
{ kind: "access-close-write", paths: ["/watch/file"], time: 4 }
{ kind: "modify-data-any", paths: ["/watch/file"], time: 5 }
]
}
}
}

View File

@ -0,0 +1,56 @@
{
state: {
queues: {
/watch/file-a: {
events: [
{ kind: "create-any", paths: ["*"], time: 1 }
]
}
/watch/file-b: {
events: [
{ kind: "create-any", paths: ["*"], time: 2 }
]
}
}
cache: {
/watch/file-a: 1
/watch/file-b: 2
}
file_system: {
/watch/file-a: 1
/watch/file-b: 2
/watch/file-c: 3
}
}
events: [
{ kind: "other", flags: ["rescan"], time: 3 }
]
expected: {
queues: {
/watch/file-a: {
events: [
{ kind: "create-any", paths: ["*"], time: 1 }
]
}
/watch/file-b: {
events: [
{ kind: "create-any", paths: ["*"], time: 2 }
]
}
}
rescan_event: { kind: "other", flags: ["rescan"], time: 3 }
cache: {
/watch/file-a: 1
/watch/file-b: 2
/watch/file-c: 3
}
events: {
short: []
long: [
{ kind: "create-any", paths: ["/watch/file-a"], time: 1 }
{ kind: "create-any", paths: ["/watch/file-b"], time: 2 }
{ kind: "other", flags: ["rescan"], time: 3 }
]
}
}
}

View File

@ -0,0 +1,22 @@
{
state: {
file_system: {
/watch/file: 1
}
}
events: [
{ kind: "modify-data-any", paths: ["/watch/file"] }
]
expected: {
queues: {
/watch/file: {
events: [
{ kind: "modify-data-any", paths: ["*"] }
]
}
}
cache: {
/watch/file: 1
}
}
}