Skip to main content

zombienet_support/fs/
local.rs

1use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path};
2
3use async_trait::async_trait;
4use tokio::io::AsyncWriteExt;
5
6use super::{FileSystem, FileSystemError, FileSystemResult};
7
8#[derive(Default, Debug, Clone)]
9pub struct LocalFileSystem;
10
11#[async_trait]
12impl FileSystem for LocalFileSystem {
13    async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
14    where
15        P: AsRef<Path> + Send,
16    {
17        tokio::fs::create_dir(path).await.map_err(Into::into)
18    }
19
20    async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
21    where
22        P: AsRef<Path> + Send,
23    {
24        tokio::fs::create_dir_all(path).await.map_err(Into::into)
25    }
26
27    async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
28    where
29        P: AsRef<Path> + Send,
30    {
31        tokio::fs::read(path).await.map_err(Into::into)
32    }
33
34    async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
35    where
36        P: AsRef<Path> + Send,
37    {
38        // use `from_utf8_lossy` to read files, mostly because can be logs with unvalid utf-8
39        let content = tokio::fs::read(path)
40            .await
41            .map_err(Into::<FileSystemError>::into)?;
42        Ok(String::from_utf8_lossy(&content).to_string())
43    }
44
45    async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
46    where
47        P: AsRef<Path> + Send,
48        C: AsRef<[u8]> + Send,
49    {
50        tokio::fs::write(path, contents).await.map_err(Into::into)
51    }
52
53    async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
54    where
55        P: AsRef<Path> + Send,
56        C: AsRef<[u8]> + Send,
57    {
58        let contents = contents.as_ref();
59        let mut file = tokio::fs::OpenOptions::new()
60            .create(true)
61            .append(true)
62            .open(path)
63            .await
64            .map_err(Into::<FileSystemError>::into)?;
65
66        file.write_all(contents)
67            .await
68            .map_err(Into::<FileSystemError>::into)?;
69
70        file.flush().await.and(Ok(())).map_err(Into::into)
71    }
72
73    async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
74    where
75        P1: AsRef<Path> + Send,
76        P2: AsRef<Path> + Send,
77    {
78        tokio::fs::copy(from, to)
79            .await
80            .and(Ok(()))
81            .map_err(Into::into)
82    }
83
84    async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()>
85    where
86        P: AsRef<Path> + Send,
87    {
88        tokio::fs::set_permissions(path, Permissions::from_mode(mode))
89            .await
90            .map_err(Into::into)
91    }
92
93    async fn exists<P>(&self, path: P) -> bool
94    where
95        P: AsRef<Path> + Send,
96    {
97        path.as_ref().exists()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use uuid::Uuid;
104
105    use super::*;
106
107    const FILE_BITS: u32 = 0o100000;
108    const DIR_BITS: u32 = 0o40000;
109
110    fn setup() -> String {
111        let test_dir = format!("/tmp/unit_test_{}", Uuid::new_v4());
112        std::fs::create_dir(&test_dir).unwrap();
113        test_dir
114    }
115
116    fn teardown(test_dir: String) {
117        std::fs::remove_dir_all(test_dir).unwrap();
118    }
119
120    #[tokio::test]
121    async fn create_dir_should_create_a_new_directory_at_path() {
122        let test_dir = setup();
123        let fs = LocalFileSystem;
124
125        let new_dir = format!("{test_dir}/mynewdir");
126        fs.create_dir(&new_dir).await.unwrap();
127
128        let new_dir_path = Path::new(&new_dir);
129        assert!(new_dir_path.exists() && new_dir_path.is_dir());
130        teardown(test_dir);
131    }
132
133    #[tokio::test]
134    async fn create_dir_should_bubble_up_error_if_some_happens() {
135        let test_dir = setup();
136        let fs = LocalFileSystem;
137
138        let new_dir = format!("{test_dir}/mynewdir");
139        // intentionally create new dir before calling function to force error
140        std::fs::create_dir(&new_dir).unwrap();
141        let err = fs.create_dir(&new_dir).await.unwrap_err();
142
143        assert_eq!(err.to_string(), "File exists (os error 17)");
144        teardown(test_dir);
145    }
146
147    #[tokio::test]
148    async fn create_dir_all_should_create_a_new_directory_and_all_of_it_ancestors_at_path() {
149        let test_dir = setup();
150        let fs = LocalFileSystem;
151
152        let new_dir = format!("{test_dir}/the/path/to/mynewdir");
153        fs.create_dir_all(&new_dir).await.unwrap();
154
155        let new_dir_path = Path::new(&new_dir);
156        assert!(new_dir_path.exists() && new_dir_path.is_dir());
157        teardown(test_dir);
158    }
159
160    #[tokio::test]
161    async fn create_dir_all_should_bubble_up_error_if_some_happens() {
162        let test_dir = setup();
163        let fs = LocalFileSystem;
164
165        let new_dir = format!("{test_dir}/the/path/to/mynewdir");
166        // intentionally create new file as ancestor before calling function to force error
167        std::fs::write(format!("{test_dir}/the"), b"test").unwrap();
168        let err = fs.create_dir_all(&new_dir).await.unwrap_err();
169
170        assert_eq!(err.to_string(), "Not a directory (os error 20)");
171        teardown(test_dir);
172    }
173
174    #[tokio::test]
175    async fn read_should_return_the_contents_of_the_file_at_path() {
176        let test_dir = setup();
177        let fs = LocalFileSystem;
178
179        let file_path = format!("{test_dir}/myfile");
180        std::fs::write(&file_path, b"Test").unwrap();
181        let contents = fs.read(file_path).await.unwrap();
182
183        assert_eq!(contents, b"Test");
184        teardown(test_dir);
185    }
186
187    #[tokio::test]
188    async fn read_should_bubble_up_error_if_some_happens() {
189        let test_dir = setup();
190        let fs = LocalFileSystem;
191
192        let file_path = format!("{test_dir}/myfile");
193        // intentionally forget to create file to force error
194        let err = fs.read(file_path).await.unwrap_err();
195
196        assert_eq!(err.to_string(), "No such file or directory (os error 2)");
197        teardown(test_dir);
198    }
199
200    #[tokio::test]
201    async fn read_to_string_should_return_the_contents_of_the_file_at_path_as_string() {
202        let test_dir = setup();
203        let fs = LocalFileSystem;
204
205        let file_path = format!("{test_dir}/myfile");
206        std::fs::write(&file_path, b"Test").unwrap();
207        let contents = fs.read_to_string(file_path).await.unwrap();
208
209        assert_eq!(contents, "Test");
210        teardown(test_dir);
211    }
212
213    #[tokio::test]
214    async fn read_to_string_should_bubble_up_error_if_some_happens() {
215        let test_dir = setup();
216        let fs = LocalFileSystem;
217
218        let file_path = format!("{test_dir}/myfile");
219        // intentionally forget to create file to force error
220        let err = fs.read_to_string(file_path).await.unwrap_err();
221
222        assert_eq!(err.to_string(), "No such file or directory (os error 2)");
223        teardown(test_dir);
224    }
225
226    #[tokio::test]
227    async fn write_should_create_a_new_file_at_path_with_contents() {
228        let test_dir = setup();
229        let fs = LocalFileSystem;
230
231        let file_path = format!("{test_dir}/myfile");
232        fs.write(&file_path, "Test").await.unwrap();
233
234        assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
235        teardown(test_dir);
236    }
237
238    #[tokio::test]
239    async fn write_should_overwrite_an_existing_file_with_contents() {
240        let test_dir = setup();
241        let fs = LocalFileSystem;
242
243        let file_path = format!("{test_dir}/myfile");
244        std::fs::write(&file_path, "Test").unwrap();
245        assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
246        fs.write(&file_path, "Test updated").await.unwrap();
247
248        assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
249        teardown(test_dir);
250    }
251
252    #[tokio::test]
253    async fn write_should_bubble_up_error_if_some_happens() {
254        let test_dir = setup();
255        let fs = LocalFileSystem;
256
257        let file_path = format!("{test_dir}/myfile");
258        // intentionally create directory instead of file to force error
259        std::fs::create_dir(&file_path).unwrap();
260        let err = fs.write(&file_path, "Test").await.unwrap_err();
261
262        assert_eq!(err.to_string(), "Is a directory (os error 21)");
263        teardown(test_dir);
264    }
265
266    #[tokio::test]
267    async fn append_should_create_a_new_file_at_path_with_contents() {
268        let test_dir = setup();
269        let fs = LocalFileSystem;
270
271        let file_path = format!("{test_dir}/myfile");
272        fs.append(&file_path, "Test").await.unwrap();
273
274        assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test");
275        teardown(test_dir);
276    }
277
278    #[tokio::test]
279    async fn append_should_updates_an_existing_file_by_appending_contents() {
280        let test_dir = setup();
281        let fs = LocalFileSystem;
282
283        let file_path = format!("{test_dir}/myfile");
284        std::fs::write(&file_path, "Test").unwrap();
285        assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "Test");
286        fs.append(&file_path, " updated").await.unwrap();
287
288        assert_eq!(std::fs::read_to_string(file_path).unwrap(), "Test updated");
289        teardown(test_dir);
290    }
291
292    #[tokio::test]
293    async fn append_should_bubble_up_error_if_some_happens() {
294        let test_dir = setup();
295        let fs = LocalFileSystem;
296
297        let file_path = format!("{test_dir}/myfile");
298        // intentionally create directory instead of file to force error
299        std::fs::create_dir(&file_path).unwrap();
300        let err = fs.append(&file_path, "Test").await.unwrap_err();
301
302        assert_eq!(err.to_string(), "Is a directory (os error 21)");
303        teardown(test_dir);
304    }
305
306    #[tokio::test]
307    async fn copy_should_create_a_duplicate_of_source() {
308        let test_dir = setup();
309        let fs = LocalFileSystem;
310
311        let from_path = format!("{test_dir}/myfile");
312        std::fs::write(&from_path, "Test").unwrap();
313        let to_path = format!("{test_dir}/mycopy");
314        fs.copy(&from_path, &to_path).await.unwrap();
315
316        assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Test");
317        teardown(test_dir);
318    }
319
320    #[tokio::test]
321    async fn copy_should_ovewrite_destination_if_alread_exists() {
322        let test_dir = setup();
323        let fs = LocalFileSystem;
324
325        let from_path = format!("{test_dir}/myfile");
326        std::fs::write(&from_path, "Test").unwrap();
327        let to_path = format!("{test_dir}/mycopy");
328        std::fs::write(&from_path, "Some content").unwrap();
329        fs.copy(&from_path, &to_path).await.unwrap();
330
331        assert_eq!(std::fs::read_to_string(to_path).unwrap(), "Some content");
332        teardown(test_dir);
333    }
334
335    #[tokio::test]
336    async fn copy_should_bubble_up_error_if_some_happens() {
337        let test_dir = setup();
338        let fs = LocalFileSystem;
339
340        let from_path = format!("{test_dir}/nonexistentfile");
341        let to_path = format!("{test_dir}/mycopy");
342        let err = fs.copy(&from_path, &to_path).await.unwrap_err();
343
344        assert_eq!(err.to_string(), "No such file or directory (os error 2)");
345        teardown(test_dir);
346    }
347
348    #[tokio::test]
349    async fn set_mode_should_update_the_file_mode_at_path() {
350        let test_dir = setup();
351        let fs = LocalFileSystem;
352        let path = format!("{test_dir}/myfile");
353        std::fs::write(&path, "Test").unwrap();
354        assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (FILE_BITS + 0o400));
355
356        fs.set_mode(&path, 0o400).await.unwrap();
357
358        assert_eq!(
359            std::fs::metadata(&path).unwrap().permissions().mode(),
360            FILE_BITS + 0o400
361        );
362        teardown(test_dir);
363    }
364
365    #[tokio::test]
366    async fn set_mode_should_update_the_directory_mode_at_path() {
367        let test_dir = setup();
368        let fs = LocalFileSystem;
369        let path = format!("{test_dir}/mydir");
370        std::fs::create_dir(&path).unwrap();
371        assert!(std::fs::metadata(&path).unwrap().permissions().mode() != (DIR_BITS + 0o700));
372
373        fs.set_mode(&path, 0o700).await.unwrap();
374
375        assert_eq!(
376            std::fs::metadata(&path).unwrap().permissions().mode(),
377            DIR_BITS + 0o700
378        );
379        teardown(test_dir);
380    }
381
382    #[tokio::test]
383    async fn set_mode_should_bubble_up_error_if_some_happens() {
384        let test_dir = setup();
385        let fs = LocalFileSystem;
386        let path = format!("{test_dir}/somemissingfile");
387        // intentionnally don't create file
388
389        let err = fs.set_mode(&path, 0o400).await.unwrap_err();
390
391        assert_eq!(err.to_string(), "No such file or directory (os error 2)");
392        teardown(test_dir);
393    }
394}