Skip to main content

origin_platform/
memory_workspace.rs

1//! Memory double for [`WorkspaceFs`] — test-only in-memory filesystem.
2//!
3//! Never use this in a shipped application. It exists so tests and contract suites
4//! can validate behaviour without touching the real filesystem.
5
6use crate::workspace::{DirEntry, RelPath, WorkspaceFs, WorkspaceRoot};
7use async_trait::async_trait;
8use origin_domain::{AppError, Result};
9use std::collections::HashMap;
10use std::sync::Mutex;
11
12type FileMap = HashMap<(WorkspaceRoot, RelPath), Vec<u8>>;
13type DirMap = HashMap<(WorkspaceRoot, RelPath), Vec<DirEntry>>;
14
15/// In-memory filesystem whose contents are seeded by tests.
16///
17/// Every access is scoped to a [`WorkspaceRoot`] — a path under a different
18/// root is a different set of entries.
19#[derive(Debug, Default)]
20pub struct MemoryWorkspaceFs {
21    files: Mutex<FileMap>,
22    dirs: Mutex<DirMap>,
23}
24
25impl MemoryWorkspaceFs {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Seed a file so `read_file` returns its content.
31    pub fn seed_file(&self, root: &WorkspaceRoot, path: &RelPath, content: Vec<u8>) {
32        self.files
33            .lock()
34            .expect("poisoned")
35            .insert((root.clone(), path.clone()), content);
36    }
37
38    /// Seed a directory listing — `list_dir` returns these entries.
39    pub fn seed_dir(&self, root: &WorkspaceRoot, path: &RelPath, entries: Vec<DirEntry>) {
40        self.dirs
41            .lock()
42            .expect("poisoned")
43            .insert((root.clone(), path.clone()), entries);
44    }
45}
46
47#[async_trait]
48impl WorkspaceFs for MemoryWorkspaceFs {
49    async fn list_dir(&self, root: &WorkspaceRoot, path: &RelPath) -> Result<Vec<DirEntry>> {
50        let dirs = self.dirs.lock().expect("poisoned");
51        Ok(dirs
52            .get(&(root.clone(), path.clone()))
53            .cloned()
54            .unwrap_or_default())
55    }
56
57    async fn read_file(&self, root: &WorkspaceRoot, path: &RelPath) -> Result<Vec<u8>> {
58        let files = self.files.lock().expect("poisoned");
59        files
60            .get(&(root.clone(), path.clone()))
61            .cloned()
62            .ok_or_else(|| {
63                AppError::validation(format!(
64                    "file not found: {}/{}",
65                    root.as_path().display(),
66                    path.as_path().display()
67                ))
68            })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::path::Path;
76
77    #[tokio::test]
78    async fn seeded_file_is_readable() {
79        let fs = MemoryWorkspaceFs::new();
80        let root = WorkspaceRoot::new(Path::new("/").to_path_buf()).unwrap();
81        let path = RelPath::new("README.md").unwrap();
82
83        fs.seed_file(&root, &path, b"hello world".to_vec());
84
85        let content = fs.read_file(&root, &path).await.unwrap();
86        assert_eq!(content, b"hello world");
87    }
88
89    #[tokio::test]
90    async fn unseeded_file_returns_an_error() {
91        let fs = MemoryWorkspaceFs::new();
92        let root = WorkspaceRoot::new(Path::new("/").to_path_buf()).unwrap();
93
94        let result = fs.read_file(&root, &RelPath::new("nope").unwrap()).await;
95
96        assert!(result.is_err());
97    }
98
99    #[tokio::test]
100    async fn seeded_dir_returns_entries() {
101        let fs = MemoryWorkspaceFs::new();
102        let root = WorkspaceRoot::new(Path::new("/").to_path_buf()).unwrap();
103        let dir = RelPath::new("src").unwrap();
104
105        fs.seed_dir(
106            &root,
107            &dir,
108            vec![DirEntry::file("main.rs"), DirEntry::directory("lib")],
109        );
110
111        let entries = fs.list_dir(&root, &dir).await.unwrap();
112        assert_eq!(entries.len(), 2);
113        assert_eq!(entries[0].name, "main.rs");
114        assert!(!entries[0].is_dir);
115        assert!(entries[1].is_dir);
116    }
117
118    #[tokio::test]
119    async fn different_roots_are_isolated() {
120        let fs = MemoryWorkspaceFs::new();
121        let root_a = WorkspaceRoot::new(Path::new("/a").to_path_buf()).unwrap();
122        let root_b = WorkspaceRoot::new(Path::new("/b").to_path_buf()).unwrap();
123        let path = RelPath::new("shared.txt").unwrap();
124
125        fs.seed_file(&root_a, &path, b"from A".to_vec());
126        fs.seed_file(&root_b, &path, b"from B".to_vec());
127
128        let content_a = fs.read_file(&root_a, &path).await.unwrap();
129        let content_b = fs.read_file(&root_b, &path).await.unwrap();
130
131        assert_eq!(content_a, b"from A");
132        assert_eq!(content_b, b"from B");
133    }
134}