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