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