fs.rs

  1use std::{
  2    collections::BTreeSet,
  3    io::Write,
  4    path::{Path, PathBuf},
  5    time::Duration,
  6};
  7
  8use futures::{FutureExt, StreamExt};
  9
 10use fs::*;
 11use gpui::{BackgroundExecutor, TestAppContext};
 12use serde_json::json;
 13use tempfile::TempDir;
 14use util::path;
 15
 16#[gpui::test]
 17async fn test_fake_fs(executor: BackgroundExecutor) {
 18    let fs = FakeFs::new(executor.clone());
 19    fs.insert_tree(
 20        path!("/root"),
 21        json!({
 22            "dir1": {
 23                "a": "A",
 24                "b": "B"
 25            },
 26            "dir2": {
 27                "c": "C",
 28                "dir3": {
 29                    "d": "D"
 30                }
 31            }
 32        }),
 33    )
 34    .await;
 35
 36    assert_eq!(
 37        fs.files(),
 38        vec![
 39            PathBuf::from(path!("/root/dir1/a")),
 40            PathBuf::from(path!("/root/dir1/b")),
 41            PathBuf::from(path!("/root/dir2/c")),
 42            PathBuf::from(path!("/root/dir2/dir3/d")),
 43        ]
 44    );
 45
 46    fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
 47        .await
 48        .unwrap();
 49
 50    assert_eq!(
 51        fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
 52            .await
 53            .unwrap(),
 54        PathBuf::from(path!("/root/dir2/dir3")),
 55    );
 56    assert_eq!(
 57        fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
 58            .await
 59            .unwrap(),
 60        PathBuf::from(path!("/root/dir2/dir3/d")),
 61    );
 62    assert_eq!(
 63        fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
 64            .await
 65            .unwrap(),
 66        "D",
 67    );
 68}
 69
 70#[gpui::test]
 71async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
 72    let fs = FakeFs::new(executor.clone());
 73    fs.insert_tree(
 74        path!("/outer"),
 75        json!({
 76            "a": "A",
 77            "b": "B",
 78            "inner": {}
 79        }),
 80    )
 81    .await;
 82
 83    assert_eq!(
 84        fs.files(),
 85        vec![
 86            PathBuf::from(path!("/outer/a")),
 87            PathBuf::from(path!("/outer/b")),
 88        ]
 89    );
 90
 91    let source = Path::new(path!("/outer/a"));
 92    let target = Path::new(path!("/outer/a copy"));
 93    copy_recursive(fs.as_ref(), source, target, Default::default())
 94        .await
 95        .unwrap();
 96
 97    assert_eq!(
 98        fs.files(),
 99        vec![
100            PathBuf::from(path!("/outer/a")),
101            PathBuf::from(path!("/outer/a copy")),
102            PathBuf::from(path!("/outer/b")),
103        ]
104    );
105
106    let source = Path::new(path!("/outer/a"));
107    let target = Path::new(path!("/outer/inner/a copy"));
108    copy_recursive(fs.as_ref(), source, target, Default::default())
109        .await
110        .unwrap();
111
112    assert_eq!(
113        fs.files(),
114        vec![
115            PathBuf::from(path!("/outer/a")),
116            PathBuf::from(path!("/outer/a copy")),
117            PathBuf::from(path!("/outer/b")),
118            PathBuf::from(path!("/outer/inner/a copy")),
119        ]
120    );
121}
122
123#[gpui::test]
124async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
125    let fs = FakeFs::new(executor.clone());
126    fs.insert_tree(
127        path!("/outer"),
128        json!({
129            "a": "A",
130            "empty": {},
131            "non-empty": {
132                "b": "B",
133            }
134        }),
135    )
136    .await;
137
138    assert_eq!(
139        fs.files(),
140        vec![
141            PathBuf::from(path!("/outer/a")),
142            PathBuf::from(path!("/outer/non-empty/b")),
143        ]
144    );
145    assert_eq!(
146        fs.directories(false),
147        vec![
148            PathBuf::from(path!("/")),
149            PathBuf::from(path!("/outer")),
150            PathBuf::from(path!("/outer/empty")),
151            PathBuf::from(path!("/outer/non-empty")),
152        ]
153    );
154
155    let source = Path::new(path!("/outer/empty"));
156    let target = Path::new(path!("/outer/empty copy"));
157    copy_recursive(fs.as_ref(), source, target, Default::default())
158        .await
159        .unwrap();
160
161    assert_eq!(
162        fs.files(),
163        vec![
164            PathBuf::from(path!("/outer/a")),
165            PathBuf::from(path!("/outer/non-empty/b")),
166        ]
167    );
168    assert_eq!(
169        fs.directories(false),
170        vec![
171            PathBuf::from(path!("/")),
172            PathBuf::from(path!("/outer")),
173            PathBuf::from(path!("/outer/empty")),
174            PathBuf::from(path!("/outer/empty copy")),
175            PathBuf::from(path!("/outer/non-empty")),
176        ]
177    );
178
179    let source = Path::new(path!("/outer/non-empty"));
180    let target = Path::new(path!("/outer/non-empty copy"));
181    copy_recursive(fs.as_ref(), source, target, Default::default())
182        .await
183        .unwrap();
184
185    assert_eq!(
186        fs.files(),
187        vec![
188            PathBuf::from(path!("/outer/a")),
189            PathBuf::from(path!("/outer/non-empty/b")),
190            PathBuf::from(path!("/outer/non-empty copy/b")),
191        ]
192    );
193    assert_eq!(
194        fs.directories(false),
195        vec![
196            PathBuf::from(path!("/")),
197            PathBuf::from(path!("/outer")),
198            PathBuf::from(path!("/outer/empty")),
199            PathBuf::from(path!("/outer/empty copy")),
200            PathBuf::from(path!("/outer/non-empty")),
201            PathBuf::from(path!("/outer/non-empty copy")),
202        ]
203    );
204}
205
206#[gpui::test]
207async fn test_copy_recursive(executor: BackgroundExecutor) {
208    let fs = FakeFs::new(executor.clone());
209    fs.insert_tree(
210        path!("/outer"),
211        json!({
212            "inner1": {
213                "a": "A",
214                "b": "B",
215                "inner3": {
216                    "d": "D",
217                },
218                "inner4": {}
219            },
220            "inner2": {
221                "c": "C",
222            }
223        }),
224    )
225    .await;
226
227    assert_eq!(
228        fs.files(),
229        vec![
230            PathBuf::from(path!("/outer/inner1/a")),
231            PathBuf::from(path!("/outer/inner1/b")),
232            PathBuf::from(path!("/outer/inner2/c")),
233            PathBuf::from(path!("/outer/inner1/inner3/d")),
234        ]
235    );
236    assert_eq!(
237        fs.directories(false),
238        vec![
239            PathBuf::from(path!("/")),
240            PathBuf::from(path!("/outer")),
241            PathBuf::from(path!("/outer/inner1")),
242            PathBuf::from(path!("/outer/inner2")),
243            PathBuf::from(path!("/outer/inner1/inner3")),
244            PathBuf::from(path!("/outer/inner1/inner4")),
245        ]
246    );
247
248    let source = Path::new(path!("/outer"));
249    let target = Path::new(path!("/outer/inner1/outer"));
250    copy_recursive(fs.as_ref(), source, target, Default::default())
251        .await
252        .unwrap();
253
254    assert_eq!(
255        fs.files(),
256        vec![
257            PathBuf::from(path!("/outer/inner1/a")),
258            PathBuf::from(path!("/outer/inner1/b")),
259            PathBuf::from(path!("/outer/inner2/c")),
260            PathBuf::from(path!("/outer/inner1/inner3/d")),
261            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
262            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
263            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
264            PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
265        ]
266    );
267    assert_eq!(
268        fs.directories(false),
269        vec![
270            PathBuf::from(path!("/")),
271            PathBuf::from(path!("/outer")),
272            PathBuf::from(path!("/outer/inner1")),
273            PathBuf::from(path!("/outer/inner2")),
274            PathBuf::from(path!("/outer/inner1/inner3")),
275            PathBuf::from(path!("/outer/inner1/inner4")),
276            PathBuf::from(path!("/outer/inner1/outer")),
277            PathBuf::from(path!("/outer/inner1/outer/inner1")),
278            PathBuf::from(path!("/outer/inner1/outer/inner2")),
279            PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
280            PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
281        ]
282    );
283}
284
285#[gpui::test]
286async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
287    let fs = FakeFs::new(executor.clone());
288    fs.insert_tree(
289        path!("/outer"),
290        json!({
291            "inner1": {
292                "a": "A",
293                "b": "B",
294                "outer": {
295                    "inner1": {
296                        "a": "B"
297                    }
298                }
299            },
300            "inner2": {
301                "c": "C",
302            }
303        }),
304    )
305    .await;
306
307    assert_eq!(
308        fs.files(),
309        vec![
310            PathBuf::from(path!("/outer/inner1/a")),
311            PathBuf::from(path!("/outer/inner1/b")),
312            PathBuf::from(path!("/outer/inner2/c")),
313            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
314        ]
315    );
316    assert_eq!(
317        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
318            .await
319            .unwrap(),
320        "B",
321    );
322
323    let source = Path::new(path!("/outer"));
324    let target = Path::new(path!("/outer/inner1/outer"));
325    copy_recursive(
326        fs.as_ref(),
327        source,
328        target,
329        CopyOptions {
330            overwrite: true,
331            ..Default::default()
332        },
333    )
334    .await
335    .unwrap();
336
337    assert_eq!(
338        fs.files(),
339        vec![
340            PathBuf::from(path!("/outer/inner1/a")),
341            PathBuf::from(path!("/outer/inner1/b")),
342            PathBuf::from(path!("/outer/inner2/c")),
343            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
344            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
345            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
346            PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
347        ]
348    );
349    assert_eq!(
350        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
351            .await
352            .unwrap(),
353        "A"
354    );
355}
356
357#[gpui::test]
358async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
359    let fs = FakeFs::new(executor.clone());
360    fs.insert_tree(
361        path!("/outer"),
362        json!({
363            "inner1": {
364                "a": "A",
365                "b": "B",
366                "outer": {
367                    "inner1": {
368                        "a": "B"
369                    }
370                }
371            },
372            "inner2": {
373                "c": "C",
374            }
375        }),
376    )
377    .await;
378
379    assert_eq!(
380        fs.files(),
381        vec![
382            PathBuf::from(path!("/outer/inner1/a")),
383            PathBuf::from(path!("/outer/inner1/b")),
384            PathBuf::from(path!("/outer/inner2/c")),
385            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
386        ]
387    );
388    assert_eq!(
389        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
390            .await
391            .unwrap(),
392        "B",
393    );
394
395    let source = Path::new(path!("/outer"));
396    let target = Path::new(path!("/outer/inner1/outer"));
397    copy_recursive(
398        fs.as_ref(),
399        source,
400        target,
401        CopyOptions {
402            ignore_if_exists: true,
403            ..Default::default()
404        },
405    )
406    .await
407    .unwrap();
408
409    assert_eq!(
410        fs.files(),
411        vec![
412            PathBuf::from(path!("/outer/inner1/a")),
413            PathBuf::from(path!("/outer/inner1/b")),
414            PathBuf::from(path!("/outer/inner2/c")),
415            PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
416            PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
417            PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
418            PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
419        ]
420    );
421    assert_eq!(
422        fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
423            .await
424            .unwrap(),
425        "B"
426    );
427}
428
429#[gpui::test]
430async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
431    // With the file handle still open, the file should be replaced
432    // https://github.com/zed-industries/zed/issues/30054
433    let fs = RealFs::new(None, executor);
434    let temp_dir = TempDir::new().unwrap();
435    let file_to_be_replaced = temp_dir.path().join("file.txt");
436    let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
437    file.write_all(b"Hello").unwrap();
438    // drop(file);  // We still hold the file handle here
439    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
440    assert_eq!(content, "Hello");
441    smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
442    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
443    assert_eq!(content, "World");
444}
445
446#[gpui::test]
447async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
448    let fs = RealFs::new(None, executor);
449    let temp_dir = TempDir::new().unwrap();
450    let file_to_be_replaced = temp_dir.path().join("file.txt");
451    smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
452    let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
453    assert_eq!(content, "Hello");
454}
455
456#[gpui::test]
457#[cfg(target_os = "windows")]
458async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
459    use util::paths::SanitizedPath;
460
461    let fs = RealFs::new(None, executor);
462    let temp_dir = TempDir::new().unwrap();
463    let file = temp_dir.path().join("test (1).txt");
464    let file = SanitizedPath::new(&file);
465    std::fs::write(&file, "test").unwrap();
466
467    let canonicalized = fs.canonicalize(file.as_path()).await;
468    assert!(canonicalized.is_ok());
469}
470
471#[gpui::test]
472async fn test_rename(executor: BackgroundExecutor) {
473    let fs = FakeFs::new(executor.clone());
474    fs.insert_tree(
475        path!("/root"),
476        json!({
477            "src": {
478                "file_a.txt": "content a",
479                "file_b.txt": "content b"
480            }
481        }),
482    )
483    .await;
484
485    fs.rename(
486        Path::new(path!("/root/src/file_a.txt")),
487        Path::new(path!("/root/src/new/renamed_a.txt")),
488        RenameOptions {
489            create_parents: true,
490            ..Default::default()
491        },
492    )
493    .await
494    .unwrap();
495
496    // Assert that the `file_a.txt` file was being renamed and moved to a
497    // different directory that did not exist before.
498    assert_eq!(
499        fs.files(),
500        vec![
501            PathBuf::from(path!("/root/src/file_b.txt")),
502            PathBuf::from(path!("/root/src/new/renamed_a.txt")),
503        ]
504    );
505
506    let result = fs
507        .rename(
508            Path::new(path!("/root/src/file_b.txt")),
509            Path::new(path!("/root/src/old/renamed_b.txt")),
510            RenameOptions {
511                create_parents: false,
512                ..Default::default()
513            },
514        )
515        .await;
516
517    // Assert that the `file_b.txt` file was not renamed nor moved, as
518    // `create_parents` was set to `false`.
519    // different directory that did not exist before.
520    assert!(result.is_err());
521    assert_eq!(
522        fs.files(),
523        vec![
524            PathBuf::from(path!("/root/src/file_b.txt")),
525            PathBuf::from(path!("/root/src/new/renamed_a.txt")),
526        ]
527    );
528}
529
530#[gpui::test]
531#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
532async fn test_realfs_parallel_rename_without_overwrite_preserves_losing_source(
533    executor: BackgroundExecutor,
534) {
535    let temp_dir = TempDir::new().unwrap();
536    let root = temp_dir.path();
537    let source_a = root.join("dir_a/shared.txt");
538    let source_b = root.join("dir_b/shared.txt");
539    let target = root.join("shared.txt");
540
541    std::fs::create_dir_all(source_a.parent().unwrap()).unwrap();
542    std::fs::create_dir_all(source_b.parent().unwrap()).unwrap();
543    std::fs::write(&source_a, "from a").unwrap();
544    std::fs::write(&source_b, "from b").unwrap();
545
546    let fs = RealFs::new(None, executor);
547    let (first_result, second_result) = futures::future::join(
548        fs.rename(&source_a, &target, RenameOptions::default()),
549        fs.rename(&source_b, &target, RenameOptions::default()),
550    )
551    .await;
552
553    assert_ne!(first_result.is_ok(), second_result.is_ok());
554    assert!(target.exists());
555    assert_eq!(source_a.exists() as u8 + source_b.exists() as u8, 1);
556}
557
558#[gpui::test]
559#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
560async fn test_realfs_rename_ignore_if_exists_leaves_source_and_target_unchanged(
561    executor: BackgroundExecutor,
562) {
563    let temp_dir = TempDir::new().unwrap();
564    let root = temp_dir.path();
565    let source = root.join("source.txt");
566    let target = root.join("target.txt");
567
568    std::fs::write(&source, "from source").unwrap();
569    std::fs::write(&target, "from target").unwrap();
570
571    let fs = RealFs::new(None, executor);
572    let result = fs
573        .rename(
574            &source,
575            &target,
576            RenameOptions {
577                ignore_if_exists: true,
578                ..Default::default()
579            },
580        )
581        .await;
582
583    assert!(result.is_ok());
584
585    assert_eq!(std::fs::read_to_string(&source).unwrap(), "from source");
586    assert_eq!(std::fs::read_to_string(&target).unwrap(), "from target");
587}
588
589#[gpui::test]
590#[cfg(unix)]
591async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) {
592    let tempdir = TempDir::new().unwrap();
593    let path = tempdir.path();
594    let fs = RealFs::new(None, executor);
595    let symlink_path = path.join("symlink");
596    smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap();
597    let metadata = fs
598        .metadata(&symlink_path)
599        .await
600        .expect("metadata call succeeds")
601        .expect("metadata returned");
602    assert!(metadata.is_symlink);
603    assert!(!metadata.is_dir);
604    assert!(!metadata.is_fifo);
605    assert!(!metadata.is_executable);
606    // don't care about len or mtime on symlinks?
607}
608
609#[gpui::test]
610#[cfg(unix)]
611async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) {
612    let tempdir = TempDir::new().unwrap();
613    let path = tempdir.path();
614    let fs = RealFs::new(None, executor);
615    let symlink_path = path.join("symlink");
616    smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap();
617    let metadata = fs
618        .metadata(&symlink_path)
619        .await
620        .expect("metadata call succeeds")
621        .expect("metadata returned");
622    assert!(metadata.is_symlink);
623    assert!(!metadata.is_dir);
624    assert!(!metadata.is_fifo);
625    assert!(!metadata.is_executable);
626    // don't care about len or mtime on symlinks?
627}
628
629#[gpui::test]
630#[ignore = "stress test; run explicitly when needed"]
631async fn test_realfs_watch_stress_reports_missed_paths(
632    executor: BackgroundExecutor,
633    cx: &mut TestAppContext,
634) {
635    const FILE_COUNT: usize = 32000;
636    cx.executor().allow_parking();
637
638    let fs = RealFs::new(None, executor.clone());
639    let temp_dir = TempDir::new().expect("create temp dir");
640    let root = temp_dir.path();
641
642    let mut file_paths = Vec::with_capacity(FILE_COUNT);
643    let mut expected_paths = BTreeSet::new();
644
645    for index in 0..FILE_COUNT {
646        let dir_path = root.join(format!("dir-{index:04}"));
647        let file_path = dir_path.join("file.txt");
648        fs.create_dir(&dir_path).await.expect("create watched dir");
649        fs.write(&file_path, b"before")
650            .await
651            .expect("create initial file");
652        expected_paths.insert(file_path.clone());
653        file_paths.push(file_path);
654    }
655
656    let (mut events, watcher) = fs.watch(root, Duration::from_millis(10)).await;
657    let _watcher = watcher;
658
659    for file_path in &expected_paths {
660        _watcher
661            .add(file_path.parent().expect("file has parent"))
662            .expect("add explicit directory watch");
663    }
664
665    for (index, file_path) in file_paths.iter().enumerate() {
666        let content = format!("after-{index}");
667        fs.write(file_path, content.as_bytes())
668            .await
669            .expect("modify watched file");
670    }
671
672    let mut changed_paths = BTreeSet::new();
673    let mut rescan_count: u32 = 0;
674    let timeout = executor.timer(Duration::from_secs(10)).fuse();
675
676    futures::pin_mut!(timeout);
677
678    let mut ticks = 0;
679    while ticks < 1000 {
680        if let Some(batch) = events.next().fuse().now_or_never().flatten() {
681            for event in batch {
682                if event.kind == Some(PathEventKind::Rescan) {
683                    rescan_count += 1;
684                }
685                if expected_paths.contains(&event.path) {
686                    changed_paths.insert(event.path);
687                }
688            }
689            if changed_paths.len() == expected_paths.len() {
690                break;
691            }
692            ticks = 0;
693        } else {
694            ticks += 1;
695            executor.timer(Duration::from_millis(10)).await;
696        }
697    }
698
699    let missed_paths: BTreeSet<_> = expected_paths.difference(&changed_paths).cloned().collect();
700
701    eprintln!(
702        "realfs watch stress: expected={}, observed={}, missed={}, rescan={}",
703        expected_paths.len(),
704        changed_paths.len(),
705        missed_paths.len(),
706        rescan_count
707    );
708
709    assert!(
710        missed_paths.is_empty() || rescan_count > 0,
711        "missed {} paths without rescan being reported",
712        missed_paths.len()
713    );
714}