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}