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
56    #[tokio::test]
57    async fn watching_twice_returns_different_handles_same_channel() {
58        let watcher = MemoryWorkspaceWatcher::new();
59        let root = WorkspaceRoot::new(std::env::temp_dir().join("repo")).unwrap();
60
61        let mut handle_a = watcher.watch(&root).await.unwrap();
62        let mut handle_b = watcher.watch(&root).await.unwrap();
63
64        watcher.emit(&root, WorkspaceChange::modified("README.md"));
65
66        assert_eq!(handle_a.recv().await.unwrap().path(), "README.md");
67        assert_eq!(handle_b.recv().await.unwrap().path(), "README.md");
68    }
69
70    #[tokio::test]
71    async fn different_roots_get_different_channels() {
72        let watcher = MemoryWorkspaceWatcher::new();
73        let root_a = WorkspaceRoot::new(std::env::temp_dir().join("a")).unwrap();
74        let root_b = WorkspaceRoot::new(std::env::temp_dir().join("b")).unwrap();
75
76        let mut handle_a = watcher.watch(&root_a).await.unwrap();
77        let mut handle_b = watcher.watch(&root_b).await.unwrap();
78
79        // Emit only to A
80        watcher.emit(&root_a, WorkspaceChange::modified("a.txt"));
81        // Emit only to B
82        watcher.emit(&root_b, WorkspaceChange::modified("b.txt"));
83
84        let a_event = handle_a.try_recv().unwrap();
85        let b_event = handle_b.try_recv().unwrap();
86
87        assert_eq!(a_event.path(), "a.txt");
88        assert_eq!(b_event.path(), "b.txt");
89
90        // A's channel should have nothing more
91        assert!(handle_a.try_recv().is_err());
92    }
93
94    #[tokio::test]
95    async fn emitting_to_an_unwatched_root_delivers_to_no_one() {
96        let watcher = MemoryWorkspaceWatcher::new();
97        let root = WorkspaceRoot::new(std::env::temp_dir().join("unwatched")).unwrap();
98
99        let delivered = watcher.emit(&root, WorkspaceChange::created("nope.txt"));
100        assert_eq!(delivered, 0);
101    }
102}