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 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}