tinymist_project/
watch.rs

1//! upstream <https://github.com/rust-lang/rust-analyzer/tree/master/crates/vfs-notify>
2//!
3//! An implementation of `watch_deps` using `notify` crate.
4//!
5//! The file watching bits here are untested and quite probably buggy. For this
6//! reason, by default we don't watch files and rely on editor's file watching
7//! capabilities.
8//!
9//! Hopefully, one day a reliable file watching/walking crate appears on
10//! crates.io, and we can reduce this to trivial glue code.
11
12use std::{collections::HashMap, fmt, path::Path};
13
14use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
15use tinymist_std::{ImmutPath, error::IgnoreLogging};
16use tinymist_world::vfs::notify::NotifyDeps;
17use tokio::sync::mpsc;
18use typst::diag::{FileError, FileResult};
19
20use tinymist_world::vfs::{
21    Bytes, FileChangeSet, FileSnapshot, PathAccessModel,
22    notify::{FilesystemEvent, NotifyMessage, UpstreamUpdateEvent},
23    system::SystemAccessModel,
24};
25
26type WatcherPair = (RecommendedWatcher, mpsc::UnboundedReceiver<NotifyEvent>);
27type NotifyEvent = notify::Result<notify::Event>;
28type FileEntry = (/* key */ ImmutPath, /* value */ FileSnapshot);
29
30trait NotifyActorAccess: fmt::Debug + Send + Sync {
31    fn content(&self, src: &Path) -> FileResult<Bytes>;
32
33    fn is_watchable_file(&self, src: &Path) -> bool;
34}
35
36#[derive(Debug)]
37struct SystemNotifyActorAccess(SystemAccessModel);
38
39impl NotifyActorAccess for SystemNotifyActorAccess {
40    fn content(&self, src: &Path) -> FileResult<Bytes> {
41        self.0.content(src)
42    }
43
44    fn is_watchable_file(&self, src: &Path) -> bool {
45        src.metadata().is_ok_and(|meta| !meta.is_dir())
46    }
47}
48
49#[derive(Debug)]
50enum NotifyWatcher {
51    System(WatcherPair),
52    #[cfg(test)]
53    Fake(FakeWatcher),
54}
55
56impl NotifyWatcher {
57    async fn recv(&mut self) -> Option<NotifyEvent> {
58        match self {
59            Self::System((_, watcher_receiver)) => watcher_receiver.recv().await,
60            #[cfg(test)]
61            Self::Fake(_) => None,
62        }
63    }
64
65    fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> notify::Result<()> {
66        match self {
67            Self::System((watcher, _)) => watcher.watch(path, recursive_mode),
68            #[cfg(test)]
69            Self::Fake(watcher) => watcher.watch(path),
70        }
71    }
72
73    fn unwatch(&mut self, path: &Path) -> notify::Result<()> {
74        match self {
75            Self::System((watcher, _)) => watcher.unwatch(path),
76            #[cfg(test)]
77            Self::Fake(watcher) => watcher.unwatch(path),
78        }
79    }
80}
81
82#[cfg(test)]
83#[derive(Debug, Clone, PartialEq, Eq)]
84enum FakeWatchCommand {
85    Watch(std::path::PathBuf),
86    Unwatch(std::path::PathBuf),
87}
88
89#[cfg(test)]
90#[derive(Debug, Default, Clone)]
91struct FakeWatchCommands(std::sync::Arc<std::sync::Mutex<Vec<FakeWatchCommand>>>);
92
93#[cfg(test)]
94impl FakeWatchCommands {
95    fn push(&self, command: FakeWatchCommand) {
96        self.0
97            .lock()
98            .expect("fake watch commands poisoned")
99            .push(command);
100    }
101
102    fn take(&self) -> Vec<FakeWatchCommand> {
103        std::mem::take(&mut *self.0.lock().expect("fake watch commands poisoned"))
104    }
105}
106
107#[cfg(test)]
108#[derive(Debug)]
109struct FakeWatcher {
110    commands: FakeWatchCommands,
111}
112
113#[cfg(test)]
114impl FakeWatcher {
115    fn watch(&self, path: &Path) -> notify::Result<()> {
116        self.commands
117            .push(FakeWatchCommand::Watch(path.to_path_buf()));
118        Ok(())
119    }
120
121    fn unwatch(&self, path: &Path) -> notify::Result<()> {
122        self.commands
123            .push(FakeWatchCommand::Unwatch(path.to_path_buf()));
124        Ok(())
125    }
126}
127
128/// The state of a watched file.
129///
130/// It is used to determine some dirty editors' implementation.
131#[derive(Debug)]
132enum WatchState {
133    /// The file is stable, which means we believe that it keeps synchronized
134    /// as expected.
135    Stable,
136    /// The file is empty or removed, but there is a chance that the file is not
137    /// stable. So we need to recheck the file after a while.
138    EmptyOrRemoval {
139        recheck_at: usize,
140        payload: FileSnapshot,
141    },
142}
143
144/// By default, the state is stable.
145impl Default for WatchState {
146    fn default() -> Self {
147        Self::Stable
148    }
149}
150
151/// The data entry of a watched file.
152#[derive(Debug)]
153struct WatchedEntry {
154    /// The lifetime of the entry.
155    ///
156    /// The entry will be removed if the entry is too old.
157    // todo: generalize lifetime
158    lifetime: usize,
159    /// A flag for whether it is really watching.
160    watching: bool,
161    /// A flag for watch update.
162    seen: bool,
163    /// The state of the entry.
164    state: WatchState,
165    /// Previous content of the file.
166    prev: Option<FileSnapshot>,
167}
168
169/// Self produced event that check whether the file is stable after a while.
170#[derive(Debug)]
171struct UndeterminedNotifyEvent {
172    /// The time when the event is produced.
173    at_realtime: tinymist_std::time::Instant,
174    /// The logical tick when the event is produced.
175    at_logical_tick: usize,
176    /// The path of the file.
177    path: ImmutPath,
178}
179
180// Drop order is significant.
181/// The actor that watches files.
182/// It is used to watch files and send events to the consumers
183#[derive(Debug)]
184pub struct NotifyActor<F: FnMut(FilesystemEvent)> {
185    /// The access model of the actor.
186    inner: Box<dyn NotifyActorAccess>,
187
188    /// The lifetime of the watched files.
189    lifetime: usize,
190    /// The logical tick of the actor.
191    logical_tick: usize,
192
193    /// Internal channel for recheck events.
194    undetermined_send: mpsc::UnboundedSender<UndeterminedNotifyEvent>,
195    undetermined_recv: mpsc::UnboundedReceiver<UndeterminedNotifyEvent>,
196
197    /// The hold entries for watching, one entry for per file.
198    watched_entries: HashMap<ImmutPath, WatchedEntry>,
199
200    interrupted_by_events: F,
201
202    /// The builtin watcher object.
203    watcher: Option<NotifyWatcher>,
204}
205
206impl<F: FnMut(FilesystemEvent) + Send + Sync> NotifyActor<F> {
207    /// Create a new actor.
208    pub fn new(interrupted_by_events: F) -> Self {
209        let (undetermined_send, undetermined_recv) = mpsc::unbounded_channel();
210        let (watcher_tx, watcher_rx) = mpsc::unbounded_channel();
211        let watcher = log_notify_error(
212            RecommendedWatcher::new(
213                move |event| {
214                    watcher_tx.send(event).log_error("failed to send fs notify");
215                },
216                Config::default(),
217            ),
218            "failed to create watcher",
219        );
220
221        NotifyActor {
222            inner: Box::new(SystemNotifyActorAccess(SystemAccessModel)),
223            // we start from 1 to distinguish from 0 (default value)
224            lifetime: 1,
225            logical_tick: 1,
226
227            interrupted_by_events,
228
229            undetermined_send,
230            undetermined_recv,
231
232            watched_entries: HashMap::new(),
233            watcher: watcher.map(|it| NotifyWatcher::System((it, watcher_rx))),
234        }
235    }
236
237    #[cfg(test)]
238    fn new_for_test(
239        inner: Box<dyn NotifyActorAccess>,
240        commands: FakeWatchCommands,
241        interrupted_by_events: F,
242    ) -> Self {
243        let (undetermined_send, undetermined_recv) = mpsc::unbounded_channel();
244
245        NotifyActor {
246            inner,
247            // we start from 1 to distinguish from 0 (default value)
248            lifetime: 1,
249            logical_tick: 1,
250
251            interrupted_by_events,
252
253            undetermined_send,
254            undetermined_recv,
255
256            watched_entries: HashMap::new(),
257            watcher: Some(NotifyWatcher::Fake(FakeWatcher { commands })),
258        }
259    }
260
261    /// Get the notify event from the watcher.
262    async fn get_notify_event(watcher: &mut Option<NotifyWatcher>) -> Option<NotifyEvent> {
263        match watcher {
264            Some(watcher) => watcher.recv().await,
265            None => None,
266        }
267    }
268
269    /// Main loop of the actor.
270    pub async fn run(mut self, mut inbox: mpsc::UnboundedReceiver<NotifyMessage>) {
271        use NotifyMessage::*;
272        /// The event of the actor.
273        #[derive(Debug)]
274        enum ActorEvent {
275            /// Recheck the notify event.
276            ReCheck(UndeterminedNotifyEvent),
277            /// Poll missing files that cannot be watched by notify-rs.
278            PollMissing,
279            /// external message to change notifier's state
280            Message(Option<NotifyMessage>),
281            /// notify event from builtin watcher
282            NotifyEvent(NotifyEvent),
283        }
284
285        let mut missing_poll =
286            tokio::time::interval(tinymist_std::time::Duration::from_millis(300));
287        missing_poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
288
289        'event_loop: loop {
290            // Get the event from the inbox or the watcher.
291            let event = tokio::select! {
292                it = inbox.recv() => ActorEvent::Message(it),
293                Some(it) = Self::get_notify_event(&mut self.watcher) => ActorEvent::NotifyEvent(it),
294                Some(it) = self.undetermined_recv.recv() => ActorEvent::ReCheck(it),
295                _ = missing_poll.tick() => ActorEvent::PollMissing,
296            };
297
298            // Increase the logical tick per event.
299            self.logical_tick += 1;
300
301            // log::info!("vfs-notify event {event:?}");
302            // function entries to handle some event
303            match event {
304                ActorEvent::Message(None) => {
305                    log::info!("NotifyActor: failed to get event, exiting...");
306                    break 'event_loop;
307                }
308                ActorEvent::Message(Some(Settle)) => {
309                    log::info!("NotifyActor: settle event received");
310                    break 'event_loop;
311                }
312                ActorEvent::Message(Some(UpstreamUpdate(event))) => {
313                    self.invalidate_upstream(event);
314                }
315                ActorEvent::Message(Some(SyncDependency(paths))) => {
316                    if let Some(changeset) = self.update_watches(paths.as_ref()) {
317                        (self.interrupted_by_events)(FilesystemEvent::Update(changeset, true));
318                    }
319                }
320                ActorEvent::NotifyEvent(event) => {
321                    // log::info!("notify event {event:?}");
322                    if let Some(event) = log_notify_error(event, "failed to notify") {
323                        self.notify_event(event);
324                    }
325                }
326                ActorEvent::ReCheck(event) => {
327                    self.recheck_notify_event(event).await;
328                }
329                ActorEvent::PollMissing => {
330                    self.poll_missing_watches();
331                }
332            }
333        }
334
335        log::info!("NotifyActor: exited");
336    }
337
338    /// Update the watches of corresponding invalidation
339    fn invalidate_upstream(&mut self, event: UpstreamUpdateEvent) {
340        // Update watches of invalidated files.
341        let changeset = self.update_watches(&event.invalidates).unwrap_or_default();
342
343        // Send the event to the consumer.
344        (self.interrupted_by_events)(FilesystemEvent::UpstreamUpdate {
345            changeset,
346            upstream_event: Some(event),
347        });
348    }
349
350    /// Update the watches of corresponding files.
351    fn update_watches(&mut self, paths: &dyn NotifyDeps) -> Option<FileChangeSet> {
352        // Increase the lifetime per external message.
353        self.lifetime += 1;
354
355        let mut changeset = FileChangeSet::default();
356
357        // Mark the old entries as unseen.
358        for path in self.watched_entries.values_mut() {
359            path.seen = false;
360        }
361
362        // Update watched entries.
363        //
364        // Also check whether the file is updated since there is a window
365        // between unwatch the file and watch the file again.
366        paths.dependencies(&mut |path| {
367            let mut contained = false;
368            // Update or insert the entry with the new lifetime.
369            let entry = self
370                .watched_entries
371                .entry(path.clone())
372                .and_modify(|watch_entry| {
373                    contained = true;
374                    watch_entry.lifetime = self.lifetime;
375                })
376                .or_insert_with(|| WatchedEntry {
377                    lifetime: self.lifetime,
378                    watching: false,
379                    seen: false,
380                    state: WatchState::Stable,
381                    prev: None,
382                });
383
384            if entry.seen {
385                return;
386            }
387            entry.seen = true;
388
389            if self.watcher.is_some() {
390                self.watch_file_if_needed(path, contained);
391
392                changeset.may_insert(self.notify_entry_update(path.clone()));
393            } else {
394                let watched = self.inner.content(path);
395                changeset.inserts.push((path.clone(), watched.into()));
396            }
397        });
398
399        // Remove old entries.
400        // Note: since we have increased the lifetime, it is safe to remove the
401        // old entries after updating the watched entries.
402        self.watched_entries.retain(|path, entry| {
403            if !entry.seen && entry.watching {
404                log::debug!("unwatch {path:?}");
405                if let Some(watcher) = &mut self.watcher {
406                    log_notify_error(watcher.unwatch(path), "failed to unwatch");
407                    entry.watching = false;
408                }
409            }
410
411            let fresh = self.lifetime - entry.lifetime < 30;
412            if !fresh {
413                changeset.removes.push(path.clone());
414            }
415            fresh
416        });
417
418        (!changeset.is_empty()).then_some(changeset)
419    }
420
421    fn watch_file_if_needed(&mut self, path: &ImmutPath, contained: bool) {
422        let Some(entry) = self.watched_entries.get_mut(path) else {
423            return;
424        };
425
426        if !self.inner.is_watchable_file(path.as_ref()) {
427            return;
428        }
429
430        // Case1. meta = Err(..) We cannot get the metadata successfully, so we
431        // are okay to ignore this file for watching.
432        //
433        // Case2. meta = Ok(..) Watch the file if it's not watched.
434        if !contained || !entry.watching {
435            log::debug!("watching {path:?}");
436            if let Some(watcher) = &mut self.watcher {
437                entry.watching = log_notify_error(
438                    watcher.watch(path.as_ref(), RecursiveMode::NonRecursive),
439                    "failed to watch",
440                )
441                .is_some();
442            }
443        }
444    }
445
446    fn poll_missing_watches(&mut self) {
447        if self.watcher.is_none() {
448            return;
449        }
450
451        let paths = self
452            .watched_entries
453            .iter()
454            .filter(|(_, entry)| entry.seen && !entry.watching && entry_is_known_missing(entry))
455            .filter(|&(path, _)| self.inner.is_watchable_file(path.as_ref()))
456            .map(|(path, _)| path.clone())
457            .collect::<Vec<_>>();
458
459        if paths.is_empty() {
460            return;
461        }
462
463        let mut changeset = FileChangeSet::default();
464        for path in paths {
465            self.watch_file_if_needed(&path, true);
466            changeset.may_insert(self.notify_entry_update(path));
467        }
468
469        if !changeset.is_empty() {
470            (self.interrupted_by_events)(FilesystemEvent::Update(changeset, false));
471        }
472    }
473
474    /// Notify the event from the builtin watcher.
475    fn notify_event(&mut self, event: notify::Event) {
476        if !is_relevant_event_kind(&event.kind) {
477            return;
478        }
479
480        // Account file updates.
481        let mut changeset = FileChangeSet::default();
482        for path in event.paths.iter() {
483            // todo: remove this clone: path.into()
484            changeset.may_insert(self.notify_entry_update(path.as_path().into()));
485        }
486
487        // Workaround for notify-rs' implicit unwatch on remove/rename
488        // (triggered by some editors when saving files) with the
489        // inotify backend. By keeping track of the potentially
490        // unwatched files, we can allow those we still depend on to be
491        // watched again later on.
492        if matches!(
493            event.kind,
494            notify::EventKind::Remove(notify::event::RemoveKind::File)
495                | notify::EventKind::Modify(notify::event::ModifyKind::Name(
496                    notify::event::RenameMode::From
497                ))
498        ) {
499            for path in &event.paths {
500                let Some(entry) = self.watched_entries.get_mut(path.as_path()) else {
501                    continue;
502                };
503                if !entry.watching {
504                    continue;
505                }
506                // Remove affected path from the watched map to restart
507                // watching on it later again.
508                if let Some(watcher) = &mut self.watcher {
509                    log_notify_error(watcher.unwatch(path), "failed to unwatch");
510                }
511                entry.watching = false;
512            }
513        }
514
515        // Send file updates.
516        if !changeset.is_empty() {
517            (self.interrupted_by_events)(FilesystemEvent::Update(changeset, false));
518        }
519    }
520
521    /// Notify any update of the file entry
522    fn notify_entry_update(&mut self, path: ImmutPath) -> Option<FileEntry> {
523        // The following code in rust-analyzer is commented out
524        // todo: check whether we need this
525        // if meta.file_type().is_dir() && self
526        //   .watched_entries.iter().any(|entry| entry.contains_dir(&path))
527        // {
528        //     self.watch(path);
529        //     return None;
530        // }
531
532        // Find entry and continue
533        let entry = self.watched_entries.get_mut(&path)?;
534
535        // Check meta, path, and content
536        let file = FileSnapshot::from(self.inner.content(&path));
537
538        // Check state in fast path: compare state, return None on not sending
539        // the file change
540        match (entry.prev.as_deref(), file.as_ref()) {
541            // update the content of the entry in the following cases:
542            // + Case 1: previous content is clear
543            // + Case 2: previous content is not clear but some error, and the
544            // current content is ok
545            (None, ..) | (Some(Err(..)), Ok(..)) => {}
546            // Meet some error currently
547            (Some(it), Err(err)) => match &mut entry.state {
548                // If the file is stable, check whether the editor is removing
549                // or truncating the file. They are possibly flushing the file
550                // but not finished yet.
551                WatchState::Stable => {
552                    if matches!(err.as_ref(), FileError::NotFound(..) | FileError::Other(..)) {
553                        entry.state = WatchState::EmptyOrRemoval {
554                            recheck_at: self.logical_tick,
555                            payload: file.clone(),
556                        };
557                        entry.prev = Some(file);
558                        let event = UndeterminedNotifyEvent {
559                            at_realtime: tinymist_std::time::Instant::now(),
560                            at_logical_tick: self.logical_tick,
561                            path: path.clone(),
562                        };
563                        log_send_error("recheck", self.undetermined_send.send(event));
564                        return None;
565                    }
566                    // Otherwise, we push the error to the consumer.
567
568                    // Ignores the error if the error is stable
569                    if it.as_ref().is_err_and(|it| it == err) {
570                        return None;
571                    }
572                }
573
574                // Very complicated case of check error sequence, so we simplify
575                // a bit, we regard any subsequent error as the same error.
576                WatchState::EmptyOrRemoval { payload, .. } => {
577                    // update payload
578                    *payload = file;
579                    return None;
580                }
581            },
582            // Compare content for transitional the state
583            (Some(Ok(prev_content)), Ok(next_content)) => {
584                // So far it is accurately no change for the file, skip it
585                if prev_content == next_content {
586                    return None;
587                }
588
589                match entry.state {
590                    // If the file is stable, check whether the editor is
591                    // removing or truncating the file. They are possibly
592                    // flushing the file but not finished yet.
593                    WatchState::Stable => {
594                        if next_content.is_empty() {
595                            entry.state = WatchState::EmptyOrRemoval {
596                                recheck_at: self.logical_tick,
597                                payload: file.clone(),
598                            };
599                            entry.prev = Some(file);
600                            let event = UndeterminedNotifyEvent {
601                                at_realtime: tinymist_std::time::Instant::now(),
602                                at_logical_tick: self.logical_tick,
603                                path,
604                            };
605                            log_send_error("recheck", self.undetermined_send.send(event));
606                            return None;
607                        }
608                    }
609
610                    // Still empty
611                    WatchState::EmptyOrRemoval { .. } if next_content.is_empty() => return None,
612                    // Otherwise, we push the diff to the consumer.
613                    WatchState::EmptyOrRemoval { .. } => {}
614                }
615            }
616        };
617
618        // Send the update to the consumer
619        // Update the entry according to the state
620        entry.state = WatchState::Stable;
621        entry.prev = Some(file.clone());
622
623        // Slow path: trigger the file change for consumer
624        Some((path, file))
625    }
626
627    /// Recheck the notify event after a while.
628    async fn recheck_notify_event(&mut self, event: UndeterminedNotifyEvent) -> Option<()> {
629        let now = tinymist_std::time::Instant::now();
630        log::debug!("recheck event {event:?} at {now:?}");
631
632        // The async scheduler is not accurate, so we need to ensure a window here
633        let reserved = now - event.at_realtime;
634        if reserved < tinymist_std::time::Duration::from_millis(50) {
635            let send = self.undetermined_send.clone();
636            tokio::spawn(async move {
637                // todo: sleep in browser
638                tokio::time::sleep(tinymist_std::time::Duration::from_millis(50) - reserved).await;
639                log_send_error("reschedule", send.send(event));
640            });
641            return None;
642        }
643
644        // Check whether the entry is still valid
645        let entry = self.watched_entries.get_mut(&event.path)?;
646
647        // Check the state of the entry
648        match std::mem::take(&mut entry.state) {
649            // If the entry is stable, we do nothing
650            WatchState::Stable => {}
651            // If the entry is not stable, and no other event is produced after
652            // this event, we send the event to the consumer.
653            WatchState::EmptyOrRemoval {
654                recheck_at,
655                payload,
656            } => {
657                if recheck_at == event.at_logical_tick {
658                    log::debug!("notify event real happened {event:?}, state: {payload:?}");
659
660                    // Send the underlying change to the consumer
661                    let mut changeset = FileChangeSet::default();
662                    changeset.inserts.push((event.path, payload));
663
664                    (self.interrupted_by_events)(FilesystemEvent::Update(changeset, false));
665                }
666            }
667        };
668
669        Some(())
670    }
671}
672
673/// Whether a kind of watch event is relevant for compilation.
674fn is_relevant_event_kind(kind: &notify::EventKind) -> bool {
675    match kind {
676        notify::EventKind::Any => true,
677        notify::EventKind::Access(_) => false,
678        notify::EventKind::Create(_) => true,
679        notify::EventKind::Modify(kind) => match kind {
680            notify::event::ModifyKind::Any => true,
681            notify::event::ModifyKind::Data(_) => true,
682            notify::event::ModifyKind::Metadata(_) => false,
683            notify::event::ModifyKind::Name(_) => true,
684            notify::event::ModifyKind::Other => false,
685        },
686        notify::EventKind::Remove(_) => true,
687        notify::EventKind::Other => false,
688    }
689}
690
691#[inline]
692fn log_notify_error<T>(res: notify::Result<T>, reason: &'static str) -> Option<T> {
693    res.map_err(|err| log::warn!("{reason}: notify error: {err}"))
694        .ok()
695}
696
697#[inline]
698fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
699    res.map_err(|err| log::warn!("NotifyActor: send to {chan} error: {err}"))
700        .is_ok()
701}
702
703/// Watches on a set of *files*.
704pub async fn watch_deps(
705    inbox: mpsc::UnboundedReceiver<NotifyMessage>,
706    interrupted_by_events: impl FnMut(FilesystemEvent) + Send + Sync + 'static,
707) {
708    log::info!("NotifyActor: start watching files...");
709    // Watch messages to notify
710    spawn_watch_deps(inbox, interrupted_by_events);
711}
712
713fn spawn_watch_deps(
714    inbox: mpsc::UnboundedReceiver<NotifyMessage>,
715    interrupted_by_events: impl FnMut(FilesystemEvent) + Send + Sync + 'static,
716) -> tokio::task::JoinHandle<()> {
717    tokio::spawn(NotifyActor::new(interrupted_by_events).run(inbox))
718}
719
720fn entry_is_known_missing(entry: &WatchedEntry) -> bool {
721    entry.prev.as_ref().is_some_and(|snapshot| {
722        matches!(snapshot.as_ref(), Err(err) if matches!(err.as_ref(), FileError::NotFound(..)))
723    })
724}
725
726#[cfg(test)]
727mod tests {
728    use std::{
729        collections::HashMap,
730        path::{Path, PathBuf},
731        sync::{Arc, Mutex},
732    };
733
734    use notify::event::{CreateKind, DataChange, ModifyKind, RemoveKind, RenameMode};
735
736    use super::*;
737
738    type EventSink = Box<dyn FnMut(FilesystemEvent) + Send + Sync>;
739
740    // Matrix coverage note:
741    // Different notify backends can report the same editor operation as
742    // rename-from/rename-to pairs, RenameMode::Both, create/modify batches, or
743    // multi-path modify events. These deterministic rows model the equivalent
744    // actor inputs directly so the core expectations do not depend on a host
745    // backend's timing or coalescing policy.
746
747    #[derive(Debug, Clone)]
748    struct TestFile {
749        snapshot: FileSnapshot,
750        watchable: bool,
751    }
752
753    #[derive(Debug, Default, Clone)]
754    struct TestAccess {
755        files: Arc<Mutex<HashMap<PathBuf, TestFile>>>,
756        reads: Arc<Mutex<HashMap<PathBuf, usize>>>,
757    }
758
759    impl TestAccess {
760        fn set_content(&self, path: &ImmutPath, content: &str) {
761            self.set_snapshot(path, content_snapshot(content), true);
762        }
763
764        fn set_empty(&self, path: &ImmutPath) {
765            self.set_content(path, "");
766        }
767
768        fn set_error(&self, path: &ImmutPath) {
769            self.set_snapshot(
770                path,
771                Err::<Bytes, FileError>(FileError::Other(None)).into(),
772                true,
773            );
774        }
775
776        fn set_missing(&self, path: &ImmutPath) {
777            self.files
778                .lock()
779                .expect("test access poisoned")
780                .remove(path.as_ref());
781        }
782
783        fn set_snapshot(&self, path: &ImmutPath, snapshot: FileSnapshot, watchable: bool) {
784            self.files.lock().expect("test access poisoned").insert(
785                path.as_ref().to_path_buf(),
786                TestFile {
787                    snapshot,
788                    watchable,
789                },
790            );
791        }
792
793        fn read_count(&self, path: &ImmutPath) -> usize {
794            self.reads
795                .lock()
796                .expect("test read counts poisoned")
797                .get(path.as_ref())
798                .copied()
799                .unwrap_or_default()
800        }
801    }
802
803    impl NotifyActorAccess for TestAccess {
804        fn content(&self, src: &Path) -> FileResult<Bytes> {
805            *self
806                .reads
807                .lock()
808                .expect("test read counts poisoned")
809                .entry(src.to_path_buf())
810                .or_default() += 1;
811
812            self.files
813                .lock()
814                .expect("test access poisoned")
815                .get(src)
816                .map_or_else(
817                    || Err(FileError::NotFound(src.into())),
818                    |file| file.snapshot.content().cloned(),
819                )
820        }
821
822        fn is_watchable_file(&self, src: &Path) -> bool {
823            self.files
824                .lock()
825                .expect("test access poisoned")
826                .get(src)
827                .is_some_and(|file| file.watchable)
828        }
829    }
830
831    #[derive(Debug)]
832    enum MatrixInput {
833        SyncDependency(Vec<ImmutPath>),
834        UpstreamInvalidation {
835            invalidates: Vec<ImmutPath>,
836            opaque: usize,
837        },
838        WatcherEvent {
839            kind: notify::EventKind,
840            paths: Vec<ImmutPath>,
841        },
842        DelayedRecheck(ImmutPath),
843        DelayedRecheckAt {
844            path: ImmutPath,
845            recheck_at: usize,
846        },
847        PollMissing,
848    }
849
850    struct NotifyActorHarness {
851        access: TestAccess,
852        commands: FakeWatchCommands,
853        events: Arc<Mutex<Vec<FilesystemEvent>>>,
854        actor: NotifyActor<EventSink>,
855    }
856
857    impl NotifyActorHarness {
858        fn new() -> Self {
859            let access = TestAccess::default();
860            let commands = FakeWatchCommands::default();
861            let events = Arc::new(Mutex::new(Vec::new()));
862            let sink_events = events.clone();
863            let sink: EventSink = Box::new(move |event| {
864                sink_events
865                    .lock()
866                    .expect("test event sink poisoned")
867                    .push(event);
868            });
869            let actor = NotifyActor::new_for_test(Box::new(access.clone()), commands.clone(), sink);
870
871            Self {
872                access,
873                commands,
874                events,
875                actor,
876            }
877        }
878
879        async fn apply(&mut self, input: MatrixInput) {
880            self.actor.logical_tick += 1;
881
882            match input {
883                MatrixInput::SyncDependency(paths) => {
884                    if let Some(changeset) = self.actor.update_watches(&paths) {
885                        (self.actor.interrupted_by_events)(FilesystemEvent::Update(
886                            changeset, true,
887                        ));
888                    }
889                }
890                MatrixInput::UpstreamInvalidation {
891                    invalidates,
892                    opaque,
893                } => {
894                    self.actor.invalidate_upstream(UpstreamUpdateEvent {
895                        invalidates,
896                        opaque: Box::new(opaque),
897                    });
898                }
899                MatrixInput::WatcherEvent { kind, paths } => {
900                    self.actor.notify_event(notify_event(kind, paths));
901                }
902                MatrixInput::DelayedRecheck(path) => {
903                    let recheck_at = self.pending_recheck_at(&path);
904                    self.force_recheck_at(path, recheck_at).await;
905                }
906                MatrixInput::DelayedRecheckAt { path, recheck_at } => {
907                    self.force_recheck_at(path, recheck_at).await;
908                }
909                MatrixInput::PollMissing => {
910                    self.actor.poll_missing_watches();
911                }
912            }
913        }
914
915        fn pending_recheck_at(&self, path: &ImmutPath) -> usize {
916            match self
917                .actor
918                .watched_entries
919                .get(path)
920                .expect("watched entry must exist for delayed recheck")
921                .state
922            {
923                WatchState::EmptyOrRemoval { recheck_at, .. } => recheck_at,
924                WatchState::Stable => panic!("watched entry must be pending recheck"),
925            }
926        }
927
928        async fn force_recheck_at(&mut self, path: ImmutPath, recheck_at: usize) {
929            self.actor
930                .recheck_notify_event(UndeterminedNotifyEvent {
931                    at_realtime: tinymist_std::time::Instant::now()
932                        - tinymist_std::time::Duration::from_millis(60),
933                    at_logical_tick: recheck_at,
934                    path,
935                })
936                .await;
937        }
938
939        fn take_events(&self) -> Vec<FilesystemEvent> {
940            std::mem::take(&mut *self.events.lock().expect("test event sink poisoned"))
941        }
942
943        fn assert_no_events(&self) {
944            assert!(
945                self.events
946                    .lock()
947                    .expect("test event sink poisoned")
948                    .is_empty(),
949                "expected no filesystem events"
950            );
951        }
952
953        fn take_commands(&self) -> Vec<FakeWatchCommand> {
954            self.commands.take()
955        }
956
957        fn assert_watching(&self, path: &ImmutPath, expected: bool) {
958            assert_eq!(
959                self.actor
960                    .watched_entries
961                    .get(path)
962                    .map(|entry| entry.watching),
963                Some(expected)
964            );
965        }
966    }
967
968    #[tokio::test(flavor = "current_thread")]
969    async fn sync_dependency_updates_watch_set_and_changed_contents() {
970        let mut harness = NotifyActorHarness::new();
971        let first = test_path("sync-first.typ");
972        let second = test_path("sync-second.typ");
973
974        harness.access.set_content(&first, "first-v1");
975        harness.access.set_content(&second, "second-v1");
976        harness
977            .apply(MatrixInput::SyncDependency(vec![
978                first.clone(),
979                second.clone(),
980            ]))
981            .await;
982
983        assert_eq!(harness.take_commands(), vec![watch(&first), watch(&second)]);
984        let events = harness.take_events();
985        assert_eq!(events.len(), 1);
986        assert_update(
987            &events[0],
988            true,
989            &[
990                (&first, ExpectedSnapshot::Content("first-v1")),
991                (&second, ExpectedSnapshot::Content("second-v1")),
992            ],
993        );
994
995        harness.access.set_content(&first, "first-v2");
996        harness
997            .apply(MatrixInput::SyncDependency(vec![
998                first.clone(),
999                second.clone(),
1000            ]))
1001            .await;
1002
1003        assert_eq!(harness.take_commands(), Vec::new());
1004        let events = harness.take_events();
1005        assert_eq!(events.len(), 1);
1006        assert_update(
1007            &events[0],
1008            true,
1009            &[(&first, ExpectedSnapshot::Content("first-v2"))],
1010        );
1011
1012        harness
1013            .apply(MatrixInput::SyncDependency(vec![first.clone()]))
1014            .await;
1015
1016        assert_eq!(harness.take_commands(), vec![unwatch(&second)]);
1017        harness.assert_no_events();
1018
1019        harness.access.set_content(&second, "second-v2");
1020        harness
1021            .apply(MatrixInput::SyncDependency(vec![
1022                first.clone(),
1023                second.clone(),
1024            ]))
1025            .await;
1026
1027        assert_eq!(harness.take_commands(), vec![watch(&second)]);
1028        let events = harness.take_events();
1029        assert_eq!(events.len(), 1);
1030        assert_update(
1031            &events[0],
1032            true,
1033            &[(&second, ExpectedSnapshot::Content("second-v2"))],
1034        );
1035    }
1036
1037    #[tokio::test(flavor = "current_thread")]
1038    async fn create_and_modify_events_update_watched_dependencies() {
1039        let mut harness = NotifyActorHarness::new();
1040        let dep = test_path("create-modify.typ");
1041
1042        harness.access.set_content(&dep, "initial");
1043        harness
1044            .apply(MatrixInput::SyncDependency(vec![dep.clone()]))
1045            .await;
1046        harness.take_events();
1047        harness.take_commands();
1048
1049        harness.access.set_content(&dep, "created");
1050        harness
1051            .apply(MatrixInput::WatcherEvent {
1052                kind: notify::EventKind::Create(CreateKind::File),
1053                paths: vec![dep.clone()],
1054            })
1055            .await;
1056
1057        let events = harness.take_events();
1058        assert_eq!(events.len(), 1);
1059        assert_update(
1060            &events[0],
1061            false,
1062            &[(&dep, ExpectedSnapshot::Content("created"))],
1063        );
1064
1065        harness.access.set_content(&dep, "modified");
1066        harness
1067            .apply(MatrixInput::WatcherEvent {
1068                kind: modify_data_event(),
1069                paths: vec![dep.clone()],
1070            })
1071            .await;
1072
1073        let events = harness.take_events();
1074        assert_eq!(events.len(), 1);
1075        assert_update(
1076            &events[0],
1077            false,
1078            &[(&dep, ExpectedSnapshot::Content("modified"))],
1079        );
1080
1081        harness
1082            .apply(MatrixInput::WatcherEvent {
1083                kind: modify_data_event(),
1084                paths: vec![dep.clone()],
1085            })
1086            .await;
1087
1088        harness.assert_no_events();
1089        assert_eq!(harness.take_commands(), Vec::new());
1090    }
1091
1092    #[tokio::test(flavor = "current_thread")]
1093    async fn raw_events_for_unwatched_paths_are_ignored() {
1094        let mut harness = NotifyActorHarness::new();
1095        let watched = test_path("watched.typ");
1096        let unwatched = test_path("unwatched.typ");
1097
1098        harness.access.set_content(&watched, "watched");
1099        harness.access.set_content(&unwatched, "unwatched");
1100        harness
1101            .apply(MatrixInput::SyncDependency(vec![watched.clone()]))
1102            .await;
1103        harness.take_events();
1104        harness.take_commands();
1105
1106        harness.access.set_content(&unwatched, "unwatched-change");
1107        harness
1108            .apply(MatrixInput::WatcherEvent {
1109                kind: modify_data_event(),
1110                paths: vec![unwatched],
1111            })
1112            .await;
1113
1114        harness.assert_no_events();
1115        assert_eq!(harness.take_commands(), Vec::new());
1116    }
1117
1118    #[tokio::test(flavor = "current_thread")]
1119    async fn irrelevant_events_are_ignored_without_rereading_watched_file() {
1120        let mut harness = NotifyActorHarness::new();
1121        let dep = test_path("irrelevant-events.typ");
1122
1123        harness.access.set_content(&dep, "stable");
1124        harness
1125            .apply(MatrixInput::SyncDependency(vec![dep.clone()]))
1126            .await;
1127        harness.take_events();
1128        harness.take_commands();
1129
1130        let irrelevant_kinds = [
1131            notify::EventKind::Access(notify::event::AccessKind::Open(
1132                notify::event::AccessMode::Any,
1133            )),
1134            notify::EventKind::Access(notify::event::AccessKind::Close(
1135                notify::event::AccessMode::Read,
1136            )),
1137            notify::EventKind::Modify(notify::event::ModifyKind::Metadata(
1138                notify::event::MetadataKind::Any,
1139            )),
1140            notify::EventKind::Modify(notify::event::ModifyKind::Other),
1141            notify::EventKind::Other,
1142        ];
1143
1144        for kind in irrelevant_kinds {
1145            let reads_before_event = harness.access.read_count(&dep);
1146            harness
1147                .apply(MatrixInput::WatcherEvent {
1148                    kind,
1149                    paths: vec![dep.clone()],
1150                })
1151                .await;
1152
1153            harness.assert_no_events();
1154            assert_eq!(harness.take_commands(), Vec::new());
1155            assert_eq!(harness.access.read_count(&dep), reads_before_event);
1156        }
1157    }
1158
1159    #[tokio::test(flavor = "current_thread")]
1160    async fn remove_and_rename_from_reset_watch_state_and_confirm_changes() {
1161        let mut harness = NotifyActorHarness::new();
1162        let dep = test_path("remove-rename-from.typ");
1163
1164        harness.access.set_content(&dep, "alive");
1165        harness
1166            .apply(MatrixInput::SyncDependency(vec![dep.clone()]))
1167            .await;
1168        harness.take_events();
1169        harness.take_commands();
1170
1171        harness.access.set_missing(&dep);
1172        harness
1173            .apply(MatrixInput::WatcherEvent {
1174                kind: notify::EventKind::Remove(RemoveKind::File),
1175                paths: vec![dep.clone()],
1176            })
1177            .await;
1178
1179        assert_eq!(harness.take_commands(), vec![unwatch(&dep)]);
1180        harness.assert_watching(&dep, false);
1181        harness.assert_no_events();
1182
1183        harness
1184            .apply(MatrixInput::DelayedRecheck(dep.clone()))
1185            .await;
1186
1187        let events = harness.take_events();
1188        assert_eq!(events.len(), 1);
1189        assert_update(&events[0], false, &[(&dep, ExpectedSnapshot::NotFound)]);
1190
1191        harness.access.set_content(&dep, "restored");
1192        harness
1193            .apply(MatrixInput::SyncDependency(vec![dep.clone()]))
1194            .await;
1195
1196        assert_eq!(harness.take_commands(), vec![watch(&dep)]);
1197        harness.assert_watching(&dep, true);
1198        let events = harness.take_events();
1199        assert_eq!(events.len(), 1);
1200        assert_update(
1201            &events[0],
1202            true,
1203            &[(&dep, ExpectedSnapshot::Content("restored"))],
1204        );
1205
1206        harness.access.set_missing(&dep);
1207        harness
1208            .apply(MatrixInput::WatcherEvent {
1209                kind: notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)),
1210                paths: vec![dep.clone()],
1211            })
1212            .await;
1213
1214        assert_eq!(harness.take_commands(), vec![unwatch(&dep)]);
1215        harness.assert_watching(&dep, false);
1216        harness.assert_no_events();
1217
1218        harness
1219            .apply(MatrixInput::DelayedRecheck(dep.clone()))
1220            .await;
1221
1222        let events = harness.take_events();
1223        assert_eq!(events.len(), 1);
1224        assert_update(&events[0], false, &[(&dep, ExpectedSnapshot::NotFound)]);
1225    }
1226
1227    #[tokio::test(flavor = "current_thread")]
1228    async fn missing_poll_rewatches_recreated_dependency_without_notify_event() {
1229        let mut harness = NotifyActorHarness::new();
1230        let dep = test_path("missing-poll-recreate.typ");
1231
1232        harness.access.set_content(&dep, "alive");
1233        harness
1234            .apply(MatrixInput::SyncDependency(vec![dep.clone()]))
1235            .await;
1236        harness.take_events();
1237        harness.take_commands();
1238
1239        harness.access.set_missing(&dep);
1240        harness
1241            .apply(MatrixInput::WatcherEvent {
1242                kind: notify::EventKind::Remove(RemoveKind::File),
1243                paths: vec![dep.clone()],
1244            })
1245            .await;
1246        assert_eq!(harness.take_commands(), vec![unwatch(&dep)]);
1247
1248        harness
1249            .apply(MatrixInput::DelayedRecheck(dep.clone()))
1250            .await;
1251        let events = harness.take_events();
1252        assert_eq!(events.len(), 1);
1253        assert_update(&events[0], false, &[(&dep, ExpectedSnapshot::NotFound)]);
1254
1255        harness.access.set_content(&dep, "recreated");
1256        harness.apply(MatrixInput::PollMissing).await;
1257
1258        assert_eq!(harness.take_commands(), vec![watch(&dep)]);
1259        harness.assert_watching(&dep, true);
1260        let events = harness.take_events();
1261        assert_eq!(events.len(), 1);
1262        assert_update(
1263            &events[0],
1264            false,
1265            &[(&dep, ExpectedSnapshot::Content("recreated"))],
1266        );
1267    }
1268
1269    #[tokio::test(flavor = "current_thread")]
1270    async fn rename_to_paired_rename_and_multi_path_events_are_mapped() {
1271        let mut harness = NotifyActorHarness::new();
1272        let rename_to = test_path("rename-to.typ");
1273        let rename_from = test_path("paired-from.typ");
1274        let paired_to = test_path("paired-to.typ");
1275        let multi_first = test_path("multi-first.typ");
1276        let multi_second = test_path("multi-second.typ");
1277        let ignored = test_path("multi-ignored.typ");
1278
1279        for path in [
1280            &rename_to,
1281            &rename_from,
1282            &paired_to,
1283            &multi_first,
1284            &multi_second,
1285        ] {
1286            harness.access.set_content(path, "initial");
1287        }
1288        harness.access.set_content(&ignored, "ignored");
1289        harness
1290            .apply(MatrixInput::SyncDependency(vec![
1291                rename_to.clone(),
1292                rename_from.clone(),
1293                paired_to.clone(),
1294                multi_first.clone(),
1295                multi_second.clone(),
1296            ]))
1297            .await;
1298        harness.take_events();
1299        harness.take_commands();
1300
1301        harness.access.set_content(&rename_to, "rename-to-content");
1302        harness
1303            .apply(MatrixInput::WatcherEvent {
1304                kind: notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)),
1305                paths: vec![rename_to.clone()],
1306            })
1307            .await;
1308
1309        let events = harness.take_events();
1310        assert_eq!(events.len(), 1);
1311        assert_update(
1312            &events[0],
1313            false,
1314            &[(&rename_to, ExpectedSnapshot::Content("rename-to-content"))],
1315        );
1316        assert_eq!(harness.take_commands(), Vec::new());
1317
1318        harness.access.set_missing(&rename_from);
1319        harness.access.set_content(&paired_to, "paired-to-content");
1320        harness
1321            .apply(MatrixInput::WatcherEvent {
1322                kind: notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
1323                paths: vec![rename_from.clone(), paired_to.clone()],
1324            })
1325            .await;
1326
1327        let events = harness.take_events();
1328        assert_eq!(events.len(), 1);
1329        assert_update(
1330            &events[0],
1331            false,
1332            &[(&paired_to, ExpectedSnapshot::Content("paired-to-content"))],
1333        );
1334        assert_eq!(harness.take_commands(), Vec::new());
1335
1336        harness
1337            .apply(MatrixInput::DelayedRecheck(rename_from.clone()))
1338            .await;
1339
1340        let events = harness.take_events();
1341        assert_eq!(events.len(), 1);
1342        assert_update(
1343            &events[0],
1344            false,
1345            &[(&rename_from, ExpectedSnapshot::NotFound)],
1346        );
1347
1348        harness
1349            .access
1350            .set_content(&multi_first, "multi-first-content");
1351        harness
1352            .access
1353            .set_content(&multi_second, "multi-second-content");
1354        harness.access.set_content(&ignored, "ignored-content");
1355        harness
1356            .apply(MatrixInput::WatcherEvent {
1357                kind: modify_data_event(),
1358                paths: vec![multi_first.clone(), multi_second.clone(), ignored],
1359            })
1360            .await;
1361
1362        let events = harness.take_events();
1363        assert_eq!(events.len(), 1);
1364        assert_update(
1365            &events[0],
1366            false,
1367            &[
1368                (
1369                    &multi_first,
1370                    ExpectedSnapshot::Content("multi-first-content"),
1371                ),
1372                (
1373                    &multi_second,
1374                    ExpectedSnapshot::Content("multi-second-content"),
1375                ),
1376            ],
1377        );
1378        assert_eq!(harness.take_commands(), Vec::new());
1379    }
1380
1381    #[tokio::test(flavor = "current_thread")]
1382    async fn unstable_reads_delay_confirmation_and_recover_before_recheck() {
1383        let mut harness = NotifyActorHarness::new();
1384        let empty = test_path("unstable-empty.typ");
1385        let missing = test_path("unstable-missing.typ");
1386        let errored = test_path("unstable-error.typ");
1387        let recovery = test_path("unstable-recovery.typ");
1388
1389        for path in [&empty, &missing, &errored, &recovery] {
1390            harness.access.set_content(path, "stable");
1391        }
1392        harness
1393            .apply(MatrixInput::SyncDependency(vec![
1394                empty.clone(),
1395                missing.clone(),
1396                errored.clone(),
1397                recovery.clone(),
1398            ]))
1399            .await;
1400        harness.take_events();
1401        harness.take_commands();
1402
1403        harness.access.set_empty(&empty);
1404        harness
1405            .apply(MatrixInput::WatcherEvent {
1406                kind: modify_data_event(),
1407                paths: vec![empty.clone()],
1408            })
1409            .await;
1410        harness.assert_no_events();
1411        harness
1412            .apply(MatrixInput::DelayedRecheck(empty.clone()))
1413            .await;
1414
1415        let events = harness.take_events();
1416        assert_eq!(events.len(), 1);
1417        assert_update(
1418            &events[0],
1419            false,
1420            &[(&empty, ExpectedSnapshot::Content(""))],
1421        );
1422
1423        harness.access.set_missing(&missing);
1424        harness
1425            .apply(MatrixInput::WatcherEvent {
1426                kind: modify_data_event(),
1427                paths: vec![missing.clone()],
1428            })
1429            .await;
1430        harness.assert_no_events();
1431        harness
1432            .apply(MatrixInput::DelayedRecheck(missing.clone()))
1433            .await;
1434
1435        let events = harness.take_events();
1436        assert_eq!(events.len(), 1);
1437        assert_update(&events[0], false, &[(&missing, ExpectedSnapshot::NotFound)]);
1438
1439        harness.access.set_error(&errored);
1440        harness
1441            .apply(MatrixInput::WatcherEvent {
1442                kind: modify_data_event(),
1443                paths: vec![errored.clone()],
1444            })
1445            .await;
1446        harness.assert_no_events();
1447        harness
1448            .apply(MatrixInput::DelayedRecheck(errored.clone()))
1449            .await;
1450
1451        let events = harness.take_events();
1452        assert_eq!(events.len(), 1);
1453        assert_update(&events[0], false, &[(&errored, ExpectedSnapshot::Other)]);
1454
1455        harness.access.set_empty(&recovery);
1456        harness
1457            .apply(MatrixInput::WatcherEvent {
1458                kind: modify_data_event(),
1459                paths: vec![recovery.clone()],
1460            })
1461            .await;
1462        harness.assert_no_events();
1463        let recovery_recheck_at = harness.pending_recheck_at(&recovery);
1464
1465        harness.access.set_content(&recovery, "recovered");
1466        harness
1467            .apply(MatrixInput::WatcherEvent {
1468                kind: modify_data_event(),
1469                paths: vec![recovery.clone()],
1470            })
1471            .await;
1472
1473        let events = harness.take_events();
1474        assert_eq!(events.len(), 1);
1475        assert_update(
1476            &events[0],
1477            false,
1478            &[(&recovery, ExpectedSnapshot::Content("recovered"))],
1479        );
1480
1481        harness
1482            .apply(MatrixInput::DelayedRecheckAt {
1483                path: recovery.clone(),
1484                recheck_at: recovery_recheck_at,
1485            })
1486            .await;
1487        harness.assert_no_events();
1488    }
1489
1490    #[tokio::test(flavor = "current_thread")]
1491    async fn upstream_invalidation_refreshes_watches_and_carries_payload() {
1492        let mut harness = NotifyActorHarness::new();
1493        let existing = test_path("upstream-existing.typ");
1494        let added = test_path("upstream-added.typ");
1495
1496        harness.access.set_content(&existing, "existing-v1");
1497        harness
1498            .apply(MatrixInput::SyncDependency(vec![existing.clone()]))
1499            .await;
1500        harness.take_events();
1501        harness.take_commands();
1502
1503        harness.access.set_content(&existing, "existing-v2");
1504        harness.access.set_content(&added, "added-v1");
1505        harness
1506            .apply(MatrixInput::UpstreamInvalidation {
1507                invalidates: vec![existing.clone(), added.clone()],
1508                opaque: 42,
1509            })
1510            .await;
1511
1512        assert_eq!(harness.take_commands(), vec![watch(&added)]);
1513        let events = harness.take_events();
1514        assert_eq!(events.len(), 1);
1515        assert_upstream_update(
1516            &events[0],
1517            &[
1518                (&existing, ExpectedSnapshot::Content("existing-v2")),
1519                (&added, ExpectedSnapshot::Content("added-v1")),
1520            ],
1521            &[existing.clone(), added.clone()],
1522            42,
1523        );
1524    }
1525
1526    #[tokio::test(flavor = "current_thread")]
1527    #[ignore = "uses the host filesystem watcher; CI runs real_fs_* explicitly"]
1528    async fn real_fs_sync_dependency_updates_and_readds_dependencies() {
1529        let mut harness = RealFsHarness::new();
1530        let first = harness.write("sync-first.typ", "first-v1");
1531        let second = harness.write("sync-second.typ", "second-v1");
1532        let third = harness.write("sync-third.typ", "third-v1");
1533
1534        harness.sync(&[first.clone(), second.clone()]);
1535        harness
1536            .expect_update_all(
1537                true,
1538                &[
1539                    (&first, ExpectedSnapshot::Content("first-v1")),
1540                    (&second, ExpectedSnapshot::Content("second-v1")),
1541                ],
1542            )
1543            .await;
1544
1545        harness.write_path(&first, "first-v2");
1546        harness
1547            .expect_update(&first, false, ExpectedSnapshot::Content("first-v2"))
1548            .await;
1549
1550        harness.sync(std::slice::from_ref(&first));
1551
1552        harness.sync(&[first.clone(), third.clone()]);
1553        harness
1554            .expect_update(&third, true, ExpectedSnapshot::Content("third-v1"))
1555            .await;
1556        harness.settle().await;
1557    }
1558
1559    #[tokio::test(flavor = "current_thread")]
1560    #[ignore = "uses the host filesystem watcher; CI runs real_fs_* explicitly"]
1561    async fn real_fs_modify_unwatched_and_multi_file_updates() {
1562        let mut harness = RealFsHarness::new();
1563        let watched = harness.write("watched.typ", "watched-v1");
1564        let other = harness.write("other.typ", "other-v1");
1565        let unwatched = harness.write("unwatched.typ", "unwatched-v1");
1566
1567        harness.sync(&[watched.clone(), other.clone()]);
1568        harness
1569            .expect_update_all(
1570                true,
1571                &[
1572                    (&watched, ExpectedSnapshot::Content("watched-v1")),
1573                    (&other, ExpectedSnapshot::Content("other-v1")),
1574                ],
1575            )
1576            .await;
1577
1578        harness.write_path(&watched, "watched-v2");
1579        harness
1580            .expect_update(&watched, false, ExpectedSnapshot::Content("watched-v2"))
1581            .await;
1582
1583        harness.drain_events();
1584        harness.write_path(&unwatched, "unwatched-v2");
1585        harness.expect_no_update(&unwatched).await;
1586
1587        harness.write_path(&watched, "watched-v3");
1588        harness.write_path(&other, "other-v2");
1589        harness
1590            .expect_update(&watched, false, ExpectedSnapshot::Content("watched-v3"))
1591            .await;
1592        harness
1593            .expect_update(&other, false, ExpectedSnapshot::Content("other-v2"))
1594            .await;
1595        harness.settle().await;
1596    }
1597
1598    #[tokio::test(flavor = "current_thread")]
1599    #[ignore = "uses the host filesystem watcher; CI runs real_fs_* explicitly"]
1600    async fn real_fs_remove_rename_away_and_readd_dependencies() {
1601        let mut harness = RealFsHarness::new();
1602        let remove = harness.write("remove.typ", "remove-v1");
1603        let rename = harness.write("rename-away.typ", "rename-v1");
1604        let renamed = harness.path("renamed-away.typ");
1605
1606        harness.sync(&[remove.clone(), rename.clone()]);
1607        harness
1608            .expect_update_all(
1609                true,
1610                &[
1611                    (&remove, ExpectedSnapshot::Content("remove-v1")),
1612                    (&rename, ExpectedSnapshot::Content("rename-v1")),
1613                ],
1614            )
1615            .await;
1616
1617        harness.remove(&remove);
1618        harness
1619            .expect_update(&remove, false, ExpectedSnapshot::NotFound)
1620            .await;
1621
1622        harness.write_path(&remove, "remove-v2");
1623        harness.sync(&[remove.clone(), rename.clone()]);
1624        harness
1625            .expect_update(&remove, true, ExpectedSnapshot::Content("remove-v2"))
1626            .await;
1627
1628        harness.rename(&rename, &renamed);
1629        harness
1630            .expect_update(&rename, false, ExpectedSnapshot::NotFound)
1631            .await;
1632        harness.settle().await;
1633    }
1634
1635    #[tokio::test(flavor = "current_thread")]
1636    #[ignore = "uses the host filesystem watcher; CI runs real_fs_* explicitly"]
1637    async fn real_fs_atomic_replace_empty_missing_and_recovery() {
1638        let mut harness = RealFsHarness::new();
1639        let atomic = harness.write("atomic.typ", "atomic-v1");
1640        let empty = harness.write("empty.typ", "stable");
1641        let missing = harness.write("missing.typ", "stable");
1642        let recovery = harness.write("recovery.typ", "stable");
1643
1644        harness.sync(&[
1645            atomic.clone(),
1646            empty.clone(),
1647            missing.clone(),
1648            recovery.clone(),
1649        ]);
1650        harness
1651            .expect_update_all(
1652                true,
1653                &[
1654                    (&atomic, ExpectedSnapshot::Content("atomic-v1")),
1655                    (&empty, ExpectedSnapshot::Content("stable")),
1656                    (&missing, ExpectedSnapshot::Content("stable")),
1657                    (&recovery, ExpectedSnapshot::Content("stable")),
1658                ],
1659            )
1660            .await;
1661
1662        let atomic_tmp = harness.write("atomic.tmp", "atomic-v2");
1663        harness.rename(&atomic_tmp, &atomic);
1664        harness
1665            .expect_update(&atomic, false, ExpectedSnapshot::Content("atomic-v2"))
1666            .await;
1667
1668        harness.write_path(&empty, "");
1669        harness
1670            .expect_update(&empty, false, ExpectedSnapshot::Content(""))
1671            .await;
1672
1673        harness.remove(&missing);
1674        harness
1675            .expect_update(&missing, false, ExpectedSnapshot::NotFound)
1676            .await;
1677
1678        harness.write_path(&recovery, "");
1679        harness.write_path(&recovery, "recovered");
1680        harness
1681            .expect_update(&recovery, false, ExpectedSnapshot::Content("recovered"))
1682            .await;
1683        harness.settle().await;
1684    }
1685
1686    #[tokio::test(flavor = "current_thread")]
1687    #[ignore = "uses the host filesystem watcher; CI runs real_fs_* explicitly"]
1688    async fn real_fs_upstream_invalidation_refreshes_watches() {
1689        let mut harness = RealFsHarness::new();
1690        let existing = harness.write("upstream-existing.typ", "existing-v1");
1691        let added = harness.write("upstream-added.typ", "added-v1");
1692
1693        harness.sync(std::slice::from_ref(&existing));
1694        harness
1695            .expect_update(&existing, true, ExpectedSnapshot::Content("existing-v1"))
1696            .await;
1697
1698        harness.write_path(&existing, "existing-v2");
1699        harness.upstream(&[existing.clone(), added.clone()], 7);
1700        harness
1701            .expect_upstream_after_optional_updates(
1702                &[(&added, ExpectedSnapshot::Content("added-v1"))],
1703                &[(&existing, ExpectedSnapshot::Content("existing-v2"))],
1704                &[existing.clone(), added.clone()],
1705                7,
1706            )
1707            .await;
1708        harness.write_path(&added, "added-v2");
1709        harness
1710            .expect_update(&added, false, ExpectedSnapshot::Content("added-v2"))
1711            .await;
1712        harness.settle().await;
1713    }
1714
1715    #[derive(Debug, Clone, Copy)]
1716    enum ExpectedSnapshot<'a> {
1717        Content(&'a str),
1718        NotFound,
1719        Other,
1720    }
1721
1722    fn test_path(name: &str) -> ImmutPath {
1723        Arc::from(
1724            PathBuf::from("/tinymist-notify-actor-test")
1725                .join(name)
1726                .into_boxed_path(),
1727        )
1728    }
1729
1730    fn notify_event(kind: notify::EventKind, paths: Vec<ImmutPath>) -> notify::Event {
1731        paths
1732            .into_iter()
1733            .fold(notify::Event::new(kind), |event, path| {
1734                event.add_path(path.as_ref().to_path_buf())
1735            })
1736    }
1737
1738    fn modify_data_event() -> notify::EventKind {
1739        notify::EventKind::Modify(ModifyKind::Data(DataChange::Content))
1740    }
1741
1742    fn content_snapshot(content: &str) -> FileSnapshot {
1743        Ok::<Bytes, FileError>(Bytes::from_string(content.to_owned())).into()
1744    }
1745
1746    fn watch(path: &ImmutPath) -> FakeWatchCommand {
1747        FakeWatchCommand::Watch(path.as_ref().to_path_buf())
1748    }
1749
1750    fn unwatch(path: &ImmutPath) -> FakeWatchCommand {
1751        FakeWatchCommand::Unwatch(path.as_ref().to_path_buf())
1752    }
1753
1754    fn assert_update(
1755        event: &FilesystemEvent,
1756        expected_is_sync: bool,
1757        expected: &[(&ImmutPath, ExpectedSnapshot<'_>)],
1758    ) {
1759        let FilesystemEvent::Update(changeset, is_sync) = event else {
1760            panic!("expected update event, got {event:?}");
1761        };
1762
1763        assert_eq!(*is_sync, expected_is_sync);
1764        assert_changeset(changeset, expected);
1765    }
1766
1767    struct RealFsHarness {
1768        _dir: tempfile::TempDir,
1769        sender: mpsc::UnboundedSender<NotifyMessage>,
1770        events_recv: mpsc::UnboundedReceiver<FilesystemEvent>,
1771        handle: tokio::task::JoinHandle<()>,
1772    }
1773
1774    impl RealFsHarness {
1775        fn new() -> Self {
1776            let dir = tempfile::tempdir().expect("tempdir should be created");
1777            let (sender, inbox) = mpsc::unbounded_channel();
1778            let (events_send, events_recv) = mpsc::unbounded_channel();
1779            let handle = spawn_watch_deps(inbox, move |event| {
1780                events_send
1781                    .send(event)
1782                    .expect("real watcher event receiver should stay open");
1783            });
1784
1785            Self {
1786                _dir: dir,
1787                sender,
1788                events_recv,
1789                handle,
1790            }
1791        }
1792
1793        fn path(&self, name: &str) -> ImmutPath {
1794            Arc::from(self._dir.path().join(name).into_boxed_path())
1795        }
1796
1797        fn write(&self, name: &str, content: &str) -> ImmutPath {
1798            let path = self.path(name);
1799            self.write_path(&path, content);
1800            path
1801        }
1802
1803        fn write_path(&self, path: &ImmutPath, content: &str) {
1804            std::fs::write(path.as_ref(), content).expect("temp file should be written");
1805        }
1806
1807        fn remove(&self, path: &ImmutPath) {
1808            std::fs::remove_file(path.as_ref()).expect("temp file should be removed");
1809        }
1810
1811        fn rename(&self, from: &ImmutPath, to: &ImmutPath) {
1812            std::fs::rename(from.as_ref(), to.as_ref()).expect("temp file should be renamed");
1813        }
1814
1815        fn sync(&self, paths: &[ImmutPath]) {
1816            self.sender
1817                .send(NotifyMessage::SyncDependency(Box::new(paths.to_vec())))
1818                .expect("sync dependency send should succeed");
1819        }
1820
1821        fn upstream(&self, invalidates: &[ImmutPath], opaque: usize) {
1822            self.sender
1823                .send(NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
1824                    invalidates: invalidates.to_vec(),
1825                    opaque: Box::new(opaque),
1826                }))
1827                .expect("upstream update send should succeed");
1828        }
1829
1830        async fn expect_update(
1831            &mut self,
1832            expected_path: &ImmutPath,
1833            expected_is_sync: bool,
1834            expected: ExpectedSnapshot<'_>,
1835        ) {
1836            self.expect_event(
1837                || {
1838                    format!(
1839                        "update path={expected_path:?}, is_sync={expected_is_sync}, snapshot={expected:?}"
1840                    )
1841                },
1842                |event| update_contains(event, expected_path, expected_is_sync, expected),
1843            )
1844            .await;
1845        }
1846
1847        async fn expect_update_all(
1848            &mut self,
1849            expected_is_sync: bool,
1850            expected: &[(&ImmutPath, ExpectedSnapshot<'_>)],
1851        ) {
1852            self.expect_event(
1853                || format!("update is_sync={expected_is_sync}, snapshots={expected:?}"),
1854                |event| update_contains_all(event, expected_is_sync, expected),
1855            )
1856            .await;
1857        }
1858
1859        async fn expect_upstream_after_optional_updates(
1860            &mut self,
1861            required_upstream: &[(&ImmutPath, ExpectedSnapshot<'_>)],
1862            optional_prior_updates: &[(&ImmutPath, ExpectedSnapshot<'_>)],
1863            expected_invalidates: &[ImmutPath],
1864            expected_opaque: usize,
1865        ) {
1866            let mut seen_prior = vec![false; optional_prior_updates.len()];
1867
1868            self.expect_event(
1869                || {
1870                    format!(
1871                        "upstream required={required_upstream:?}, optional_prior={optional_prior_updates:?}"
1872                    )
1873                },
1874                |event| {
1875                    if let FilesystemEvent::Update(changeset, false) = event {
1876                        for (seen, (path, snapshot)) in
1877                            seen_prior.iter_mut().zip(optional_prior_updates)
1878                        {
1879                            if changeset_contains(changeset, path, *snapshot) {
1880                                *seen = true;
1881                            }
1882                        }
1883
1884                        return false;
1885                    }
1886
1887                    let FilesystemEvent::UpstreamUpdate {
1888                        changeset,
1889                        upstream_event: Some(upstream_event),
1890                    } = event
1891                    else {
1892                        return false;
1893                    };
1894
1895                    upstream_event
1896                        .opaque
1897                        .downcast_ref::<usize>()
1898                        .is_some_and(|opaque| *opaque == expected_opaque)
1899                        && upstream_event.invalidates.as_slice() == expected_invalidates
1900                        && required_upstream
1901                            .iter()
1902                            .all(|(path, snapshot)| changeset_contains(changeset, path, *snapshot))
1903                        && optional_prior_updates.iter().enumerate().all(
1904                            |(idx, (path, snapshot))| {
1905                                seen_prior[idx]
1906                                    || changeset_contains(changeset, path, *snapshot)
1907                            },
1908                        )
1909                },
1910            )
1911            .await;
1912        }
1913
1914        async fn expect_no_update(&mut self, expected_path: &ImmutPath) {
1915            let res = tokio::time::timeout(std::time::Duration::from_millis(250), async {
1916                loop {
1917                    let event = self
1918                        .events_recv
1919                        .recv()
1920                        .await
1921                        .expect("real watcher event sender should stay open");
1922                    if update_mentions_path(&event, expected_path) {
1923                        panic!("unexpected real watcher update for {expected_path:?}: {event:?}");
1924                    }
1925                }
1926            })
1927            .await;
1928            assert!(
1929                res.is_err(),
1930                "no-update wait should end by timeout, not by matching an event"
1931            );
1932        }
1933
1934        async fn expect_event(
1935            &mut self,
1936            description: impl Fn() -> String,
1937            mut matches: impl FnMut(&FilesystemEvent) -> bool,
1938        ) {
1939            let mut last_event = None;
1940            let res = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1941                loop {
1942                    let event = self
1943                        .events_recv
1944                        .recv()
1945                        .await
1946                        .expect("real watcher event sender should stay open");
1947                    if matches(&event) {
1948                        return;
1949                    }
1950                    last_event = Some(format!("{event:?}"));
1951                }
1952            })
1953            .await;
1954
1955            if res.is_err() {
1956                panic!(
1957                    "timed out waiting for real watcher {}; last event: {}",
1958                    description(),
1959                    last_event.unwrap_or_else(|| "<none>".to_owned())
1960                );
1961            }
1962        }
1963
1964        fn drain_events(&mut self) {
1965            while self.events_recv.try_recv().is_ok() {}
1966        }
1967
1968        async fn settle(self) {
1969            self.sender
1970                .send(NotifyMessage::Settle)
1971                .expect("settle send should succeed");
1972            tokio::time::timeout(std::time::Duration::from_millis(500), self.handle)
1973                .await
1974                .expect("production notify actor did not settle")
1975                .expect("production notify actor task failed");
1976        }
1977    }
1978
1979    fn update_contains(
1980        event: &FilesystemEvent,
1981        expected_path: &ImmutPath,
1982        expected_is_sync: bool,
1983        expected: ExpectedSnapshot<'_>,
1984    ) -> bool {
1985        let FilesystemEvent::Update(changeset, is_sync) = event else {
1986            return false;
1987        };
1988        *is_sync == expected_is_sync && changeset_contains(changeset, expected_path, expected)
1989    }
1990
1991    fn update_contains_all(
1992        event: &FilesystemEvent,
1993        expected_is_sync: bool,
1994        expected: &[(&ImmutPath, ExpectedSnapshot<'_>)],
1995    ) -> bool {
1996        let FilesystemEvent::Update(changeset, is_sync) = event else {
1997            return false;
1998        };
1999        *is_sync == expected_is_sync
2000            && expected
2001                .iter()
2002                .all(|(path, snapshot)| changeset_contains(changeset, path, *snapshot))
2003    }
2004
2005    fn update_mentions_path(event: &FilesystemEvent, expected_path: &ImmutPath) -> bool {
2006        let FilesystemEvent::Update(changeset, ..) = event else {
2007            return false;
2008        };
2009
2010        changeset
2011            .inserts
2012            .iter()
2013            .any(|(path, _)| path == expected_path)
2014            || changeset.removes.iter().any(|path| path == expected_path)
2015    }
2016
2017    fn changeset_contains(
2018        changeset: &FileChangeSet,
2019        expected_path: &ImmutPath,
2020        expected: ExpectedSnapshot<'_>,
2021    ) -> bool {
2022        changeset.inserts.iter().any(|(path, snapshot)| {
2023            path == expected_path && snapshot_matches(snapshot, expected_path, expected)
2024        })
2025    }
2026
2027    fn snapshot_matches(
2028        snapshot: &FileSnapshot,
2029        expected_path: &ImmutPath,
2030        expected: ExpectedSnapshot<'_>,
2031    ) -> bool {
2032        match expected {
2033            ExpectedSnapshot::Content(content) => snapshot
2034                .content()
2035                .is_ok_and(|bytes| bytes.as_slice() == content.as_bytes()),
2036            ExpectedSnapshot::NotFound => {
2037                let Err(err) = snapshot.as_ref() else {
2038                    return false;
2039                };
2040                let FileError::NotFound(actual_path) = err.as_ref() else {
2041                    return false;
2042                };
2043                actual_path.as_path() == expected_path.as_ref()
2044            }
2045            ExpectedSnapshot::Other => snapshot
2046                .as_ref()
2047                .is_err_and(|err| matches!(err.as_ref(), FileError::Other(_))),
2048        }
2049    }
2050
2051    fn assert_upstream_update(
2052        event: &FilesystemEvent,
2053        expected: &[(&ImmutPath, ExpectedSnapshot<'_>)],
2054        expected_invalidates: &[ImmutPath],
2055        expected_opaque: usize,
2056    ) {
2057        let FilesystemEvent::UpstreamUpdate {
2058            changeset,
2059            upstream_event: Some(upstream_event),
2060        } = event
2061        else {
2062            panic!("expected upstream update event, got {event:?}");
2063        };
2064
2065        assert_changeset(changeset, expected);
2066        assert_eq!(upstream_event.invalidates, expected_invalidates);
2067        assert_eq!(
2068            upstream_event
2069                .opaque
2070                .downcast_ref::<usize>()
2071                .copied()
2072                .expect("opaque payload should be usize"),
2073            expected_opaque
2074        );
2075    }
2076
2077    fn assert_changeset(
2078        changeset: &FileChangeSet,
2079        expected: &[(&ImmutPath, ExpectedSnapshot<'_>)],
2080    ) {
2081        assert_eq!(changeset.removes, Vec::<ImmutPath>::new());
2082        assert_eq!(changeset.inserts.len(), expected.len());
2083
2084        for ((actual_path, actual_snapshot), (expected_path, expected_snapshot)) in
2085            changeset.inserts.iter().zip(expected)
2086        {
2087            assert_eq!(actual_path, *expected_path);
2088            assert_snapshot(expected_path, actual_snapshot, *expected_snapshot);
2089        }
2090    }
2091
2092    fn assert_snapshot(path: &ImmutPath, snapshot: &FileSnapshot, expected: ExpectedSnapshot<'_>) {
2093        match expected {
2094            ExpectedSnapshot::Content(content) => {
2095                let bytes = snapshot.content().expect("expected file content");
2096                assert_eq!(bytes.as_slice(), content.as_bytes());
2097            }
2098            ExpectedSnapshot::NotFound => {
2099                let Err(err) = snapshot.as_ref() else {
2100                    panic!("expected not found snapshot for {path:?}");
2101                };
2102                let FileError::NotFound(actual_path) = err.as_ref() else {
2103                    panic!("expected not found snapshot for {path:?}, got {err:?}");
2104                };
2105                assert_eq!(actual_path.as_path(), path.as_ref());
2106            }
2107            ExpectedSnapshot::Other => {
2108                let Err(err) = snapshot.as_ref() else {
2109                    panic!("expected other-error snapshot for {path:?}");
2110                };
2111                assert!(
2112                    matches!(err.as_ref(), FileError::Other(_)),
2113                    "expected other-error snapshot for {path:?}, got {err:?}"
2114                );
2115            }
2116        }
2117    }
2118}