fs.rs

  1use std::{
  2    collections::BTreeSet,
  3    ffi::OsString,
  4    io::Write,
  5    path::{Path, PathBuf},
  6    time::Duration,
  7};
  8
  9use futures::{FutureExt, StreamExt};
 10
 11use fs::*;
 12use gpui::{BackgroundExecutor, TestAppContext};
 13use serde_json::json;
 14use tempfile::TempDir;
 15use util::path;
 16
 17#[gpui::test]
 18async fn test_fake_fs(executor: BackgroundExecutor) {
 19    let fs = FakeFs::new(executor.clone());
 20    fs.insert_tree(
 21        path!("/root"),
 22        json!({
 23            "dir1": {
 24                "a": "A",
 25                "b": "B"
 26            },
 27            "dir2": {
 28                "c": "C",
 29                "dir3": {
 30                    "d": "D"
 31                }
 32            }
 33        }),
 34    )
 35    .await;
 36
 37    assert_eq!(
 38        fs.files(),
 39        vec![
 40            PathBuf::from(path!("/root/dir1/a")),
 41            PathBuf::from(path!("/root/dir1/b")),
 42            PathBuf::from(path!("/root/dir2/c")),
 43            PathBuf::from(path!("/root/dir2/dir3/d")),
 44        ]
 45    );
 46
 47    fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
 48        .await
 49        .unwrap();
 50
 51    assert_eq!(
 52        fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
 53            .await
 54            .unwrap(),
 55        PathBuf::from(path!("/root/dir2/dir3")),
 56    );
 57    assert_eq!(
 58        fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
 59            .await
 60            .unwrap(),
 61        PathBuf::from(path!("/root/dir2/dir3/d")),
 62    );
 63    assert_eq!(
 64        fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
 65            .await
 66            .unwrap(),
 67        "D",
 68    );
 69}
 70
 71#[gpui::test]
 72async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
 73    let fs = FakeFs::new(executor.clone());
 74    fs.insert_tree(
 75        path!("/outer"),
 76        json!({
 77            "a": "A",
 78            "b": "B",
 79            "inner": {}
 80        }),
 81    )
 82    .await;
 83
 84    assert_eq!(
 85        fs.files(),
 86        vec![
 87            PathBuf::from(path!("/outer/a")),
 88            PathBuf::from(path!("/outer/b")),
 89        ]
 90    );
 91
 92    let source = Path::new(path!("/outer/a"));
 93    let target = Path::new(path!("/outer/a copy"));
 94    copy_recursive(fs.as_ref(), source, target, Default::default())
 95        .await
 96        .unwrap();
 97
 98    assert_eq!(
 99        fs.files(),
100        vec![
101            PathBuf::from(path!("/outer/a")),
102            PathBuf::from(path!("/outer/a copy")),
103            PathBuf::from(path!("/outer/b")),
104        ]
105    );
106
107    let source = Path::new(path!("/outer/a"));
108    let target = Path::new(path!("/outer/inner/a copy"));
109    copy_recursive(fs.as_ref(), source, target, Default::default())
110        .await
111        .unwrap();
112
113    assert_eq!(
114        fs.files(),
115        vec![
116            PathBuf::from(path!("/outer/a")),
117            PathBuf::from(path!("/outer/a copy")),
118            PathBuf::from(path!("/outer/b")),
119            PathBuf::from(path!("/outer/inner/a copy")),
120        ]
121    );
122}
123
124#[gpui::test]
125async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
126    let fs = FakeFs::new(executor.clone());
127    fs.insert_tree(
128        path!("/outer"),
129        json!({
130            "a": "A",
131            "empty": {},
132            "non-empty": {
133                "b": "B",
134            }
135        }),
136    )
137    .await;
138
139    assert_eq!(
140        fs.files(),
141        vec![
142            PathBuf::from(path!("/outer/a")),
143            PathBuf::from(path!("/outer/non-empty/b")),
144        ]
145    );
146    assert_eq!(
147        fs.directories(false),
148        vec![
149            PathBuf::from(path!("/")),
150            PathBuf::from(path!("/outer")),
151            PathBuf::from(path!("/outer/empty")),
152            PathBuf::from(path!("/outer/non-empty")),
153        ]
154    );
155
156    let source = Path::new(path!("/outer/empty"));
157    let target = Path::new(path!("/outer/empty copy"));
158    copy_recursive(fs.as_ref(), source, target, Default::default())
159        .await
160        .unwrap();
161
162    assert_eq!(
163        fs.files(),
164        vec![
165            PathBuf::from(path!("/outer/a")),
166            PathBuf::from(path!("/outer/non-empty/b")),
167        ]
168    );
169    assert_eq!(
170        fs.directories(false),
171        vec![
172            PathBuf::from(path!("/")),
173            PathBuf::from(path!("/outer")),
174            PathBuf::from(path!("/outer/empty")),
175            PathBuf::from(path!("/outer/empty copy")),
176            PathBuf::from(path!("/outer/non-empty")),
177        ]
178    );
179
180    let source = Path::new(path!("/outer/non-empty"));
181    let target = Path::new(path!("/outer/non-empty copy"));
182    copy_recursive(fs.as_ref(), source, target, Default::default())
183        .await
184        .unwrap();
185
186    assert_eq!(
187        fs.files(),
188        vec![
189            PathBuf::from(path!("/outer/a")),
190            PathBuf::from(path!("/outer/non-empty/b")),
191            PathBuf::from(path!("/outer/non-empty copy/b")),
192        ]
193    );
194    assert_eq!(
195        fs.directories(false),
196        vec![
197            PathBuf::from(path!("/")),
198            PathBuf::from(path!("/outer")),
199            PathBuf::from(path!("/outer/empty")),
200            PathBuf::from(path!("/outer/empty copy")),
201            PathBuf::from(path!("/outer/non-empty")),
202            PathBuf::from(path!("/outer/non-empty copy")),
203        ]
204    );
205}
206
207#[gpui::test]
208async fn test_copy_recursive(executor: BackgroundExecutor) {
209    let fs = FakeFs::new(executor.clone());
210    fs.insert_tree(
211        path!("/outer"),
212        json!({
213            "inner1": {
214                "a": "A",
215                "b": "B",
216                "inner3": {
217                    "d": "D",
218                },
219                "inner4": {}
220            },
221            "inner2": {
222                "c": "C",
223            }
224        }),
225    )
226    .await;
227
228    assert_eq!(
229        fs.files(),
230        vec![
231            PathBuf::from(path!("/outer/inner1/a")),
232            PathBuf::from(path!("/outer/inner1/b")),
233            PathBuf::from(path!("/outer/inner2/c")),
234            PathBuf::from(path!("/outer/inner1/inner3/d")),
235        ]
236    );
237    assert_eq!(
238        fs.directories(false),
239        vec![
240            PathBuf::from(path!("/")),
241            PathBuf::from(path!("/outer")),
242            PathBuf::from(path!("/outer/inner1")),
243            PathBuf::from(path!("/outer/inner2")),
244            PathBuf::from(path!("/outer/inner1/inner3")),
245            PathBuf::from(path!("/outer/inner1/inner4")),
246        ]
247    );
248
249    let source = Path::new(path!("/outer"));
250    let target = Path::new(path!("/outer/inner1/outer"));
251    copy_recursive(fs.as_ref(), source, target, Default::default())
252        .await
253        .unwrap();
254
255    assert_eq!(
256        fs.files(),
257        vec![
258            PathBuf::from(path!("/outer/inner1/a")),
259            PathBuf::from(path!("/outer/inner1/b")),
260            PathBuf::from(path!("/outer/inner2/c")),
261            PathBuf::from(path!("/outer/inner1/inner3/d")),
262            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
263            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
264            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
265            PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
266        ]
267    );
268    assert_eq!(
269        fs.directories(false),
270        vec![
271            PathBuf::from(path!("/")),
272            PathBuf::from(path!("/outer")),
273            PathBuf::from(path!("/outer/inner1")),
274            PathBuf::from(path!("/outer/inner2")),
275            PathBuf::from(path!("/outer/inner1/inner3")),
276            PathBuf::from(path!("/outer/inner1/inner4")),
277            PathBuf::from(path!("/outer/inner1/outer")),
278            PathBuf::from(path!("/outer/inner1/outer/inner1")),
279            PathBuf::from(path!("/outer/inner1/outer/inner2")),
280            PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
281            PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
282        ]
283    );
284}
285
286#[gpui::test]
287async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
288    let fs = FakeFs::new(executor.clone());
289    fs.insert_tree(
290        path!("/outer"),
291        json!({
292            "inner1": {
293                "a": "A",
294                "b": "B",
295                "outer": {
296                    "inner1": {
297                        "a": "B"
298                    }
299                }
300            },
301            "inner2": {
302                "c": "C",
303            }
304        }),
305    )
306    .await;
307
308    assert_eq!(
309        fs.files(),
310        vec![
311            PathBuf::from(path!("/outer/inner1/a")),
312            PathBuf::from(path!("/outer/inner1/b")),
313            PathBuf::from(path!("/outer/inner2/c")),
314            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
315        ]
316    );
317    assert_eq!(
318        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
319            .await
320            .unwrap(),
321        "B",
322    );
323
324    let source = Path::new(path!("/outer"));
325    let target = Path::new(path!("/outer/inner1/outer"));
326    copy_recursive(
327        fs.as_ref(),
328        source,
329        target,
330        CopyOptions {
331            overwrite: true,
332            ..Default::default()
333        },
334    )
335    .await
336    .unwrap();
337
338    assert_eq!(
339        fs.files(),
340        vec![
341            PathBuf::from(path!("/outer/inner1/a")),
342            PathBuf::from(path!("/outer/inner1/b")),
343            PathBuf::from(path!("/outer/inner2/c")),
344            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
345            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
346            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
347            PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
348        ]
349    );
350    assert_eq!(
351        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
352            .await
353            .unwrap(),
354        "A"
355    );
356}
357
358#[gpui::test]
359async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
360    let fs = FakeFs::new(executor.clone());
361    fs.insert_tree(
362        path!("/outer"),
363        json!({
364            "inner1": {
365                "a": "A",
366                "b": "B",
367                "outer": {
368                    "inner1": {
369                        "a": "B"
370                    }
371                }
372            },
373            "inner2": {
374                "c": "C",
375            }
376        }),
377    )
378    .await;
379
380    assert_eq!(
381        fs.files(),
382        vec![
383            PathBuf::from(path!("/outer/inner1/a")),
384            PathBuf::from(path!("/outer/inner1/b")),
385            PathBuf::from(path!("/outer/inner2/c")),
386            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
387        ]
388    );
389    assert_eq!(
390        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
391            .await
392            .unwrap(),
393        "B",
394    );
395
396    let source = Path::new(path!("/outer"));
397    let target = Path::new(path!("/outer/inner1/outer"));
398    copy_recursive(
399        fs.as_ref(),
400        source,
401        target,
402        CopyOptions {
403            ignore_if_exists: true,
404            ..Default::default()
405        },
406    )
407    .await
408    .unwrap();
409
410    assert_eq!(
411        fs.files(),
412        vec![
413            PathBuf::from(path!("/outer/inner1/a")),
414            PathBuf::from(path!("/outer/inner1/b")),
415            PathBuf::from(path!("/outer/inner2/c")),
416            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
417            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
418            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
419            PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
420        ]
421    );
422    assert_eq!(
423        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
424            .await
425            .unwrap(),
426        "B"
427    );
428}
429
430#[gpui::test]
431async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
432    // With the file handle still open, the file should be replaced
433    // https://github.com/zed-industries/zed/issues/30054
434    let fs = RealFs::new(None, executor);
435    let temp_dir = TempDir::new().unwrap();
436    let file_to_be_replaced = temp_dir.path().join("file.txt");
437    let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
438    file.write_all(b"Hello").unwrap();
439    // drop(file);  // We still hold the file handle here
440    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
441    assert_eq!(content, "Hello");
442    smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
443    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
444    assert_eq!(content, "World");
445}
446
447#[gpui::test]
448async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
449    let fs = RealFs::new(None, executor);
450    let temp_dir = TempDir::new().unwrap();
451    let file_to_be_replaced = temp_dir.path().join("file.txt");
452    smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
453    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
454    assert_eq!(content, "Hello");
455}
456
457#[gpui::test]
458#[cfg(target_os = "windows")]
459async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
460    use util::paths::SanitizedPath;
461
462    let fs = RealFs::new(None, executor);
463    let temp_dir = TempDir::new().unwrap();
464    let file = temp_dir.path().join("test (1).txt");
465    let file = SanitizedPath::new(&file);
466    std::fs::write(&file, "test").unwrap();
467
468    let canonicalized = fs.canonicalize(file.as_path()).await;
469    assert!(canonicalized.is_ok());
470}
471
472#[gpui::test]
473async fn test_rename(executor: BackgroundExecutor) {
474    let fs = FakeFs::new(executor.clone());
475    fs.insert_tree(
476        path!("/root"),
477        json!({
478            "src": {
479                "file_a.txt": "content a",
480                "file_b.txt": "content b"
481            }
482        }),
483    )
484    .await;
485
486    fs.rename(
487        Path::new(path!("/root/src/file_a.txt")),
488        Path::new(path!("/root/src/new/renamed_a.txt")),
489        RenameOptions {
490            create_parents: true,
491            ..Default::default()
492        },
493    )
494    .await
495    .unwrap();
496
497    // Assert that the `file_a.txt` file was being renamed and moved to a
498    // different directory that did not exist before.
499    assert_eq!(
500        fs.files(),
501        vec![
502            PathBuf::from(path!("/root/src/file_b.txt")),
503            PathBuf::from(path!("/root/src/new/renamed_a.txt")),
504        ]
505    );
506
507    let result = fs
508        .rename(
509            Path::new(path!("/root/src/file_b.txt")),
510            Path::new(path!("/root/src/old/renamed_b.txt")),
511            RenameOptions {
512                create_parents: false,
513                ..Default::default()
514            },
515        )
516        .await;
517
518    // Assert that the `file_b.txt` file was not renamed nor moved, as
519    // `create_parents` was set to `false`.
520    // different directory that did not exist before.
521    assert!(result.is_err());
522    assert_eq!(
523        fs.files(),
524        vec![
525            PathBuf::from(path!("/root/src/file_b.txt")),
526            PathBuf::from(path!("/root/src/new/renamed_a.txt")),
527        ]
528    );
529}
530
531#[gpui::test]
532#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
533async fn test_realfs_parallel_rename_without_overwrite_preserves_losing_source(
534    executor: BackgroundExecutor,
535) {
536    let temp_dir = TempDir::new().unwrap();
537    let root = temp_dir.path();
538    let source_a = root.join("dir_a/shared.txt");
539    let source_b = root.join("dir_b/shared.txt");
540    let target = root.join("shared.txt");
541
542    std::fs::create_dir_all(source_a.parent().unwrap()).unwrap();
543    std::fs::create_dir_all(source_b.parent().unwrap()).unwrap();
544    std::fs::write(&source_a, "from a").unwrap();
545    std::fs::write(&source_b, "from b").unwrap();
546
547    let fs = RealFs::new(None, executor);
548    let (first_result, second_result) = futures::future::join(
549        fs.rename(&source_a, &target, RenameOptions::default()),
550        fs.rename(&source_b, &target, RenameOptions::default()),
551    )
552    .await;
553
554    assert_ne!(first_result.is_ok(), second_result.is_ok());
555    assert!(target.exists());
556    assert_eq!(source_a.exists() as u8 + source_b.exists() as u8, 1);
557}
558
559#[gpui::test]
560#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
561async fn test_realfs_rename_ignore_if_exists_leaves_source_and_target_unchanged(
562    executor: BackgroundExecutor,
563) {
564    let temp_dir = TempDir::new().unwrap();
565    let root = temp_dir.path();
566    let source = root.join("source.txt");
567    let target = root.join("target.txt");
568
569    std::fs::write(&source, "from source").unwrap();
570    std::fs::write(&target, "from target").unwrap();
571
572    let fs = RealFs::new(None, executor);
573    let result = fs
574        .rename(
575            &source,
576            &target,
577            RenameOptions {
578                ignore_if_exists: true,
579                ..Default::default()
580            },
581        )
582        .await;
583
584    assert!(result.is_ok());
585
586    assert_eq!(std::fs::read_to_string(&source).unwrap(), "from source");
587    assert_eq!(std::fs::read_to_string(&target).unwrap(), "from target");
588}
589
590#[gpui::test]
591#[cfg(unix)]
592async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) {
593    let tempdir = TempDir::new().unwrap();
594    let path = tempdir.path();
595    let fs = RealFs::new(None, executor);
596    let symlink_path = path.join("symlink");
597    smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap();
598    let metadata = fs
599        .metadata(&symlink_path)
600        .await
601        .expect("metadata call succeeds")
602        .expect("metadata returned");
603    assert!(metadata.is_symlink);
604    assert!(!metadata.is_dir);
605    assert!(!metadata.is_fifo);
606    assert!(!metadata.is_executable);
607    // don't care about len or mtime on symlinks?
608}
609
610#[gpui::test]
611#[cfg(unix)]
612async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) {
613    let tempdir = TempDir::new().unwrap();
614    let path = tempdir.path();
615    let fs = RealFs::new(None, executor);
616    let symlink_path = path.join("symlink");
617    smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap();
618    let metadata = fs
619        .metadata(&symlink_path)
620        .await
621        .expect("metadata call succeeds")
622        .expect("metadata returned");
623    assert!(metadata.is_symlink);
624    assert!(!metadata.is_dir);
625    assert!(!metadata.is_fifo);
626    assert!(!metadata.is_executable);
627    // don't care about len or mtime on symlinks?
628}
629
630#[gpui::test]
631async fn test_fake_fs_trash(executor: BackgroundExecutor) {
632    let fs = FakeFs::new(executor.clone());
633    fs.insert_tree(
634        path!("/root"),
635        json!({
636            "src": {
637                "file_c.txt": "File C",
638                "file_d.txt": "File D"
639            },
640            "file_a.txt": "File A",
641            "file_b.txt": "File B",
642        }),
643    )
644    .await;
645
646    // Trashing a file.
647    let root_path = PathBuf::from(path!("/root"));
648    let path = path!("/root/file_a.txt").as_ref();
649    let trashed_entry = fs
650        .trash(path, Default::default())
651        .await
652        .expect("should be able to trash {path:?}");
653
654    assert_eq!(trashed_entry.name, "file_a.txt");
655    assert_eq!(trashed_entry.original_parent, root_path);
656    assert_eq!(
657        fs.files(),
658        vec![
659            PathBuf::from(path!("/root/file_b.txt")),
660            PathBuf::from(path!("/root/src/file_c.txt")),
661            PathBuf::from(path!("/root/src/file_d.txt"))
662        ]
663    );
664
665    let trash_entries = fs.trash_entries();
666    assert_eq!(trash_entries.len(), 1);
667    assert_eq!(trash_entries[0].name, "file_a.txt");
668    assert_eq!(trash_entries[0].original_parent, root_path);
669
670    // Trashing a directory.
671    let path = path!("/root/src").as_ref();
672    let trashed_entry = fs
673        .trash(
674            path,
675            RemoveOptions {
676                recursive: true,
677                ..Default::default()
678            },
679        )
680        .await
681        .expect("should be able to trash {path:?}");
682
683    assert_eq!(trashed_entry.name, "src");
684    assert_eq!(trashed_entry.original_parent, root_path);
685    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_b.txt"))]);
686
687    let trash_entries = fs.trash_entries();
688    assert_eq!(trash_entries.len(), 2);
689    assert_eq!(trash_entries[1].name, "src");
690    assert_eq!(trash_entries[1].original_parent, root_path);
691}
692
693#[gpui::test]
694async fn test_fake_fs_restore(executor: BackgroundExecutor) {
695    let fs = FakeFs::new(executor.clone());
696    fs.insert_tree(
697        path!("/root"),
698        json!({
699            "src": {
700                "file_a.txt": "File A",
701                "file_b.txt": "File B",
702            },
703            "file_c.txt": "File C",
704        }),
705    )
706    .await;
707
708    // Providing a non-existent `TrashedEntry` should result in an error.
709    let id = OsString::from("/trash/file_c.txt");
710    let name = OsString::from("file_c.txt");
711    let original_parent = PathBuf::from(path!("/root"));
712    let trashed_entry = TrashedEntry {
713        id,
714        name,
715        original_parent,
716    };
717    let result = fs.restore(trashed_entry).await;
718    assert!(matches!(result, Err(TrashRestoreError::NotFound { .. })));
719
720    // Attempt deleting a file, asserting that the filesystem no longer reports
721    // it as part of its list of files, restore it and verify that the list of
722    // files and trash has been updated accordingly.
723    let path = path!("/root/src/file_a.txt").as_ref();
724    let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
725
726    assert_eq!(fs.trash_entries().len(), 1);
727    assert_eq!(
728        fs.files(),
729        vec![
730            PathBuf::from(path!("/root/file_c.txt")),
731            PathBuf::from(path!("/root/src/file_b.txt"))
732        ]
733    );
734
735    fs.restore(trashed_entry).await.unwrap();
736
737    assert_eq!(fs.trash_entries().len(), 0);
738    assert_eq!(
739        fs.files(),
740        vec![
741            PathBuf::from(path!("/root/file_c.txt")),
742            PathBuf::from(path!("/root/src/file_a.txt")),
743            PathBuf::from(path!("/root/src/file_b.txt"))
744        ]
745    );
746
747    // Deleting and restoring a directory should also remove all of its files
748    // but create a single trashed entry, which should be removed after
749    // restoration.
750    let options = RemoveOptions {
751        recursive: true,
752        ..Default::default()
753    };
754    let path = path!("/root/src/").as_ref();
755    let trashed_entry = fs.trash(path, options).await.unwrap();
756
757    assert_eq!(fs.trash_entries().len(), 1);
758    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
759
760    fs.restore(trashed_entry).await.unwrap();
761
762    assert_eq!(
763        fs.files(),
764        vec![
765            PathBuf::from(path!("/root/file_c.txt")),
766            PathBuf::from(path!("/root/src/file_a.txt")),
767            PathBuf::from(path!("/root/src/file_b.txt"))
768        ]
769    );
770    assert_eq!(fs.trash_entries().len(), 0);
771
772    // A collision error should be returned in case a file is being restored to
773    // a path where a file already exists.
774    let path = path!("/root/src/file_a.txt").as_ref();
775    let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
776
777    assert_eq!(fs.trash_entries().len(), 1);
778    assert_eq!(
779        fs.files(),
780        vec![
781            PathBuf::from(path!("/root/file_c.txt")),
782            PathBuf::from(path!("/root/src/file_b.txt"))
783        ]
784    );
785
786    fs.write(path, "New File A".as_bytes()).await.unwrap();
787
788    assert_eq!(fs.trash_entries().len(), 1);
789    assert_eq!(
790        fs.files(),
791        vec![
792            PathBuf::from(path!("/root/file_c.txt")),
793            PathBuf::from(path!("/root/src/file_a.txt")),
794            PathBuf::from(path!("/root/src/file_b.txt"))
795        ]
796    );
797
798    let file_contents = fs.files_with_contents(path);
799    assert!(fs.restore(trashed_entry).await.is_err());
800    assert_eq!(
801        file_contents,
802        vec![(PathBuf::from(path), b"New File A".to_vec())]
803    );
804
805    // A collision error should be returned in case a directory is being
806    // restored to a path where a directory already exists.
807    let options = RemoveOptions {
808        recursive: true,
809        ..Default::default()
810    };
811    let path = path!("/root/src/").as_ref();
812    let trashed_entry = fs.trash(path, options).await.unwrap();
813
814    assert_eq!(fs.trash_entries().len(), 2);
815    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
816
817    fs.create_dir(path).await.unwrap();
818
819    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
820    assert_eq!(fs.trash_entries().len(), 2);
821
822    let result = fs.restore(trashed_entry).await;
823    assert!(result.is_err());
824
825    assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
826    assert_eq!(fs.trash_entries().len(), 2);
827}
828
829#[gpui::test]
830#[ignore = "stress test; run explicitly when needed"]
831async fn test_realfs_watch_stress_reports_missed_paths(
832    executor: BackgroundExecutor,
833    cx: &mut TestAppContext,
834) {
835    const FILE_COUNT: usize = 32000;
836    cx.executor().allow_parking();
837
838    let fs = RealFs::new(None, executor.clone());
839    let temp_dir = TempDir::new().expect("create temp dir");
840    let root = temp_dir.path();
841
842    let mut file_paths = Vec::with_capacity(FILE_COUNT);
843    let mut expected_paths = BTreeSet::new();
844
845    for index in 0..FILE_COUNT {
846        let dir_path = root.join(format!("dir-{index:04}"));
847        let file_path = dir_path.join("file.txt");
848        fs.create_dir(&dir_path).await.expect("create watched dir");
849        fs.write(&file_path, b"before")
850            .await
851            .expect("create initial file");
852        expected_paths.insert(file_path.clone());
853        file_paths.push(file_path);
854    }
855
856    let (mut events, watcher) = fs.watch(root, Duration::from_millis(10)).await;
857    let _watcher = watcher;
858
859    for file_path in &expected_paths {
860        _watcher
861            .add(file_path.parent().expect("file has parent"))
862            .expect("add explicit directory watch");
863    }
864
865    for (index, file_path) in file_paths.iter().enumerate() {
866        let content = format!("after-{index}");
867        fs.write(file_path, content.as_bytes())
868            .await
869            .expect("modify watched file");
870    }
871
872    let mut changed_paths = BTreeSet::new();
873    let mut rescan_count: u32 = 0;
874    let timeout = executor.timer(Duration::from_secs(10)).fuse();
875
876    futures::pin_mut!(timeout);
877
878    let mut ticks = 0;
879    while ticks < 1000 {
880        if let Some(batch) = events.next().fuse().now_or_never().flatten() {
881            for event in batch {
882                if event.kind == Some(PathEventKind::Rescan) {
883                    rescan_count += 1;
884                }
885                if expected_paths.contains(&event.path) {
886                    changed_paths.insert(event.path);
887                }
888            }
889            if changed_paths.len() == expected_paths.len() {
890                break;
891            }
892            ticks = 0;
893        } else {
894            ticks += 1;
895            executor.timer(Duration::from_millis(10)).await;
896        }
897    }
898
899    let missed_paths: BTreeSet<_> = expected_paths.difference(&changed_paths).cloned().collect();
900
901    eprintln!(
902        "realfs watch stress: expected={}, observed={}, missed={}, rescan={}",
903        expected_paths.len(),
904        changed_paths.len(),
905        missed_paths.len(),
906        rescan_count
907    );
908
909    assert!(
910        missed_paths.is_empty() || rescan_count > 0,
911        "missed {} paths without rescan being reported",
912        missed_paths.len()
913    );
914}