mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-10-06 08:58:11 +03:00
Merge pull request #3850 from Byron/review-and-refactor
Watcher adjustments
This commit is contained in:
commit
319550d7b5
63
Cargo.lock
generated
63
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -1,3 +1,33 @@
|
||||
// Note that this file contains substantial portions of code
|
||||
// from https://github.com/notify-rs/notify/blob/main/notify-debouncer-full/src/cache.rs,
|
||||
// and what follows is a reproduction of its license.
|
||||
//
|
||||
// Copyright (c) 2023 Notify Contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any
|
||||
// person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the
|
||||
// Software without restriction, including without
|
||||
// limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software
|
||||
// is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
@ -19,7 +49,7 @@ pub trait FileIdCache {
|
||||
/// Add a new path to the cache or update its value.
|
||||
///
|
||||
/// This will be called if a new file or directory is created or if an existing file is overridden.
|
||||
fn add_path(&mut self, path: &Path);
|
||||
fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode);
|
||||
|
||||
/// Remove a path from the cache.
|
||||
///
|
||||
@ -29,7 +59,11 @@ pub trait FileIdCache {
|
||||
/// Re-scan all paths.
|
||||
///
|
||||
/// This will be called if the notification back-end has dropped events.
|
||||
fn rescan(&mut self);
|
||||
fn rescan(&mut self, roots: &[(PathBuf, RecursiveMode)]) {
|
||||
for (root, recursive_mode) in roots {
|
||||
self.add_path(root, *recursive_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache to hold the file system IDs of all watched files.
|
||||
@ -39,39 +73,9 @@ pub trait FileIdCache {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileIdMap {
|
||||
paths: HashMap<PathBuf, FileId>,
|
||||
roots: Vec<(PathBuf, RecursiveMode)>,
|
||||
}
|
||||
|
||||
impl FileIdMap {
|
||||
/// Construct an empty cache.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Add a path to the cache.
|
||||
///
|
||||
/// If `recursive_mode` is `Recursive`, all children will be added to the cache as well
|
||||
/// and all paths will be kept up-to-date in case of changes like new files being added,
|
||||
/// files being removed or renamed.
|
||||
#[allow(dead_code)]
|
||||
pub fn add_root(&mut self, path: impl Into<PathBuf>, recursive_mode: RecursiveMode) {
|
||||
let path = path.into();
|
||||
|
||||
self.roots.push((path.clone(), recursive_mode));
|
||||
|
||||
self.add_path(&path);
|
||||
}
|
||||
|
||||
/// Remove a path form the cache.
|
||||
///
|
||||
/// If the path was added with `Recursive` mode, all children will also be removed from the cache.
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_root(&mut self, path: impl AsRef<Path>) {
|
||||
self.roots.retain(|(root, _)| !root.starts_with(&path));
|
||||
|
||||
self.remove_path(path.as_ref());
|
||||
}
|
||||
|
||||
fn dir_scan_depth(is_recursive: bool) -> usize {
|
||||
if is_recursive {
|
||||
usize::max_value()
|
||||
@ -86,18 +90,8 @@ impl FileIdCache for FileIdMap {
|
||||
self.paths.get(path)
|
||||
}
|
||||
|
||||
fn add_path(&mut self, path: &Path) {
|
||||
let is_recursive = self
|
||||
.roots
|
||||
.iter()
|
||||
.find_map(|(root, recursive_mode)| {
|
||||
if path.starts_with(root) {
|
||||
Some(*recursive_mode == RecursiveMode::Recursive)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode) {
|
||||
let is_recursive = recursive_mode == RecursiveMode::Recursive;
|
||||
|
||||
for (path, file_id) in WalkDir::new(path)
|
||||
.follow_links(true)
|
||||
@ -116,27 +110,4 @@ impl FileIdCache for FileIdMap {
|
||||
fn remove_path(&mut self, path: &Path) {
|
||||
self.paths.retain(|p, _| !p.starts_with(path));
|
||||
}
|
||||
|
||||
fn rescan(&mut self) {
|
||||
for (root, _) in self.roots.clone() {
|
||||
self.add_path(&root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation of the `FileIdCache` trait that doesn't hold any data.
|
||||
///
|
||||
/// This pseudo cache can be used to disable the file tracking using file system IDs.
|
||||
pub struct NoCache;
|
||||
|
||||
impl FileIdCache for NoCache {
|
||||
fn cached_file_id(&self, _path: &Path) -> Option<&FileId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn add_path(&mut self, _path: &Path) {}
|
||||
|
||||
fn remove_path(&mut self, _path: &Path) {}
|
||||
|
||||
fn rescan(&mut self) {}
|
||||
}
|
77
crates/gitbutler-watcher/src/debouncer/event.rs
Normal file
77
crates/gitbutler-watcher/src/debouncer/event.rs
Normal file
@ -0,0 +1,77 @@
|
||||
// Note that this file contains substantial portions of code
|
||||
// from https://github.com/notify-rs/notify/blob/main/notify-types/src/debouncer_full.rs,
|
||||
// and what follows is a reproduction of its license.
|
||||
//
|
||||
// Copyright (c) 2023 Notify Contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any
|
||||
// person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the
|
||||
// Software without restriction, including without
|
||||
// limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software
|
||||
// is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// 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;
|
||||
|
||||
/// A debounced event is emitted after a short delay.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DebouncedEvent {
|
||||
/// The original event.
|
||||
pub event: Event,
|
||||
|
||||
/// The time at which the event occurred.
|
||||
pub time: Instant,
|
||||
}
|
||||
|
||||
impl DebouncedEvent {
|
||||
pub fn new(event: Event, time: Instant) -> Self {
|
||||
Self { event, time }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DebouncedEvent {
|
||||
type Target = Event;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.event
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DebouncedEvent {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.event
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Event> for DebouncedEvent {
|
||||
fn from(event: Event) -> Self {
|
||||
Self {
|
||||
event,
|
||||
time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +1,33 @@
|
||||
//! 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.
|
||||
|
||||
// Note that this file contains substantial portions of code
|
||||
// from https://github.com/notify-rs/notify/blob/main/notify-debouncer-full/src/lib.rs,
|
||||
// and what follows is a reproduction of its license.
|
||||
//
|
||||
// Copyright (c) 2023 Notify Contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any
|
||||
// person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the
|
||||
// Software without restriction, including without
|
||||
// limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software
|
||||
// is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
path::PathBuf,
|
||||
@ -69,41 +35,30 @@ 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},
|
||||
Error, ErrorKind, Event, EventKind, RecommendedWatcher, Watcher,
|
||||
Error, ErrorKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
debouncer_cache::{FileIdCache, FileIdMap},
|
||||
debouncer_event::DebouncedEvent,
|
||||
};
|
||||
pub(crate) mod cache;
|
||||
mod event;
|
||||
#[cfg(test)]
|
||||
mod testing;
|
||||
|
||||
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);
|
||||
@ -169,6 +124,7 @@ impl Queue {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DebounceDataInner<T> {
|
||||
queues: HashMap<PathBuf, Queue>,
|
||||
roots: Vec<(PathBuf, RecursiveMode)>,
|
||||
cache: T,
|
||||
rename_event: Option<(DebouncedEvent, Option<FileId>)>,
|
||||
rescan_event: Option<DebouncedEvent>,
|
||||
@ -180,6 +136,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
pub(crate) fn new(cache: T, timeout: Duration) -> Self {
|
||||
Self {
|
||||
queues: HashMap::new(),
|
||||
roots: Vec::new(),
|
||||
cache,
|
||||
rename_event: None,
|
||||
rescan_event: None,
|
||||
@ -206,7 +163,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
for (path, mut queue) in self.queues.drain() {
|
||||
let mut kind_index = HashMap::new();
|
||||
|
||||
tracing::debug!("Checking path: {:?}", path);
|
||||
tracing::trace!("Checking path: {:?}", path);
|
||||
while let Some(event) = queue.events.pop_front() {
|
||||
if now.saturating_duration_since(event.time) >= self.timeout {
|
||||
// remove previous event of the same kind
|
||||
@ -224,7 +181,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
|
||||
events_expired.push(event);
|
||||
} else if flush_all {
|
||||
tracing::debug!("Flushing event! {:?}", event.event);
|
||||
tracing::trace!("Flushing event! {:?}", event.event);
|
||||
events_expired.push(event);
|
||||
} else {
|
||||
queue.events.push_front(event);
|
||||
@ -250,7 +207,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
});
|
||||
|
||||
for event in &events_expired {
|
||||
tracing::debug!("Dispatching event: {:?}", event.event);
|
||||
tracing::trace!("Dispatching event: {:?}", event.event);
|
||||
}
|
||||
|
||||
events_expired
|
||||
@ -270,10 +227,10 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
|
||||
/// Add new event to debouncer cache
|
||||
pub fn add_event(&mut self, event: Event) {
|
||||
tracing::debug!("Received event: {:?}", event);
|
||||
tracing::trace!("Received event: {:?}", event);
|
||||
|
||||
if event.need_rescan() {
|
||||
self.cache.rescan();
|
||||
self.cache.rescan(&self.roots);
|
||||
self.rescan_event = Some(event.into());
|
||||
return;
|
||||
}
|
||||
@ -282,8 +239,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
|
||||
match &event.kind {
|
||||
EventKind::Create(_) => {
|
||||
self.cache.add_path(path);
|
||||
|
||||
self.cache.add_path(path, self.recursive_mode(path));
|
||||
self.push_event(event, Instant::now());
|
||||
}
|
||||
EventKind::Modify(ModifyKind::Name(rename_mode)) => {
|
||||
@ -317,7 +273,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
}
|
||||
_ => {
|
||||
if self.cache.cached_file_id(path).is_none() {
|
||||
self.cache.add_path(path);
|
||||
self.cache.add_path(path, self.recursive_mode(path));
|
||||
}
|
||||
|
||||
self.push_event(event, Instant::now());
|
||||
@ -325,6 +281,19 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_mode(&self, path: &Path) -> RecursiveMode {
|
||||
self.roots
|
||||
.iter()
|
||||
.find_map(|(root, recursive_mode)| {
|
||||
if path.starts_with(root) {
|
||||
Some(*recursive_mode)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(RecursiveMode::NonRecursive)
|
||||
}
|
||||
|
||||
fn handle_rename_from(&mut self, event: Event) {
|
||||
let time = Instant::now();
|
||||
let path = &event.paths[0];
|
||||
@ -339,7 +308,8 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
}
|
||||
|
||||
fn handle_rename_to(&mut self, event: Event) {
|
||||
self.cache.add_path(&event.paths[0]);
|
||||
let path = &event.paths[0];
|
||||
self.cache.add_path(path, self.recursive_mode(path));
|
||||
|
||||
let trackers_match = self
|
||||
.rename_event
|
||||
@ -359,7 +329,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
|
||||
.and_then(|(_, id)| id.as_ref())
|
||||
.and_then(|from_file_id| {
|
||||
self.cache
|
||||
.cached_file_id(&event.paths[0])
|
||||
.cached_file_id(path)
|
||||
.map(|to_file_id| from_file_id == to_file_id)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@ -534,15 +504,36 @@ impl<T: Watcher, C: FileIdCache> Debouncer<T, C> {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn add_root(&mut self, path: impl Into<PathBuf>, recursive_mode: RecursiveMode) {
|
||||
let path = path.into();
|
||||
|
||||
let mut data = self.data.lock();
|
||||
|
||||
// skip, if the root has already been added
|
||||
if data.roots.iter().any(|(p, _)| p == &path) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.roots.push((path.clone(), recursive_mode));
|
||||
|
||||
data.cache.add_path(&path, recursive_mode);
|
||||
}
|
||||
|
||||
// Note that code for unwatching/remove_root is available in the history.
|
||||
pub fn watch(
|
||||
&mut self,
|
||||
path: impl AsRef<Path>,
|
||||
recursive_mode: RecursiveMode,
|
||||
) -> notify::Result<()> {
|
||||
self.watcher.watch(path.as_ref(), recursive_mode)?;
|
||||
self.add_root(path.as_ref(), recursive_mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Indicates that on the next tick of the debouncer thread, all events should be emitted.
|
||||
pub fn flush_nonblocking(&self) {
|
||||
self.flush.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Access to the internally used notify Watcher backend
|
||||
pub fn watcher(&mut self) -> &mut T {
|
||||
&mut self.watcher
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Watcher, C: FileIdCache> Drop for Debouncer<T, C> {
|
||||
@ -632,7 +623,7 @@ pub fn new_debouncer_opt<F: DebounceEventHandler, T: Watcher, C: FileIdCache + S
|
||||
}
|
||||
if !send_data.is_empty() {
|
||||
if should_flush {
|
||||
tracing::debug!("Flushed {} events", send_data.len());
|
||||
tracing::trace!("Flushed {} events", send_data.len());
|
||||
}
|
||||
|
||||
event_handler.handle_event(Ok(send_data));
|
||||
@ -683,7 +674,7 @@ pub fn new_debouncer<F: DebounceEventHandler>(
|
||||
tick_rate,
|
||||
flush_after,
|
||||
event_handler,
|
||||
FileIdMap::new(),
|
||||
FileIdMap::default(),
|
||||
notify::Config::default(),
|
||||
)
|
||||
}
|
210
crates/gitbutler-watcher/src/debouncer/testing/mod.rs
Normal file
210
crates/gitbutler-watcher/src/debouncer/testing/mod.rs
Normal file
@ -0,0 +1,210 @@
|
||||
// Note that this file contains substantial portions of code
|
||||
// from https://github.com/notify-rs/notify/blob/main/notify-debouncer-full/src/testing.rs,
|
||||
// and what follows is a reproduction of its license.
|
||||
//
|
||||
// Copyright (c) 2023 Notify Contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any
|
||||
// person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the
|
||||
// Software without restriction, including without
|
||||
// limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software
|
||||
// is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use mock_instant::Instant;
|
||||
|
||||
pub(crate) use schema::TestCase;
|
||||
|
||||
use mock_instant::MockClock;
|
||||
use notify::RecursiveMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[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 =
|
||||
std::fs::read_to_string(Path::new(&format!("tests/fixtures/{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.clone_from(&backup_queues);
|
||||
|
||||
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(false),
|
||||
events,
|
||||
"debounced events after a `{delay}` delay"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod schema;
|
||||
mod utils {
|
||||
use crate::debouncer::FileIdCache;
|
||||
|
||||
use file_id::FileId;
|
||||
use notify::RecursiveMode;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
297
crates/gitbutler-watcher/src/debouncer/testing/schema.rs
Normal file
297
crates/gitbutler-watcher/src/debouncer/testing/schema.rs
Normal file
@ -0,0 +1,297 @@
|
||||
// Note that this file contains substantial portions of code
|
||||
// from https://github.com/notify-rs/notify/blob/main/notify-debouncer-full/src/testing.rs,
|
||||
// and what follows is a reproduction of its license.
|
||||
//
|
||||
// Copyright (c) 2023 Notify Contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any
|
||||
// person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the
|
||||
// Software without restriction, including without
|
||||
// limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software
|
||||
// is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
use crate::debouncer::{DebounceDataInner, DebouncedEvent};
|
||||
use file_id::FileId;
|
||||
use mock_instant::Instant;
|
||||
use notify::event::{
|
||||
AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind,
|
||||
RenameMode,
|
||||
};
|
||||
use notify::{ErrorKind, EventKind};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::utils::TestCache;
|
||||
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 Error {
|
||||
pub fn into_notify_error(self) -> notify::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 = notify::Error::new(kind);
|
||||
|
||||
for p in self.paths {
|
||||
error = error.add_path(PathBuf::from(p));
|
||||
}
|
||||
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
impl 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 = notify::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 State {
|
||||
pub(crate) fn into_debounce_data_inner(self, time: Instant) -> DebounceDataInner<TestCache> {
|
||||
let queues = self
|
||||
.queues
|
||||
.into_iter()
|
||||
.map(|(path, queue)| {
|
||||
let queue = crate::debouncer::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)),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use notify::Event;
|
||||
|
||||
/// A debounced event is emitted after a short delay.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DebouncedEvent {
|
||||
/// The original event.
|
||||
pub event: Event,
|
||||
|
||||
/// The time at which the event occurred.
|
||||
pub time: Instant,
|
||||
}
|
||||
|
||||
impl DebouncedEvent {
|
||||
pub fn new(event: Event, time: Instant) -> Self {
|
||||
Self { event, time }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DebouncedEvent {
|
||||
type Target = Event;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.event
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DebouncedEvent {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.event
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DebouncedEvent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
event: Default::default(),
|
||||
time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Event> for DebouncedEvent {
|
||||
fn from(event: Event) -> Self {
|
||||
Self {
|
||||
event,
|
||||
time: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use crate::debouncer::cache::FileIdMap;
|
||||
use crate::debouncer::Debouncer;
|
||||
use crate::debouncer_cache::FileIdMap;
|
||||
use crate::{debouncer::new_debouncer, events::InternalEvent};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gitbutler_core::ops::OPLOG_FILE_NAME;
|
||||
use gitbutler_core::projects::ProjectId;
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
use notify::RecommendedWatcher;
|
||||
use tokio::task;
|
||||
use tracing::Level;
|
||||
|
||||
@ -55,7 +55,7 @@ pub fn spawn(
|
||||
project_id: ProjectId,
|
||||
worktree_path: &std::path::Path,
|
||||
out: tokio::sync::mpsc::UnboundedSender<InternalEvent>,
|
||||
) -> Result<Arc<Debouncer<RecommendedWatcher, FileIdMap>>> {
|
||||
) -> Result<Debouncer<RecommendedWatcher, FileIdMap>> {
|
||||
let (notify_tx, notify_rx) = std::sync::mpsc::channel();
|
||||
let mut debouncer = new_debouncer(
|
||||
DEBOUNCE_TIMEOUT,
|
||||
@ -88,12 +88,11 @@ pub fn spawn(
|
||||
|
||||
// Start the watcher, but retry if there are transient errors.
|
||||
backoff::retry(policy, || {
|
||||
let watcher = debouncer.watcher();
|
||||
watcher
|
||||
debouncer
|
||||
.watch(worktree_path, notify::RecursiveMode::Recursive)
|
||||
.and_then(|()| {
|
||||
if let Some(git_dir) = extra_git_dir_to_watch {
|
||||
watcher.watch(git_dir, notify::RecursiveMode::Recursive)
|
||||
debouncer.watch(git_dir, notify::RecursiveMode::Recursive)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
@ -228,7 +227,7 @@ pub fn spawn(
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(debouncer.into())
|
||||
Ok(debouncer)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
|
@ -7,14 +7,7 @@ mod events;
|
||||
use events::InternalEvent;
|
||||
pub use events::{Action, Change};
|
||||
|
||||
mod debouncer;
|
||||
mod debouncer_cache;
|
||||
mod debouncer_event;
|
||||
mod file_monitor;
|
||||
mod handler;
|
||||
|
||||
pub use handler::Handler;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@ -25,6 +18,10 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
mod debouncer;
|
||||
mod file_monitor;
|
||||
mod handler;
|
||||
|
||||
/// An abstraction over a link to the spawned watcher, which runs in the background.
|
||||
pub struct WatcherHandle {
|
||||
/// A way to post events and interact with the actual handler in the background.
|
||||
|
1
crates/gitbutler-watcher/tests/debouncer.rs
Normal file
1
crates/gitbutler-watcher/tests/debouncer.rs
Normal file
@ -0,0 +1 @@
|
||||
|
20
crates/gitbutler-watcher/tests/fixtures/add_create_dir_event_twice.hjson
vendored
Normal file
20
crates/gitbutler-watcher/tests/fixtures/add_create_dir_event_twice.hjson
vendored
Normal 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: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
crates/gitbutler-watcher/tests/fixtures/add_create_event.hjson
vendored
Normal file
29
crates/gitbutler-watcher/tests/fixtures/add_create_event.hjson
vendored
Normal 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"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
24
crates/gitbutler-watcher/tests/fixtures/add_create_event_after_remove_event.hjson
vendored
Normal file
24
crates/gitbutler-watcher/tests/fixtures/add_create_event_after_remove_event.hjson
vendored
Normal 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: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
crates/gitbutler-watcher/tests/fixtures/add_errors.hjson
vendored
Normal file
13
crates/gitbutler-watcher/tests/fixtures/add_errors.hjson
vendored
Normal 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"] }
|
||||
]
|
||||
}
|
||||
}
|
26
crates/gitbutler-watcher/tests/fixtures/add_modify_content_event_after_create_event.hjson
vendored
Normal file
26
crates/gitbutler-watcher/tests/fixtures/add_modify_content_event_after_create_event.hjson
vendored
Normal 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: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
crates/gitbutler-watcher/tests/fixtures/add_remove_event.hjson
vendored
Normal file
15
crates/gitbutler-watcher/tests/fixtures/add_remove_event.hjson
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
state: {}
|
||||
events: [
|
||||
{ kind: "remove-any", paths: ["/watch/file"] }
|
||||
]
|
||||
expected: {
|
||||
queues: {
|
||||
/watch/file: {
|
||||
events: [
|
||||
{ kind: "remove-any", paths: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
crates/gitbutler-watcher/tests/fixtures/add_remove_event_after_create_and_modify_event.hjson
vendored
Normal file
16
crates/gitbutler-watcher/tests/fixtures/add_remove_event_after_create_and_modify_event.hjson
vendored
Normal 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: {}
|
||||
}
|
15
crates/gitbutler-watcher/tests/fixtures/add_remove_event_after_create_event.hjson
vendored
Normal file
15
crates/gitbutler-watcher/tests/fixtures/add_remove_event_after_create_event.hjson
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
state: {
|
||||
queues: {
|
||||
/watch/file: {
|
||||
events: [
|
||||
{ kind: "create-any", paths: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
events: [
|
||||
{ kind: "remove-any", paths: ["/watch/file"] }
|
||||
]
|
||||
expected: {}
|
||||
}
|
23
crates/gitbutler-watcher/tests/fixtures/add_remove_event_after_modify_event.hjson
vendored
Normal file
23
crates/gitbutler-watcher/tests/fixtures/add_remove_event_after_modify_event.hjson
vendored
Normal 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: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
crates/gitbutler-watcher/tests/fixtures/add_remove_parent_event_after_remove_child_event.hjson
vendored
Normal file
25
crates/gitbutler-watcher/tests/fixtures/add_remove_parent_event_after_remove_child_event.hjson
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
7
crates/gitbutler-watcher/tests/fixtures/add_rename_both_event.hjson
vendored
Normal file
7
crates/gitbutler-watcher/tests/fixtures/add_rename_both_event.hjson
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
state: {}
|
||||
events: [
|
||||
{ kind: "rename-both", paths: ["/watch/source", "/watch/target"] }
|
||||
]
|
||||
expected: {}
|
||||
}
|
16
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event.hjson
vendored
Normal file
16
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_after_create.hjson
vendored
Normal file
24
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_after_create.hjson
vendored
Normal 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: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_after_modify_content.hjson
vendored
Normal file
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_after_modify_content.hjson
vendored
Normal 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: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_after_rename.hjson
vendored
Normal file
24
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_after_rename.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_override_created.hjson
vendored
Normal file
24
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_override_created.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_override_modified.hjson
vendored
Normal file
35
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_override_modified.hjson
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_override_removed.hjson
vendored
Normal file
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_override_removed.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_with_file_ids.hjson
vendored
Normal file
26
crates/gitbutler-watcher/tests/fixtures/add_rename_from_and_to_event_with_file_ids.hjson
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
16
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event.hjson
vendored
Normal file
16
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event.hjson
vendored
Normal 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"] }
|
||||
}
|
||||
}
|
@ -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"] }
|
||||
}
|
||||
}
|
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event_after_create_event.hjson
vendored
Normal file
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event_after_create_event.hjson
vendored
Normal 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"] }
|
||||
}
|
||||
}
|
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event_after_modify_event.hjson
vendored
Normal file
25
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event_after_modify_event.hjson
vendored
Normal 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"] }
|
||||
}
|
||||
}
|
30
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event_after_rename_from_event.hjson
vendored
Normal file
30
crates/gitbutler-watcher/tests/fixtures/add_rename_from_event_after_rename_from_event.hjson
vendored
Normal 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"] }
|
||||
}
|
||||
}
|
24
crates/gitbutler-watcher/tests/fixtures/add_rename_to_dir_event.hjson
vendored
Normal file
24
crates/gitbutler-watcher/tests/fixtures/add_rename_to_dir_event.hjson
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
15
crates/gitbutler-watcher/tests/fixtures/add_rename_to_event.hjson
vendored
Normal file
15
crates/gitbutler-watcher/tests/fixtures/add_rename_to_event.hjson
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
state: {}
|
||||
events: [
|
||||
{ kind: "rename-to", paths: ["/watch/target"] }
|
||||
]
|
||||
expected: {
|
||||
queues: {
|
||||
/watch/target: {
|
||||
events: [
|
||||
{ kind: "rename-to", paths: ["*"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
crates/gitbutler-watcher/tests/fixtures/emit_close_events_only_once.hjson
vendored
Normal file
28
crates/gitbutler-watcher/tests/fixtures/emit_close_events_only_once.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
49
crates/gitbutler-watcher/tests/fixtures/emit_continuous_modify_content_events.hjson
vendored
Normal file
49
crates/gitbutler-watcher/tests/fixtures/emit_continuous_modify_content_events.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
45
crates/gitbutler-watcher/tests/fixtures/emit_events_in_chronological_order.hjson
vendored
Normal file
45
crates/gitbutler-watcher/tests/fixtures/emit_events_in_chronological_order.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
36
crates/gitbutler-watcher/tests/fixtures/emit_events_with_a_prepended_rename_event.hjson
vendored
Normal file
36
crates/gitbutler-watcher/tests/fixtures/emit_events_with_a_prepended_rename_event.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
30
crates/gitbutler-watcher/tests/fixtures/emit_modify_event_after_close_event.hjson
vendored
Normal file
30
crates/gitbutler-watcher/tests/fixtures/emit_modify_event_after_close_event.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
56
crates/gitbutler-watcher/tests/fixtures/emit_needs_rescan_event.hjson
vendored
Normal file
56
crates/gitbutler-watcher/tests/fixtures/emit_needs_rescan_event.hjson
vendored
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
22
crates/gitbutler-watcher/tests/fixtures/read_file_id_without_create_event.hjson
vendored
Normal file
22
crates/gitbutler-watcher/tests/fixtures/read_file_id_without_create_event.hjson
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
267
crates/gitbutler-watcher/tests/schema/mod.rs
Normal file
267
crates/gitbutler-watcher/tests/schema/mod.rs
Normal file
@ -0,0 +1,267 @@
|
||||
use file_id::FileId;
|
||||
use mock_instant::Instant;
|
||||
use notify::event::{
|
||||
AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind,
|
||||
RenameMode,
|
||||
};
|
||||
use notify::{ErrorKind, EventKind};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{schema, TestCache};
|
||||
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 Error {
|
||||
pub fn into_notify_error(self) -> notify::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 = notify::Error::new(kind);
|
||||
|
||||
for p in self.paths {
|
||||
error = error.add_path(PathBuf::from(p));
|
||||
}
|
||||
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
impl 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 = notify::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 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)),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user