use notify::{RecommendedWatcher, Watcher, WatcherKind};
#[derive(Debug, thiserror::Error)]
#[error(
"the platform resolved watch backend {resolved:?}, which is not one of \
the sanctioned native event backends (FSEvents, inotify); frame dev \
refuses to start rather than fall back to a polling watcher"
)]
pub struct NonNativeBackend {
pub resolved: WatcherKind,
}
pub fn assert_native_backend() -> Result<WatcherKind, NonNativeBackend> {
classify(<RecommendedWatcher as Watcher>::kind())
}
fn classify(resolved: WatcherKind) -> Result<WatcherKind, NonNativeBackend> {
match resolved {
WatcherKind::Fsevent | WatcherKind::Inotify => Ok(resolved),
other => Err(NonNativeBackend { resolved: other }),
}
}
#[derive(Debug, Clone)]
pub struct WatchedSet {
pub watch_root: std::path::PathBuf,
manifest: std::path::PathBuf,
sources: std::path::PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Classification {
Irrelevant,
Dirtying,
RootAffected(std::path::PathBuf),
Desynchronized,
}
impl WatchedSet {
#[must_use]
pub fn of_project(root: &std::path::Path) -> Self {
let component = root.join("component");
Self {
manifest: component.join("gleam.toml"),
sources: component.join("src"),
watch_root: component,
}
}
#[must_use]
pub fn classify(&self, event: ¬ify::Event) -> Classification {
use notify::EventKind;
use notify::event::ModifyKind;
if event.need_rescan() {
return Classification::Desynchronized;
}
match event.kind {
EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => {
return Classification::Irrelevant;
}
EventKind::Any
| EventKind::Other
| EventKind::Create(_)
| EventKind::Modify(_)
| EventKind::Remove(_) => {}
}
if matches!(
event.kind,
EventKind::Remove(_) | EventKind::Modify(ModifyKind::Name(_))
) && let Some(root) = event.paths.iter().find(|path| self.is_watched_root(path))
{
return Classification::RootAffected(root.clone());
}
if event.paths.iter().any(|path| self.is_build_input(path)) {
Classification::Dirtying
} else {
Classification::Irrelevant
}
}
fn is_watched_root(&self, path: &std::path::Path) -> bool {
path == self.sources || path == self.manifest || path == self.watch_root
}
fn is_build_input(&self, path: &std::path::Path) -> bool {
path == self.manifest || path.starts_with(&self.sources)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::{Classification, NonNativeBackend, WatchedSet, assert_native_backend, classify};
use notify::WatcherKind;
#[test]
fn this_platform_resolves_a_native_event_backend() {
let resolved = assert_native_backend().expect("native backend");
assert!(
matches!(resolved, WatcherKind::Fsevent | WatcherKind::Inotify),
"resolved {resolved:?}"
);
}
#[test]
fn poll_watcher_is_refused_by_name() {
let refusal = classify(WatcherKind::PollWatcher).expect_err("refuses");
let NonNativeBackend { resolved } = &refusal;
assert_eq!(*resolved, WatcherKind::PollWatcher);
let message = refusal.to_string();
assert!(message.contains("PollWatcher"), "{message}");
assert!(message.contains("polling watcher"), "{message}");
}
#[test]
fn kqueue_is_outside_the_ruled_set() {
classify(WatcherKind::Kqueue).expect_err("refuses kqueue");
classify(WatcherKind::NullWatcher).expect_err("refuses the test fake");
}
use notify::EventKind;
use notify::event::{
AccessKind, CreateKind, DataChange, Event, MetadataKind, ModifyKind, RemoveKind, RenameMode,
};
use std::path::Path;
fn set() -> WatchedSet {
WatchedSet::of_project(Path::new("/proj"))
}
fn event(kind: EventKind, path: &str) -> Event {
Event::new(kind).add_path(path.into())
}
#[test]
fn build_inputs_dirty_on_every_mutating_kind() {
let set = set();
for kind in [
EventKind::Create(CreateKind::File),
EventKind::Modify(ModifyKind::Data(DataChange::Content)),
EventKind::Modify(ModifyKind::Any),
EventKind::Remove(RemoveKind::File),
EventKind::Any,
] {
assert_eq!(
set.classify(&event(kind, "/proj/component/src/app.gleam")),
Classification::Dirtying,
"{kind:?}"
);
}
assert_eq!(
set.classify(&event(
EventKind::Modify(ModifyKind::Data(DataChange::Content)),
"/proj/component/gleam.toml"
)),
Classification::Dirtying,
"the manifest is a build input"
);
assert_eq!(
set.classify(&event(
EventKind::Create(CreateKind::Folder),
"/proj/component/src/nested/dir"
)),
Classification::Dirtying,
"directories under src dirty too"
);
}
#[test]
fn metadata_and_access_noise_is_irrelevant() {
let set = set();
assert_eq!(
set.classify(&event(
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)),
"/proj/component/src/app.gleam"
)),
Classification::Irrelevant
);
assert_eq!(
set.classify(&event(
EventKind::Access(AccessKind::Any),
"/proj/component/src/app.gleam"
)),
Classification::Irrelevant
);
}
#[test]
fn build_outputs_and_template_files_are_outside_the_set() {
let set = set();
assert_eq!(
set.classify(&event(
EventKind::Create(CreateKind::File),
"/proj/component/build/dev/erlang/app/ebin/app.beam"
)),
Classification::Irrelevant,
"own build outputs never dirty"
);
for template in [
"/proj/host/src/lib.rs",
"/proj/host/build.rs",
"/proj/host/Cargo.toml",
"/proj/frame.toml",
"/proj/page/main.ts",
] {
assert_eq!(
set.classify(&event(
EventKind::Modify(ModifyKind::Data(DataChange::Content)),
template
)),
Classification::Irrelevant,
"{template} is a regeneration seam, not a watch event"
);
}
}
#[test]
fn root_removal_and_rename_surface_as_root_affected() {
let set = set();
for (kind, path) in [
(EventKind::Remove(RemoveKind::Folder), "/proj/component/src"),
(
EventKind::Modify(ModifyKind::Name(RenameMode::From)),
"/proj/component/src",
),
(
EventKind::Remove(RemoveKind::File),
"/proj/component/gleam.toml",
),
(EventKind::Remove(RemoveKind::Folder), "/proj/component"),
] {
assert_eq!(
set.classify(&event(kind, path)),
Classification::RootAffected(Path::new(path).to_path_buf()),
"{kind:?} {path}"
);
}
}
#[test]
fn rescan_flag_classifies_desynchronized() {
let set = set();
let lossy = Event::new(EventKind::Any).set_flag(notify::event::Flag::Rescan);
assert_eq!(set.classify(&lossy), Classification::Desynchronized);
}
}