zombienet_support/fs/
in_memory.rs

1use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc};
2
3use anyhow::anyhow;
4use async_trait::async_trait;
5use tokio::sync::RwLock;
6
7use super::{FileSystem, FileSystemResult};
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum InMemoryFile {
11    File { mode: u32, contents: Vec<u8> },
12    Directory { mode: u32 },
13}
14
15impl InMemoryFile {
16    pub fn file<C>(contents: C) -> Self
17    where
18        C: AsRef<str>,
19    {
20        Self::file_raw(contents.as_ref())
21    }
22
23    pub fn file_raw<C>(contents: C) -> Self
24    where
25        C: AsRef<[u8]>,
26    {
27        Self::File {
28            mode: 0o664,
29            contents: contents.as_ref().to_vec(),
30        }
31    }
32
33    pub fn empty() -> Self {
34        Self::file_raw(vec![])
35    }
36
37    pub fn dir() -> Self {
38        Self::Directory { mode: 0o775 }
39    }
40
41    pub fn mode(&self) -> u32 {
42        match *self {
43            Self::File { mode, .. } => mode,
44            Self::Directory { mode, .. } => mode,
45        }
46    }
47
48    pub fn contents_raw(&self) -> Option<Vec<u8>> {
49        match self {
50            Self::File { contents, .. } => Some(contents.to_vec()),
51            Self::Directory { .. } => None,
52        }
53    }
54
55    pub fn contents(&self) -> Option<String> {
56        match self {
57            Self::File { contents, .. } => Some(String::from_utf8_lossy(contents).to_string()),
58            Self::Directory { .. } => None,
59        }
60    }
61}
62
63#[derive(Default, Debug, Clone)]
64pub struct InMemoryFileSystem {
65    pub files: Arc<RwLock<HashMap<OsString, InMemoryFile>>>,
66}
67
68impl InMemoryFileSystem {
69    pub fn new(files: HashMap<OsString, InMemoryFile>) -> Self {
70        Self {
71            files: Arc::new(RwLock::new(files)),
72        }
73    }
74}
75
76#[async_trait]
77impl FileSystem for InMemoryFileSystem {
78    async fn create_dir<P>(&self, path: P) -> FileSystemResult<()>
79    where
80        P: AsRef<Path> + Send,
81    {
82        let path = path.as_ref();
83        let os_path = path.as_os_str();
84        match self.files.read().await.get(os_path) {
85            Some(InMemoryFile::File { .. }) => {
86                Err(anyhow!("file {:?} already exists", os_path.to_owned(),))?
87            },
88            Some(InMemoryFile::Directory { .. }) => {
89                Err(anyhow!("directory {:?} already exists", os_path.to_owned(),))?
90            },
91            None => {},
92        };
93
94        for path in path.ancestors().skip(1) {
95            match self.files.read().await.get(path.as_os_str()) {
96                Some(InMemoryFile::Directory { .. }) => continue,
97                Some(InMemoryFile::File { .. }) => Err(anyhow!(
98                    "ancestor {:?} is not a directory",
99                    path.as_os_str(),
100                ))?,
101                None => Err(anyhow!("ancestor {:?} doesn't exists", path.as_os_str(),))?,
102            };
103        }
104
105        self.files
106            .write()
107            .await
108            .insert(os_path.to_owned(), InMemoryFile::dir());
109
110        Ok(())
111    }
112
113    async fn create_dir_all<P>(&self, path: P) -> FileSystemResult<()>
114    where
115        P: AsRef<Path> + Send,
116    {
117        let path = path.as_ref();
118        let mut files = self.files.write().await;
119        let ancestors = path
120            .ancestors()
121            .collect::<Vec<&Path>>()
122            .into_iter()
123            .rev()
124            .skip(1);
125
126        for path in ancestors {
127            match files.get(path.as_os_str()) {
128                Some(InMemoryFile::Directory { .. }) => continue,
129                Some(InMemoryFile::File { .. }) => Err(anyhow!(
130                    "ancestor {:?} is not a directory",
131                    path.as_os_str().to_owned(),
132                ))?,
133                None => files.insert(path.as_os_str().to_owned(), InMemoryFile::dir()),
134            };
135        }
136
137        Ok(())
138    }
139
140    async fn read<P>(&self, path: P) -> FileSystemResult<Vec<u8>>
141    where
142        P: AsRef<Path> + Send,
143    {
144        let os_path = path.as_ref().as_os_str();
145
146        match self.files.read().await.get(os_path) {
147            Some(InMemoryFile::File { contents, .. }) => Ok(contents.clone()),
148            Some(InMemoryFile::Directory { .. }) => {
149                Err(anyhow!("file {:?} is a directory", os_path).into())
150            },
151            None => Err(anyhow!("file {:?} not found", os_path).into()),
152        }
153    }
154
155    async fn read_to_string<P>(&self, path: P) -> FileSystemResult<String>
156    where
157        P: AsRef<Path> + Send,
158    {
159        let os_path = path.as_ref().as_os_str().to_owned();
160        let content = self.read(path).await?;
161
162        String::from_utf8(content)
163            .map_err(|_| anyhow!("invalid utf-8 encoding for file {:?}", os_path).into())
164    }
165
166    async fn write<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
167    where
168        P: AsRef<Path> + Send,
169        C: AsRef<[u8]> + Send,
170    {
171        let path = path.as_ref();
172        let os_path = path.as_os_str();
173        let mut files = self.files.write().await;
174
175        for path in path.ancestors().skip(1) {
176            match files.get(path.as_os_str()) {
177                Some(InMemoryFile::Directory { .. }) => continue,
178                Some(InMemoryFile::File { .. }) => Err(anyhow!(
179                    "ancestor {:?} is not a directory",
180                    path.as_os_str()
181                ))?,
182                None => Err(anyhow!("ancestor {:?} doesn't exists", path.as_os_str()))?,
183            };
184        }
185
186        if let Some(InMemoryFile::Directory { .. }) = files.get(os_path) {
187            return Err(anyhow!("file {:?} is a directory", os_path).into());
188        }
189
190        files.insert(os_path.to_owned(), InMemoryFile::file_raw(contents));
191
192        Ok(())
193    }
194
195    async fn append<P, C>(&self, path: P, contents: C) -> FileSystemResult<()>
196    where
197        P: AsRef<Path> + Send,
198        C: AsRef<[u8]> + Send,
199    {
200        let path = path.as_ref();
201        let mut existing_contents = match self.read(path).await {
202            Ok(existing_contents) => existing_contents,
203            Err(err) if err.to_string() == format!("file {:?} not found", path.as_os_str()) => {
204                vec![]
205            },
206            Err(err) => Err(err)?,
207        };
208        existing_contents.append(&mut contents.as_ref().to_vec());
209
210        self.write(path, existing_contents).await
211    }
212
213    async fn copy<P1, P2>(&self, from: P1, to: P2) -> FileSystemResult<()>
214    where
215        P1: AsRef<Path> + Send,
216        P2: AsRef<Path> + Send,
217    {
218        let from_ref = from.as_ref();
219        let to_ref = to.as_ref();
220        let content = self.read(from_ref).await?;
221
222        self.write(to_ref, content).await
223    }
224
225    async fn set_mode<P>(&self, path: P, mode: u32) -> FileSystemResult<()>
226    where
227        P: AsRef<Path> + Send,
228    {
229        let os_path = path.as_ref().as_os_str();
230        if let Some(file) = self.files.write().await.get_mut(os_path) {
231            match file {
232                InMemoryFile::File { mode: old_mode, .. } => {
233                    *old_mode = mode;
234                },
235                InMemoryFile::Directory { mode: old_mode, .. } => {
236                    *old_mode = mode;
237                },
238            };
239            Ok(())
240        } else {
241            Err(anyhow!("file {:?} not found", os_path).into())
242        }
243    }
244
245    async fn exists<P>(&self, path: P) -> bool
246    where
247        P: AsRef<Path> + Send,
248    {
249        self.files
250            .read()
251            .await
252            .contains_key(path.as_ref().as_os_str())
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use std::str::FromStr;
259
260    use super::*;
261
262    #[tokio::test]
263    async fn create_dir_should_create_a_directory_at_root() {
264        let fs = InMemoryFileSystem::new(HashMap::from([(
265            OsString::from_str("/").unwrap(),
266            InMemoryFile::dir(),
267        )]));
268
269        fs.create_dir("/dir").await.unwrap();
270
271        assert_eq!(fs.files.read().await.len(), 2);
272        assert!(matches!(
273            fs.files
274                .read()
275                .await
276                .get(&OsString::from_str("/dir").unwrap())
277                .unwrap(),
278            InMemoryFile::Directory { mode } if *mode == 0o775
279        ));
280    }
281
282    #[tokio::test]
283    async fn create_dir_should_return_an_error_if_directory_already_exists() {
284        let fs = InMemoryFileSystem::new(HashMap::from([
285            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
286            (OsString::from_str("/dir").unwrap(), InMemoryFile::dir()),
287        ]));
288
289        let err = fs.create_dir("/dir").await.unwrap_err();
290
291        assert_eq!(fs.files.read().await.len(), 2);
292        assert_eq!(err.to_string(), "directory \"/dir\" already exists");
293    }
294
295    #[tokio::test]
296    async fn create_dir_should_return_an_error_if_file_already_exists() {
297        let fs = InMemoryFileSystem::new(HashMap::from([
298            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
299            (OsString::from_str("/dir").unwrap(), InMemoryFile::empty()),
300        ]));
301
302        let err = fs.create_dir("/dir").await.unwrap_err();
303
304        assert_eq!(fs.files.read().await.len(), 2);
305        assert_eq!(err.to_string(), "file \"/dir\" already exists");
306    }
307
308    #[tokio::test]
309    async fn create_dir_should_create_a_directory_if_all_ancestors_exist() {
310        let fs = InMemoryFileSystem::new(HashMap::from([
311            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
312            (OsString::from_str("/path").unwrap(), InMemoryFile::dir()),
313            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
314            (
315                OsString::from_str("/path/to/my").unwrap(),
316                InMemoryFile::dir(),
317            ),
318        ]));
319
320        fs.create_dir("/path/to/my/dir").await.unwrap();
321
322        assert_eq!(fs.files.read().await.len(), 5);
323        assert!(matches!(
324            fs.files
325                .read()
326                .await
327                .get(&OsString::from_str("/path/to/my/dir").unwrap())
328                .unwrap(),
329            InMemoryFile::Directory { mode} if *mode == 0o775
330        ));
331    }
332
333    #[tokio::test]
334    async fn create_dir_should_return_an_error_if_some_directory_ancestor_doesnt_exists() {
335        let fs = InMemoryFileSystem::new(HashMap::from([
336            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
337            (OsString::from_str("/path").unwrap(), InMemoryFile::dir()),
338            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
339        ]));
340
341        let err = fs.create_dir("/path/to/my/dir").await.unwrap_err();
342
343        assert_eq!(fs.files.read().await.len(), 3);
344        assert_eq!(err.to_string(), "ancestor \"/path/to/my\" doesn't exists");
345    }
346
347    #[tokio::test]
348    async fn create_dir_should_return_an_error_if_some_ancestor_is_not_a_directory() {
349        let fs = InMemoryFileSystem::new(HashMap::from([
350            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
351            (OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
352            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
353            (
354                OsString::from_str("/path/to/my").unwrap(),
355                InMemoryFile::dir(),
356            ),
357        ]));
358
359        let err = fs.create_dir("/path/to/my/dir").await.unwrap_err();
360
361        assert_eq!(fs.files.read().await.len(), 4);
362        assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
363    }
364
365    #[tokio::test]
366    async fn create_dir_all_should_create_a_directory_and_all_its_ancestors_if_they_dont_exist() {
367        let fs = InMemoryFileSystem::new(HashMap::from([(
368            OsString::from_str("/").unwrap(),
369            InMemoryFile::dir(),
370        )]));
371
372        fs.create_dir_all("/path/to/my/dir").await.unwrap();
373
374        assert_eq!(fs.files.read().await.len(), 5);
375        assert!(matches!(
376            fs.files
377                .read()
378                .await
379                .get(&OsString::from_str("/path").unwrap())
380                .unwrap(),
381            InMemoryFile::Directory { mode } if *mode == 0o775
382        ));
383        assert!(matches!(
384            fs.files
385                .read()
386                .await
387                .get(&OsString::from_str("/path/to").unwrap())
388                .unwrap(),
389            InMemoryFile::Directory { mode } if *mode == 0o775
390        ));
391        assert!(matches!(
392            fs.files
393                .read()
394                .await
395                .get(&OsString::from_str("/path/to/my").unwrap())
396                .unwrap(),
397            InMemoryFile::Directory { mode } if *mode == 0o775
398        ));
399        assert!(matches!(
400            fs.files
401                .read()
402                .await
403                .get(&OsString::from_str("/path/to/my/dir").unwrap())
404                .unwrap(),
405            InMemoryFile::Directory { mode } if *mode == 0o775
406        ));
407    }
408
409    #[tokio::test]
410    async fn create_dir_all_should_create_a_directory_and_some_of_its_ancestors_if_they_dont_exist()
411    {
412        let fs = InMemoryFileSystem::new(HashMap::from([
413            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
414            (OsString::from_str("/path").unwrap(), InMemoryFile::dir()),
415            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
416        ]));
417
418        fs.create_dir_all("/path/to/my/dir").await.unwrap();
419
420        assert_eq!(fs.files.read().await.len(), 5);
421        assert!(matches!(
422            fs.files
423                .read()
424                .await
425                .get(&OsString::from_str("/path/to/my").unwrap())
426                .unwrap(),
427            InMemoryFile::Directory { mode } if *mode == 0o775
428        ));
429        assert!(matches!(
430            fs.files
431                .read()
432                .await
433                .get(&OsString::from_str("/path/to/my/dir").unwrap())
434                .unwrap(),
435            InMemoryFile::Directory { mode } if *mode == 0o775
436        ));
437    }
438
439    #[tokio::test]
440    async fn create_dir_all_should_return_an_error_if_some_ancestor_is_not_a_directory() {
441        let fs = InMemoryFileSystem::new(HashMap::from([
442            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
443            (OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
444            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
445        ]));
446
447        let err = fs.create_dir_all("/path/to/my/dir").await.unwrap_err();
448
449        assert_eq!(fs.files.read().await.len(), 3);
450        assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
451    }
452
453    #[tokio::test]
454    async fn read_should_return_the_file_content() {
455        let fs = InMemoryFileSystem::new(HashMap::from([(
456            OsString::from_str("/myfile").unwrap(),
457            InMemoryFile::file("content"),
458        )]));
459
460        let content = fs.read("/myfile").await.unwrap();
461
462        assert_eq!(content, "content".as_bytes().to_vec());
463    }
464
465    #[tokio::test]
466    async fn read_should_return_an_error_if_file_doesnt_exists() {
467        let fs = InMemoryFileSystem::new(HashMap::new());
468
469        let err = fs.read("/myfile").await.unwrap_err();
470
471        assert_eq!(err.to_string(), "file \"/myfile\" not found");
472    }
473
474    #[tokio::test]
475    async fn read_should_return_an_error_if_file_is_a_directory() {
476        let fs = InMemoryFileSystem::new(HashMap::from([(
477            OsString::from_str("/myfile").unwrap(),
478            InMemoryFile::dir(),
479        )]));
480
481        let err = fs.read("/myfile").await.unwrap_err();
482
483        assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
484    }
485
486    #[tokio::test]
487    async fn read_to_string_should_return_the_file_content_as_a_string() {
488        let fs = InMemoryFileSystem::new(HashMap::from([(
489            OsString::from_str("/myfile").unwrap(),
490            InMemoryFile::file("content"),
491        )]));
492
493        let content = fs.read_to_string("/myfile").await.unwrap();
494
495        assert_eq!(content, "content");
496    }
497
498    #[tokio::test]
499    async fn read_to_string_should_return_an_error_if_file_doesnt_exists() {
500        let fs = InMemoryFileSystem::new(HashMap::new());
501
502        let err = fs.read_to_string("/myfile").await.unwrap_err();
503
504        assert_eq!(err.to_string(), "file \"/myfile\" not found");
505    }
506
507    #[tokio::test]
508    async fn read_to_string_should_return_an_error_if_file_is_a_directory() {
509        let fs = InMemoryFileSystem::new(HashMap::from([(
510            OsString::from_str("/myfile").unwrap(),
511            InMemoryFile::dir(),
512        )]));
513
514        let err = fs.read_to_string("/myfile").await.unwrap_err();
515
516        assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
517    }
518
519    #[tokio::test]
520    async fn read_to_string_should_return_an_error_if_file_isnt_utf8_encoded() {
521        let fs = InMemoryFileSystem::new(HashMap::from([(
522            OsString::from_str("/myfile").unwrap(),
523            InMemoryFile::file_raw(vec![0xC3, 0x28]),
524        )]));
525
526        let err = fs.read_to_string("/myfile").await.unwrap_err();
527
528        assert_eq!(
529            err.to_string(),
530            "invalid utf-8 encoding for file \"/myfile\""
531        );
532    }
533
534    #[tokio::test]
535    async fn write_should_create_file_with_content_if_file_doesnt_exists() {
536        let fs = InMemoryFileSystem::new(HashMap::from([(
537            OsString::from_str("/").unwrap(),
538            InMemoryFile::dir(),
539        )]));
540
541        fs.write("/myfile", "my file content").await.unwrap();
542
543        assert_eq!(fs.files.read().await.len(), 2);
544        assert!(matches!(
545            fs.files
546                .read()
547                .await
548                .get(&OsString::from_str("/myfile").unwrap()),
549            Some(InMemoryFile::File {mode, contents, .. }) if *mode == 0o664 && contents == "my file content".as_bytes()
550        ));
551    }
552
553    #[tokio::test]
554    async fn write_should_overwrite_file_content_if_file_exists() {
555        let fs = InMemoryFileSystem::new(HashMap::from([
556            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
557            (
558                OsString::from_str("/myfile").unwrap(),
559                InMemoryFile::file("my file content"),
560            ),
561        ]));
562
563        fs.write("/myfile", "my new file content").await.unwrap();
564
565        assert_eq!(fs.files.read().await.len(), 2);
566        assert!(matches!(
567            fs.files
568                .read()
569                .await
570                .get(&OsString::from_str("/myfile").unwrap()),
571            Some(InMemoryFile::File { mode, contents, .. }) if *mode == 0o664 && contents == "my new file content".as_bytes()
572        ));
573    }
574
575    #[tokio::test]
576    async fn write_should_return_an_error_if_file_is_a_directory() {
577        let fs = InMemoryFileSystem::new(HashMap::from([
578            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
579            (OsString::from_str("/myfile").unwrap(), InMemoryFile::dir()),
580        ]));
581
582        let err = fs.write("/myfile", "my file content").await.unwrap_err();
583
584        assert_eq!(fs.files.read().await.len(), 2);
585        assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
586    }
587
588    #[tokio::test]
589    async fn write_should_return_an_error_if_file_is_new_and_some_ancestor_doesnt_exists() {
590        let fs = InMemoryFileSystem::new(HashMap::from([
591            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
592            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
593        ]));
594
595        let err = fs
596            .write("/path/to/myfile", "my file content")
597            .await
598            .unwrap_err();
599
600        assert_eq!(fs.files.read().await.len(), 2);
601        assert_eq!(err.to_string(), "ancestor \"/path\" doesn't exists");
602    }
603
604    #[tokio::test]
605    async fn write_should_return_an_error_if_file_is_new_and_some_ancestor_is_not_a_directory() {
606        let fs = InMemoryFileSystem::new(HashMap::from([
607            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
608            (OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
609            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
610        ]));
611
612        let err = fs
613            .write("/path/to/myfile", "my file content")
614            .await
615            .unwrap_err();
616
617        assert_eq!(fs.files.read().await.len(), 3);
618        assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
619    }
620
621    #[tokio::test]
622    async fn append_should_update_file_content_if_file_exists() {
623        let fs = InMemoryFileSystem::new(HashMap::from([
624            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
625            (
626                OsString::from_str("/myfile").unwrap(),
627                InMemoryFile::file("my file content"),
628            ),
629        ]));
630
631        fs.append("/myfile", " has been updated with new things")
632            .await
633            .unwrap();
634
635        assert_eq!(fs.files.read().await.len(), 2);
636        assert!(matches!(
637            fs.files
638                .read()
639                .await
640                .get(&OsString::from_str("/myfile").unwrap()),
641            Some(InMemoryFile::File { mode, contents, .. }) if *mode == 0o664 && contents == "my file content has been updated with new things".as_bytes()
642        ));
643    }
644
645    #[tokio::test]
646    async fn append_should_create_file_with_content_if_file_doesnt_exists() {
647        let fs = InMemoryFileSystem::new(HashMap::from([(
648            OsString::from_str("/").unwrap(),
649            InMemoryFile::dir(),
650        )]));
651
652        fs.append("/myfile", "my file content").await.unwrap();
653
654        assert_eq!(fs.files.read().await.len(), 2);
655        assert!(matches!(
656            fs.files
657                .read()
658                .await
659                .get(&OsString::from_str("/myfile").unwrap()),
660            Some(InMemoryFile::File { mode,contents, .. }) if *mode == 0o664 && contents == "my file content".as_bytes()
661        ));
662    }
663
664    #[tokio::test]
665    async fn append_should_return_an_error_if_file_is_a_directory() {
666        let fs = InMemoryFileSystem::new(HashMap::from([(
667            OsString::from_str("/myfile").unwrap(),
668            InMemoryFile::dir(),
669        )]));
670
671        let err = fs.append("/myfile", "my file content").await.unwrap_err();
672
673        assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
674    }
675
676    #[tokio::test]
677    async fn append_should_return_an_error_if_file_is_new_and_some_ancestor_doesnt_exists() {
678        let fs = InMemoryFileSystem::new(HashMap::from([
679            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
680            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
681        ]));
682
683        let err = fs
684            .append("/path/to/myfile", "my file content")
685            .await
686            .unwrap_err();
687
688        assert_eq!(fs.files.read().await.len(), 2);
689        assert_eq!(err.to_string(), "ancestor \"/path\" doesn't exists");
690    }
691
692    #[tokio::test]
693    async fn append_should_return_an_error_if_file_is_new_and_some_ancestor_is_not_a_directory() {
694        let fs = InMemoryFileSystem::new(HashMap::from([
695            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
696            (OsString::from_str("/path").unwrap(), InMemoryFile::empty()),
697            (OsString::from_str("/path/to").unwrap(), InMemoryFile::dir()),
698        ]));
699
700        let err = fs
701            .append("/path/to/myfile", "my file content")
702            .await
703            .unwrap_err();
704
705        assert_eq!(fs.files.read().await.len(), 3);
706        assert_eq!(err.to_string(), "ancestor \"/path\" is not a directory");
707    }
708
709    #[tokio::test]
710    async fn copy_should_creates_new_destination_file_if_it_doesnt_exists() {
711        let fs = InMemoryFileSystem::new(HashMap::from([
712            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
713            (
714                OsString::from_str("/myfile").unwrap(),
715                InMemoryFile::file("my file content"),
716            ),
717        ]));
718
719        fs.copy("/myfile", "/myfilecopy").await.unwrap();
720
721        assert_eq!(fs.files.read().await.len(), 3);
722        assert!(
723            matches!(fs.files.read().await.get(&OsString::from_str("/myfilecopy").unwrap()).unwrap(), InMemoryFile::File { mode, contents, .. } if *mode == 0o664 && contents == "my file content".as_bytes())
724        );
725    }
726
727    #[tokio::test]
728    async fn copy_should_updates_the_file_content_of_the_destination_file_if_it_already_exists() {
729        let fs = InMemoryFileSystem::new(HashMap::from([
730            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
731            (
732                OsString::from_str("/myfile").unwrap(),
733                InMemoryFile::file("my new file content"),
734            ),
735            (
736                OsString::from_str("/myfilecopy").unwrap(),
737                InMemoryFile::file("my file content"),
738            ),
739        ]));
740
741        fs.copy("/myfile", "/myfilecopy").await.unwrap();
742
743        assert_eq!(fs.files.read().await.len(), 3);
744        assert!(
745            matches!(fs.files.read().await.get(&OsString::from_str("/myfilecopy").unwrap()).unwrap(), InMemoryFile::File { mode, contents, .. } if *mode == 0o664 && contents == "my new file content".as_bytes())
746        );
747    }
748
749    #[tokio::test]
750    async fn copy_should_returns_an_error_if_source_file_doesnt_exists() {
751        let fs = InMemoryFileSystem::new(HashMap::from([(
752            OsString::from_str("/").unwrap(),
753            InMemoryFile::dir(),
754        )]));
755
756        let err = fs.copy("/myfile", "/mfilecopy").await.unwrap_err();
757
758        assert_eq!(err.to_string(), "file \"/myfile\" not found");
759    }
760
761    #[tokio::test]
762    async fn copy_should_returns_an_error_if_source_file_is_a_directory() {
763        let fs = InMemoryFileSystem::new(HashMap::from([
764            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
765            (OsString::from_str("/myfile").unwrap(), InMemoryFile::dir()),
766        ]));
767
768        let err = fs.copy("/myfile", "/mfilecopy").await.unwrap_err();
769
770        assert_eq!(err.to_string(), "file \"/myfile\" is a directory");
771    }
772
773    #[tokio::test]
774    async fn copy_should_returns_an_error_if_destination_file_is_a_directory() {
775        let fs = InMemoryFileSystem::new(HashMap::from([
776            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
777            (
778                OsString::from_str("/myfile").unwrap(),
779                InMemoryFile::file("my file content"),
780            ),
781            (
782                OsString::from_str("/myfilecopy").unwrap(),
783                InMemoryFile::dir(),
784            ),
785        ]));
786
787        let err = fs.copy("/myfile", "/myfilecopy").await.unwrap_err();
788
789        assert_eq!(err.to_string(), "file \"/myfilecopy\" is a directory");
790    }
791
792    #[tokio::test]
793    async fn copy_should_returns_an_error_if_destination_file_is_new_and_some_ancestor_doesnt_exists(
794    ) {
795        let fs = InMemoryFileSystem::new(HashMap::from([
796            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
797            (
798                OsString::from_str("/myfile").unwrap(),
799                InMemoryFile::file("my file content"),
800            ),
801        ]));
802
803        let err = fs.copy("/myfile", "/somedir/myfilecopy").await.unwrap_err();
804
805        assert_eq!(fs.files.read().await.len(), 2);
806        assert_eq!(err.to_string(), "ancestor \"/somedir\" doesn't exists");
807    }
808
809    #[tokio::test]
810    async fn copy_should_returns_an_error_if_destination_file_is_new_and_some_ancestor_is_not_a_directory(
811    ) {
812        let fs = InMemoryFileSystem::new(HashMap::from([
813            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
814            (
815                OsString::from_str("/myfile").unwrap(),
816                InMemoryFile::file("my file content"),
817            ),
818            (
819                OsString::from_str("/mypath").unwrap(),
820                InMemoryFile::empty(),
821            ),
822        ]));
823
824        let err = fs.copy("/myfile", "/mypath/myfilecopy").await.unwrap_err();
825
826        assert_eq!(fs.files.read().await.len(), 3);
827        assert_eq!(err.to_string(), "ancestor \"/mypath\" is not a directory");
828    }
829
830    #[tokio::test]
831    async fn set_mode_should_update_the_file_mode_at_path() {
832        let fs = InMemoryFileSystem::new(HashMap::from([
833            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
834            (
835                OsString::from_str("/myfile").unwrap(),
836                InMemoryFile::file("my file content"),
837            ),
838        ]));
839        assert!(
840            matches!(fs.files.read().await.get(&OsString::from_str("/myfile").unwrap()).unwrap(), InMemoryFile::File { mode, .. } if *mode == 0o664)
841        );
842
843        fs.set_mode("/myfile", 0o400).await.unwrap();
844
845        assert!(
846            matches!(fs.files.read().await.get(&OsString::from_str("/myfile").unwrap()).unwrap(), InMemoryFile::File { mode, .. } if *mode == 0o400)
847        );
848    }
849
850    #[tokio::test]
851    async fn set_mode_should_update_the_directory_mode_at_path() {
852        let fs = InMemoryFileSystem::new(HashMap::from([
853            (OsString::from_str("/").unwrap(), InMemoryFile::dir()),
854            (OsString::from_str("/mydir").unwrap(), InMemoryFile::dir()),
855        ]));
856        assert!(
857            matches!(fs.files.read().await.get(&OsString::from_str("/mydir").unwrap()).unwrap(), InMemoryFile::Directory { mode } if *mode == 0o775)
858        );
859
860        fs.set_mode("/mydir", 0o700).await.unwrap();
861
862        assert!(
863            matches!(fs.files.read().await.get(&OsString::from_str("/mydir").unwrap()).unwrap(), InMemoryFile::Directory { mode } if *mode == 0o700)
864        );
865    }
866
867    #[tokio::test]
868    async fn set_mode_should_returns_an_error_if_file_doesnt_exists() {
869        let fs = InMemoryFileSystem::new(HashMap::from([(
870            OsString::from_str("/").unwrap(),
871            InMemoryFile::dir(),
872        )]));
873        // intentionally forget to create file
874
875        let err = fs.set_mode("/myfile", 0o400).await.unwrap_err();
876
877        assert_eq!(err.to_string(), "file \"/myfile\" not found");
878    }
879}