Skip to main content

fresh/services/fs/
slow.rs

1//! Slow filesystem wrapper for testing
2//!
3//! This module provides a decorator/wrapper around any FileSystem that adds
4//! configurable delays to simulate slow I/O operations. This is useful for
5//! testing editor responsiveness and performance with slow filesystems (network
6//! drives, slow disks, etc.).
7
8use crate::model::filesystem::{
9    DirEntry, FileMetadata, FilePermissions, FileReader, FileSystem, FileWriter,
10};
11use std::io;
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::Arc;
15use std::time::Duration;
16
17/// Configuration for slow filesystem simulation
18#[derive(Debug, Clone)]
19pub struct SlowFsConfig {
20    /// Delay for read_dir operations
21    pub read_dir_delay: Duration,
22    /// Delay for metadata operations
23    pub metadata_delay: Duration,
24    /// Delay for read_file operations
25    pub read_file_delay: Duration,
26    /// Delay for write_file operations
27    pub write_file_delay: Duration,
28    /// Delay for search_file operations
29    pub search_file_delay: Duration,
30    /// Delay for other operations (exists, is_dir, etc.)
31    pub other_delay: Duration,
32}
33
34impl SlowFsConfig {
35    /// Create a config with uniform delay for all operations
36    pub fn uniform(delay: Duration) -> Self {
37        Self {
38            read_dir_delay: delay,
39            metadata_delay: delay,
40            read_file_delay: delay,
41            write_file_delay: delay,
42            search_file_delay: delay,
43            other_delay: delay,
44        }
45    }
46
47    /// Create a config with no delays (useful as a baseline)
48    pub fn none() -> Self {
49        Self::uniform(Duration::ZERO)
50    }
51
52    /// Create a config simulating a slow network filesystem
53    pub fn slow_network() -> Self {
54        Self {
55            read_dir_delay: Duration::from_millis(500),
56            metadata_delay: Duration::from_millis(50),
57            read_file_delay: Duration::from_millis(200),
58            write_file_delay: Duration::from_millis(300),
59            search_file_delay: Duration::from_millis(200),
60            other_delay: Duration::from_millis(30),
61        }
62    }
63
64    /// Create a config simulating a very slow disk
65    pub fn slow_disk() -> Self {
66        Self {
67            read_dir_delay: Duration::from_millis(200),
68            metadata_delay: Duration::from_millis(20),
69            read_file_delay: Duration::from_millis(100),
70            write_file_delay: Duration::from_millis(150),
71            search_file_delay: Duration::from_millis(100),
72            other_delay: Duration::from_millis(10),
73        }
74    }
75}
76
77impl Default for SlowFsConfig {
78    fn default() -> Self {
79        Self::none()
80    }
81}
82
83/// Metrics tracking for filesystem operations
84#[derive(Debug, Default)]
85pub struct BackendMetrics {
86    /// Number of read_dir calls
87    pub read_dir_calls: AtomicUsize,
88    /// Number of metadata calls
89    pub metadata_calls: AtomicUsize,
90    /// Number of read_file calls
91    pub read_file_calls: AtomicUsize,
92    /// Number of write_file calls
93    pub write_file_calls: AtomicUsize,
94    /// Number of search_file calls
95    pub search_file_calls: AtomicUsize,
96    /// Number of other calls
97    pub other_calls: AtomicUsize,
98}
99
100impl BackendMetrics {
101    /// Create new empty metrics
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Reset all metrics to zero
107    pub fn reset(&self) {
108        self.read_dir_calls.store(0, Ordering::SeqCst);
109        self.metadata_calls.store(0, Ordering::SeqCst);
110        self.read_file_calls.store(0, Ordering::SeqCst);
111        self.write_file_calls.store(0, Ordering::SeqCst);
112        self.search_file_calls.store(0, Ordering::SeqCst);
113        self.other_calls.store(0, Ordering::SeqCst);
114    }
115
116    /// Get total number of filesystem calls
117    pub fn total_calls(&self) -> usize {
118        self.read_dir_calls.load(Ordering::SeqCst)
119            + self.metadata_calls.load(Ordering::SeqCst)
120            + self.read_file_calls.load(Ordering::SeqCst)
121            + self.write_file_calls.load(Ordering::SeqCst)
122            + self.search_file_calls.load(Ordering::SeqCst)
123            + self.other_calls.load(Ordering::SeqCst)
124    }
125}
126
127/// Slow filesystem wrapper for testing
128///
129/// Wraps any FileSystem implementation and adds configurable delays to each
130/// operation. Also tracks metrics about operation counts.
131pub struct SlowFileSystem {
132    /// The underlying real filesystem
133    inner: Arc<dyn FileSystem>,
134    /// Configuration for delays
135    config: SlowFsConfig,
136    /// Metrics tracking
137    metrics: Arc<BackendMetrics>,
138}
139
140impl SlowFileSystem {
141    /// Create a new slow filesystem wrapper
142    pub fn new(inner: Arc<dyn FileSystem>, config: SlowFsConfig) -> Self {
143        Self {
144            inner,
145            config,
146            metrics: Arc::new(BackendMetrics::new()),
147        }
148    }
149
150    /// Create with uniform delay for all operations
151    pub fn with_uniform_delay(inner: Arc<dyn FileSystem>, delay: Duration) -> Self {
152        Self::new(inner, SlowFsConfig::uniform(delay))
153    }
154
155    /// Get a reference to the metrics
156    pub fn metrics(&self) -> &Arc<BackendMetrics> {
157        &self.metrics
158    }
159
160    /// Reset metrics to zero
161    pub fn reset_metrics(&self) {
162        self.metrics.reset();
163    }
164
165    /// Add delay
166    fn add_delay(&self, delay: Duration) {
167        if !delay.is_zero() {
168            std::thread::sleep(delay);
169        }
170    }
171}
172
173impl FileSystem for SlowFileSystem {
174    fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
175        self.add_delay(self.config.read_file_delay);
176        self.metrics.read_file_calls.fetch_add(1, Ordering::SeqCst);
177        self.inner.read_file(path)
178    }
179
180    fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
181        self.add_delay(self.config.read_file_delay);
182        self.metrics.read_file_calls.fetch_add(1, Ordering::SeqCst);
183        self.inner.read_range(path, offset, len)
184    }
185
186    fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
187        self.add_delay(self.config.write_file_delay);
188        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
189        self.inner.write_file(path, data)
190    }
191
192    fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
193        self.add_delay(self.config.write_file_delay);
194        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
195        self.inner.create_file(path)
196    }
197
198    fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
199        self.add_delay(self.config.read_file_delay);
200        self.metrics.read_file_calls.fetch_add(1, Ordering::SeqCst);
201        self.inner.open_file(path)
202    }
203
204    fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
205        self.add_delay(self.config.write_file_delay);
206        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
207        self.inner.open_file_for_write(path)
208    }
209
210    fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
211        self.add_delay(self.config.write_file_delay);
212        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
213        self.inner.open_file_for_append(path)
214    }
215
216    fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
217        self.add_delay(self.config.write_file_delay);
218        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
219        self.inner.set_file_length(path, len)
220    }
221
222    fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
223        self.add_delay(self.config.other_delay);
224        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
225        self.inner.rename(from, to)
226    }
227
228    fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
229        self.add_delay(self.config.write_file_delay);
230        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
231        self.inner.copy(from, to)
232    }
233
234    fn remove_file(&self, path: &Path) -> io::Result<()> {
235        self.add_delay(self.config.other_delay);
236        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
237        self.inner.remove_file(path)
238    }
239
240    fn remove_dir(&self, path: &Path) -> io::Result<()> {
241        self.add_delay(self.config.other_delay);
242        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
243        self.inner.remove_dir(path)
244    }
245
246    fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
247        self.add_delay(self.config.metadata_delay);
248        self.metrics.metadata_calls.fetch_add(1, Ordering::SeqCst);
249        self.inner.metadata(path)
250    }
251
252    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
253        self.add_delay(self.config.metadata_delay);
254        self.metrics.metadata_calls.fetch_add(1, Ordering::SeqCst);
255        self.inner.symlink_metadata(path)
256    }
257
258    fn is_dir(&self, path: &Path) -> io::Result<bool> {
259        self.add_delay(self.config.other_delay);
260        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
261        self.inner.is_dir(path)
262    }
263
264    fn is_file(&self, path: &Path) -> io::Result<bool> {
265        self.add_delay(self.config.other_delay);
266        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
267        self.inner.is_file(path)
268    }
269
270    fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
271        self.add_delay(self.config.other_delay);
272        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
273        self.inner.set_permissions(path, permissions)
274    }
275
276    fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
277        self.add_delay(self.config.read_dir_delay);
278        self.metrics.read_dir_calls.fetch_add(1, Ordering::SeqCst);
279        self.inner.read_dir(path)
280    }
281
282    fn create_dir(&self, path: &Path) -> io::Result<()> {
283        self.add_delay(self.config.other_delay);
284        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
285        self.inner.create_dir(path)
286    }
287
288    fn create_dir_all(&self, path: &Path) -> io::Result<()> {
289        self.add_delay(self.config.other_delay);
290        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
291        self.inner.create_dir_all(path)
292    }
293
294    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
295        self.add_delay(self.config.other_delay);
296        self.metrics.other_calls.fetch_add(1, Ordering::SeqCst);
297        self.inner.canonicalize(path)
298    }
299
300    fn current_uid(&self) -> u32 {
301        self.inner.current_uid()
302    }
303
304    fn sudo_write(
305        &self,
306        path: &Path,
307        data: &[u8],
308        mode: u32,
309        uid: u32,
310        gid: u32,
311    ) -> io::Result<()> {
312        self.add_delay(self.config.write_file_delay);
313        self.metrics.write_file_calls.fetch_add(1, Ordering::SeqCst);
314        self.inner.sudo_write(path, data, mode, uid, gid)
315    }
316
317    fn search_file(
318        &self,
319        path: &Path,
320        pattern: &str,
321        opts: &crate::model::filesystem::FileSearchOptions,
322        cursor: &mut crate::model::filesystem::FileSearchCursor,
323    ) -> io::Result<Vec<crate::model::filesystem::SearchMatch>> {
324        self.add_delay(self.config.search_file_delay);
325        self.metrics
326            .search_file_calls
327            .fetch_add(1, Ordering::SeqCst);
328        crate::model::filesystem::default_search_file(&*self.inner, path, pattern, opts, cursor)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::model::filesystem::StdFileSystem;
336    use std::time::Instant;
337    use tempfile::TempDir;
338
339    #[test]
340    fn test_slow_fs_adds_delay() {
341        let temp_dir = TempDir::new().unwrap();
342        let temp_path = temp_dir.path();
343
344        let inner = Arc::new(StdFileSystem);
345        let slow_config = SlowFsConfig::uniform(Duration::from_millis(100));
346        let slow = SlowFileSystem::new(inner, slow_config);
347
348        let start = Instant::now();
349        drop(slow.read_dir(temp_path));
350        let elapsed = start.elapsed();
351
352        // Should take at least 100ms due to artificial delay
353        assert!(
354            elapsed >= Duration::from_millis(100),
355            "Expected at least 100ms delay, got {:?}",
356            elapsed
357        );
358
359        // Check metrics
360        assert_eq!(slow.metrics().read_dir_calls.load(Ordering::SeqCst), 1);
361    }
362
363    #[test]
364    fn test_metrics_tracking() {
365        let temp_dir = TempDir::new().unwrap();
366        let temp_path = temp_dir.path();
367
368        let inner = Arc::new(StdFileSystem);
369        let slow = SlowFileSystem::new(inner, SlowFsConfig::none());
370
371        // Perform various operations
372        drop(slow.read_dir(temp_path));
373        drop(slow.metadata(temp_path));
374        drop(slow.is_dir(temp_path));
375
376        assert_eq!(slow.metrics().read_dir_calls.load(Ordering::SeqCst), 1);
377        assert_eq!(slow.metrics().metadata_calls.load(Ordering::SeqCst), 1);
378        assert_eq!(slow.metrics().other_calls.load(Ordering::SeqCst), 1);
379        assert_eq!(slow.metrics().total_calls(), 3);
380    }
381
382    #[test]
383    fn test_reset_metrics() {
384        let temp_dir = TempDir::new().unwrap();
385        let temp_path = temp_dir.path();
386
387        let inner = Arc::new(StdFileSystem);
388        let slow = SlowFileSystem::new(inner, SlowFsConfig::none());
389
390        // Perform some operations
391        drop(slow.read_dir(temp_path));
392        drop(slow.metadata(temp_path));
393
394        // Verify metrics are non-zero
395        assert!(slow.metrics().total_calls() > 0);
396
397        // Reset
398        slow.reset_metrics();
399
400        // Verify metrics are zero
401        assert_eq!(slow.metrics().total_calls(), 0);
402    }
403
404    #[test]
405    fn test_preset_configs() {
406        let inner = Arc::new(StdFileSystem);
407
408        // Test slow_network preset
409        let network_config = SlowFsConfig::slow_network();
410        assert_eq!(network_config.read_dir_delay, Duration::from_millis(500));
411
412        // Test slow_disk preset
413        let disk_config = SlowFsConfig::slow_disk();
414        assert_eq!(disk_config.read_dir_delay, Duration::from_millis(200));
415
416        // Test none preset
417        let none_config = SlowFsConfig::none();
418        assert_eq!(none_config.read_dir_delay, Duration::ZERO);
419
420        // Ensure they can all be constructed
421        let _slow_network = SlowFileSystem::new(inner.clone(), network_config);
422        let _slow_disk = SlowFileSystem::new(inner.clone(), disk_config);
423        let _no_delay = SlowFileSystem::new(inner, none_config);
424    }
425}