Skip to main content

ruff_db/system/
test.rs

1use ruff_notebook::{Notebook, NotebookError};
2use rustc_hash::FxHashMap;
3use std::panic::RefUnwindSafe;
4use std::sync::{Arc, Mutex};
5
6use crate::Db;
7use crate::files::File;
8use crate::system::{
9    CommandExecutor, DirectoryEntry, MemoryFileSystem, Metadata, Result, System, SystemPath,
10    SystemPathBuf, SystemVirtualPath, WhichError, WhichResult,
11};
12
13use super::WritableSystem;
14use super::walk_directory::WalkDirectoryBuilder;
15
16/// System implementation intended for testing.
17///
18/// It uses a memory-file system by default, but can be switched to the real file system for tests
19/// verifying more advanced file system features.
20///
21/// ## Warning
22/// Don't use this system for production code. It's intended for testing only.
23#[derive(Debug)]
24pub struct TestSystem {
25    inner: Arc<dyn WritableSystem + RefUnwindSafe + Send + Sync>,
26    /// Environment variable overrides. If a key is present here, it takes precedence
27    /// over the inner system's environment variables.
28    env_overrides: Arc<Mutex<FxHashMap<String, Option<String>>>>,
29}
30
31impl Clone for TestSystem {
32    fn clone(&self) -> Self {
33        Self {
34            inner: self.inner.clone(),
35            env_overrides: self.env_overrides.clone(),
36        }
37    }
38}
39
40impl TestSystem {
41    pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self {
42        Self {
43            inner: Arc::new(inner),
44            env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
45        }
46    }
47
48    /// Sets an environment variable override. This takes precedence over the inner system.
49    pub fn set_env_var(&self, name: impl Into<String>, value: impl Into<String>) {
50        self.env_overrides
51            .lock()
52            .unwrap()
53            .insert(name.into(), Some(value.into()));
54    }
55
56    /// Removes an environment variable override, making it appear as not set.
57    pub fn remove_env_var(&self, name: impl Into<String>) {
58        self.env_overrides.lock().unwrap().insert(name.into(), None);
59    }
60
61    /// Returns the [`InMemorySystem`].
62    ///
63    /// ## Panics
64    /// If the underlying test system isn't the [`InMemorySystem`].
65    pub fn in_memory(&self) -> &InMemorySystem {
66        self.as_in_memory()
67            .expect("The test db is not using a memory file system")
68    }
69
70    /// Returns the `InMemorySystem` or `None` if the underlying test system isn't the [`InMemorySystem`].
71    fn as_in_memory(&self) -> Option<&InMemorySystem> {
72        self.system().as_any().downcast_ref::<InMemorySystem>()
73    }
74
75    /// Returns the memory file system.
76    ///
77    /// ## Panics
78    /// If the underlying test system isn't the [`InMemorySystem`].
79    pub fn memory_file_system(&self) -> &MemoryFileSystem {
80        self.in_memory().fs()
81    }
82
83    fn use_system<S>(&mut self, system: S)
84    where
85        S: WritableSystem + Send + Sync + RefUnwindSafe + 'static,
86    {
87        self.inner = Arc::new(system);
88    }
89
90    fn system(&self) -> &dyn WritableSystem {
91        &*self.inner
92    }
93}
94
95impl System for TestSystem {
96    fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
97        self.system().path_metadata(path)
98    }
99
100    fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf> {
101        self.system().canonicalize_path(path)
102    }
103
104    fn is_same_file(&self, first: &SystemPath, second: &SystemPath) -> Result<bool> {
105        self.system().is_same_file(first, second)
106    }
107
108    fn read_to_string(&self, path: &SystemPath) -> Result<String> {
109        self.system().read_to_string(path)
110    }
111
112    fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> {
113        self.system().read_to_notebook(path)
114    }
115
116    fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
117        self.system().read_virtual_path_to_string(path)
118    }
119
120    fn read_virtual_path_to_notebook(
121        &self,
122        path: &SystemVirtualPath,
123    ) -> std::result::Result<Notebook, NotebookError> {
124        self.system().read_virtual_path_to_notebook(path)
125    }
126
127    fn current_directory(&self) -> &SystemPath {
128        self.system().current_directory()
129    }
130
131    fn user_config_directory(&self) -> Option<SystemPathBuf> {
132        self.system().user_config_directory()
133    }
134
135    fn cache_dir(&self) -> Option<SystemPathBuf> {
136        self.system().cache_dir()
137    }
138
139    fn which(&self, _name: &str) -> WhichResult {
140        Err(WhichError::CannotFindBinaryPath)
141    }
142
143    fn command_executor(&self) -> Option<&dyn CommandExecutor> {
144        self.system().command_executor()
145    }
146
147    fn read_directory<'a>(
148        &'a self,
149        path: &SystemPath,
150    ) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
151        self.system().read_directory(path)
152    }
153
154    fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
155        self.system().walk_directory(path)
156    }
157
158    fn as_writable(&self) -> Option<&dyn WritableSystem> {
159        Some(self)
160    }
161
162    fn as_any(&self) -> &dyn std::any::Any {
163        self
164    }
165
166    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
167        self
168    }
169
170    fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
171        // Check overrides first
172        if let Some(override_value) = self.env_overrides.lock().unwrap().get(name) {
173            return match override_value {
174                Some(value) => Ok(value.clone()),
175                None => Err(std::env::VarError::NotPresent),
176            };
177        }
178        // Fall back to inner system
179        self.system().env_var(name)
180    }
181
182    fn dyn_clone(&self) -> Box<dyn System> {
183        Box::new(self.clone())
184    }
185}
186
187impl Default for TestSystem {
188    fn default() -> Self {
189        Self::new(InMemorySystem::default())
190    }
191}
192
193impl WritableSystem for TestSystem {
194    fn create_new_file(&self, path: &SystemPath) -> Result<()> {
195        self.system().create_new_file(path)
196    }
197
198    fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
199        self.system().write_file_bytes(path, content)
200    }
201
202    fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
203        self.system().create_directory_all(path)
204    }
205
206    fn dyn_clone(&self) -> Box<dyn WritableSystem> {
207        Box::new(self.clone())
208    }
209}
210
211/// Extension trait for databases that use a [`WritableSystem`].
212///
213/// Provides various helper function that ease testing.
214pub trait DbWithWritableSystem: Db + Sized {
215    type System: WritableSystem;
216
217    fn writable_system(&self) -> &Self::System;
218
219    /// Writes the content of the given file and notifies the Db about the change.
220    fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl AsRef<str>) -> Result<()> {
221        let path = path.as_ref();
222        match self.writable_system().write_file(path, content.as_ref()) {
223            Ok(()) => {
224                File::sync_path(self, path);
225                Ok(())
226            }
227            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
228                if let Some(parent) = path.parent() {
229                    self.writable_system().create_directory_all(parent)?;
230
231                    for ancestor in parent.ancestors() {
232                        File::sync_path(self, ancestor);
233                    }
234
235                    self.writable_system().write_file(path, content.as_ref())?;
236                    File::sync_path(self, path);
237
238                    Ok(())
239                } else {
240                    Err(error)
241                }
242            }
243            err => err,
244        }
245    }
246
247    /// Writes auto-dedented text to a file.
248    fn write_dedented(&mut self, path: &str, content: &str) -> Result<()> {
249        self.write_file(path, ruff_python_trivia::textwrap::dedent(content))?;
250        Ok(())
251    }
252
253    /// Writes the content of the given files and notifies the Db about the change.
254    fn write_files<P, C, I>(&mut self, files: I) -> Result<()>
255    where
256        I: IntoIterator<Item = (P, C)>,
257        P: AsRef<SystemPath>,
258        C: AsRef<str>,
259    {
260        for (path, content) in files {
261            self.write_file(path, content)?;
262        }
263
264        Ok(())
265    }
266}
267
268/// Extension trait for databases that use [`TestSystem`].
269///
270/// Provides various helper function that ease testing.
271pub trait DbWithTestSystem: Db + Sized {
272    fn test_system(&self) -> &TestSystem;
273
274    fn test_system_mut(&mut self) -> &mut TestSystem;
275
276    /// Writes the content of the given virtual file.
277    ///
278    /// ## Panics
279    /// If the db isn't using the [`InMemorySystem`].
280    fn write_virtual_file(
281        &mut self,
282        path: impl AsRef<SystemVirtualPath>,
283        content: impl AsRef<[u8]>,
284    ) {
285        let path = path.as_ref();
286        self.test_system()
287            .memory_file_system()
288            .write_virtual_file(path, content);
289    }
290
291    /// Uses the given system instead of the testing system.
292    ///
293    /// This useful for testing advanced file system features like permissions, symlinks, etc.
294    ///
295    /// Note that any files written to the memory file system won't be copied over.
296    fn use_system<S>(&mut self, os: S)
297    where
298        S: WritableSystem + Send + Sync + RefUnwindSafe + 'static,
299    {
300        self.test_system_mut().use_system(os);
301    }
302
303    /// Returns the memory file system.
304    ///
305    /// ## Panics
306    /// If the underlying test system isn't the [`InMemorySystem`].
307    fn memory_file_system(&self) -> &MemoryFileSystem {
308        self.test_system().memory_file_system()
309    }
310}
311
312impl<T> DbWithWritableSystem for T
313where
314    T: DbWithTestSystem,
315{
316    type System = TestSystem;
317
318    fn writable_system(&self) -> &Self::System {
319        self.test_system()
320    }
321}
322
323#[derive(Clone, Default, Debug)]
324pub struct InMemorySystem {
325    user_config_directory: Arc<Mutex<Option<SystemPathBuf>>>,
326    memory_fs: MemoryFileSystem,
327}
328
329impl InMemorySystem {
330    pub fn from_memory_fs(memory_fs: MemoryFileSystem) -> Self {
331        Self {
332            user_config_directory: Mutex::new(None).into(),
333            memory_fs,
334        }
335    }
336
337    pub fn fs(&self) -> &MemoryFileSystem {
338        &self.memory_fs
339    }
340
341    pub fn set_user_configuration_directory(&self, directory: Option<SystemPathBuf>) {
342        let mut user_directory = self.user_config_directory.lock().unwrap();
343        *user_directory = directory;
344    }
345}
346
347impl System for InMemorySystem {
348    fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
349        self.memory_fs.metadata(path)
350    }
351
352    fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf> {
353        self.memory_fs.canonicalize(path)
354    }
355
356    fn is_same_file(&self, first: &SystemPath, second: &SystemPath) -> Result<bool> {
357        // The in-memory file system does not support hard links, so canonical paths uniquely
358        // identify files.
359        Ok(self.canonicalize_path(first)? == self.canonicalize_path(second)?)
360    }
361
362    fn read_to_string(&self, path: &SystemPath) -> Result<String> {
363        self.memory_fs.read_to_string(path)
364    }
365
366    fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> {
367        let content = self.read_to_string(path)?;
368        Notebook::from_source_code(&content)
369    }
370
371    fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
372        self.memory_fs.read_virtual_path_to_string(path)
373    }
374
375    fn read_virtual_path_to_notebook(
376        &self,
377        path: &SystemVirtualPath,
378    ) -> std::result::Result<Notebook, NotebookError> {
379        let content = self.read_virtual_path_to_string(path)?;
380        Notebook::from_source_code(&content)
381    }
382
383    fn current_directory(&self) -> &SystemPath {
384        self.memory_fs.current_directory()
385    }
386
387    fn user_config_directory(&self) -> Option<SystemPathBuf> {
388        self.user_config_directory.lock().unwrap().clone()
389    }
390
391    fn cache_dir(&self) -> Option<SystemPathBuf> {
392        None
393    }
394
395    fn which(&self, _name: &str) -> WhichResult {
396        Err(WhichError::CannotFindBinaryPath)
397    }
398
399    fn read_directory<'a>(
400        &'a self,
401        path: &SystemPath,
402    ) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
403        Ok(Box::new(self.memory_fs.read_directory(path)?))
404    }
405
406    fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
407        self.memory_fs.walk_directory(path)
408    }
409
410    fn as_writable(&self) -> Option<&dyn WritableSystem> {
411        Some(self)
412    }
413
414    fn as_any(&self) -> &dyn std::any::Any {
415        self
416    }
417
418    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
419        self
420    }
421
422    fn dyn_clone(&self) -> Box<dyn System> {
423        Box::new(self.clone())
424    }
425}
426
427impl WritableSystem for InMemorySystem {
428    fn create_new_file(&self, path: &SystemPath) -> Result<()> {
429        self.memory_fs.create_new_file(path)
430    }
431
432    fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
433        self.memory_fs.write_file(path, content)
434    }
435
436    fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
437        self.memory_fs.create_directory_all(path)
438    }
439
440    fn dyn_clone(&self) -> Box<dyn WritableSystem> {
441        Box::new(self.clone())
442    }
443}