origin_platform/
memory_workspace.rs1use 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#[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 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 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
76 #[tokio::test]
77 async fn seeded_file_is_readable() {
78 let fs = MemoryWorkspaceFs::new();
79 let root = WorkspaceRoot::new(std::env::temp_dir()).unwrap();
80 let path = RelPath::new("README.md").unwrap();
81
82 fs.seed_file(&root, &path, b"hello world".to_vec());
83
84 let content = fs.read_file(&root, &path).await.unwrap();
85 assert_eq!(content, b"hello world");
86 }
87
88 #[tokio::test]
89 async fn unseeded_file_returns_an_error() {
90 let fs = MemoryWorkspaceFs::new();
91 let root = WorkspaceRoot::new(std::env::temp_dir()).unwrap();
92
93 let result = fs.read_file(&root, &RelPath::new("nope").unwrap()).await;
94
95 assert!(result.is_err());
96 }
97
98 #[tokio::test]
99 async fn seeded_dir_returns_entries() {
100 let fs = MemoryWorkspaceFs::new();
101 let root = WorkspaceRoot::new(std::env::temp_dir()).unwrap();
102 let dir = RelPath::new("src").unwrap();
103
104 fs.seed_dir(
105 &root,
106 &dir,
107 vec![DirEntry::file("main.rs"), DirEntry::directory("lib")],
108 );
109
110 let entries = fs.list_dir(&root, &dir).await.unwrap();
111 assert_eq!(entries.len(), 2);
112 assert_eq!(entries[0].name, "main.rs");
113 assert!(!entries[0].is_dir);
114 assert!(entries[1].is_dir);
115 }
116
117 #[tokio::test]
118 async fn different_roots_are_isolated() {
119 let fs = MemoryWorkspaceFs::new();
120 let root_a = WorkspaceRoot::new(std::env::temp_dir().join("a")).unwrap();
121 let root_b = WorkspaceRoot::new(std::env::temp_dir().join("b")).unwrap();
122 let path = RelPath::new("shared.txt").unwrap();
123
124 fs.seed_file(&root_a, &path, b"from A".to_vec());
125 fs.seed_file(&root_b, &path, b"from B".to_vec());
126
127 let content_a = fs.read_file(&root_a, &path).await.unwrap();
128 let content_b = fs.read_file(&root_b, &path).await.unwrap();
129
130 assert_eq!(content_a, b"from A");
131 assert_eq!(content_b, b"from B");
132 }
133}