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}