Skip to main content

origin_platform/
memory_watcher.rs

1//! Memory double for [`WorkspaceWatcher`] — pushes synthetic changes for tests.
2//!
3//! Never use this in a shipped application.
4
5use crate::watcher::{BUFFER_SIZE, WatchHandle, WorkspaceChange, WorkspaceWatcher};
6use crate::workspace::WorkspaceRoot;
7use async_trait::async_trait;
8use origin_domain::Result;
9use std::collections::HashMap;
10use std::sync::Mutex;
11use tokio::sync::broadcast;
12
13/// Records watcher registrations and allows tests to push synthetic change events.
14#[derive(Debug, Default)]
15pub struct MemoryWorkspaceWatcher {
16    senders: Mutex<HashMap<WorkspaceRoot, broadcast::Sender<WorkspaceChange>>>,
17}
18
19impl MemoryWorkspaceWatcher {
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Push a synthetic change into the watcher — as if the filesystem changed.
25    /// Returns `true` if at least one subscriber received it.
26    pub fn emit(&self, root: &WorkspaceRoot, change: WorkspaceChange) -> usize {
27        let senders = self.senders.lock().expect("poisoned");
28        match senders.get(root) {
29            Some(sender) => sender.send(change).unwrap_or(0),
30            None => 0,
31        }
32    }
33}
34
35#[async_trait]
36impl WorkspaceWatcher for MemoryWorkspaceWatcher {
37    async fn watch(&self, root: &WorkspaceRoot) -> Result<WatchHandle> {
38        let mut senders = self.senders.lock().expect("poisoned");
39        let sender = match senders.get(root) {
40            Some(existing) => existing.clone(),
41            None => {
42                let (tx, _) = broadcast::channel(BUFFER_SIZE);
43                senders.insert(root.clone(), tx.clone());
44                tx
45            }
46        };
47
48        Ok(WatchHandle::new(sender.subscribe()))
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use std::path::Path;
56
57    #[tokio::test]
58    async fn watching_twice_returns_different_handles_same_channel() {
59        let watcher = MemoryWorkspaceWatcher::new();
60        let root = WorkspaceRoot::new(Path::new("/repo").to_path_buf()).unwrap();
61
62        let mut handle_a = watcher.watch(&root).await.unwrap();
63        let mut handle_b = watcher.watch(&root).await.unwrap();
64
65        watcher.emit(&root, WorkspaceChange::modified("README.md"));
66
67        assert_eq!(handle_a.recv().await.unwrap().path(), "README.md");
68        assert_eq!(handle_b.recv().await.unwrap().path(), "README.md");
69    }
70
71    #[tokio::test]
72    async fn different_roots_get_different_channels() {
73        let watcher = MemoryWorkspaceWatcher::new();
74        let root_a = WorkspaceRoot::new(Path::new("/a").to_path_buf()).unwrap();
75        let root_b = WorkspaceRoot::new(Path::new("/b").to_path_buf()).unwrap();
76
77        let mut handle_a = watcher.watch(&root_a).await.unwrap();
78        let mut handle_b = watcher.watch(&root_b).await.unwrap();
79
80        // Emit only to A
81        watcher.emit(&root_a, WorkspaceChange::modified("a.txt"));
82        // Emit only to B
83        watcher.emit(&root_b, WorkspaceChange::modified("b.txt"));
84
85        let a_event = handle_a.try_recv().unwrap();
86        let b_event = handle_b.try_recv().unwrap();
87
88        assert_eq!(a_event.path(), "a.txt");
89        assert_eq!(b_event.path(), "b.txt");
90
91        // A's channel should have nothing more
92        assert!(handle_a.try_recv().is_err());
93    }
94
95    #[tokio::test]
96    async fn emitting_to_an_unwatched_root_delivers_to_no_one() {
97        let watcher = MemoryWorkspaceWatcher::new();
98        let root = WorkspaceRoot::new(Path::new("/unwatched").to_path_buf()).unwrap();
99
100        let delivered = watcher.emit(&root, WorkspaceChange::created("nope.txt"));
101        assert_eq!(delivered, 0);
102    }
103}