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_file(executor: BackgroundExecutor) {
632 let fs = FakeFs::new(executor.clone());
633 fs.insert_tree(
634 path!("/root"),
635 json!({
636 "file_a.txt": "File A",
637 "file_b.txt": "File B",
638 }),
639 )
640 .await;
641
642 let root_path = PathBuf::from(path!("/root"));
643 let path = path!("/root/file_a.txt").as_ref();
644 let trashed_entry = fs
645 .trash_file(path)
646 .await
647 .expect("should be able to trash {path:?}");
648
649 assert_eq!(trashed_entry.name, "file_a.txt");
650 assert_eq!(trashed_entry.original_parent, root_path);
651 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_b.txt"))]);
652
653 let trash_entries = fs.trash_entries();
654 assert_eq!(trash_entries.len(), 1);
655 assert_eq!(trash_entries[0].name, "file_a.txt");
656 assert_eq!(trash_entries[0].original_parent, root_path);
657}
658
659#[gpui::test]
660async fn test_fake_fs_trash_dir(executor: BackgroundExecutor) {
661 let fs = FakeFs::new(executor.clone());
662 fs.insert_tree(
663 path!("/root"),
664 json!({
665 "src": {
666 "file_a.txt": "File A",
667 "file_b.txt": "File B",
668 },
669 "file_c.txt": "File C",
670 }),
671 )
672 .await;
673
674 let root_path = PathBuf::from(path!("/root"));
675 let path = path!("/root/src").as_ref();
676 let trashed_entry = fs
677 .trash_dir(path)
678 .await
679 .expect("should be able to trash {path:?}");
680
681 assert_eq!(trashed_entry.name, "src");
682 assert_eq!(trashed_entry.original_parent, root_path);
683 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
684
685 let trash_entries = fs.trash_entries();
686 assert_eq!(trash_entries.len(), 1);
687 assert_eq!(trash_entries[0].name, "src");
688 assert_eq!(trash_entries[0].original_parent, root_path);
689}
690
691#[gpui::test]
692async fn test_fake_fs_restore(executor: BackgroundExecutor) {
693 let fs = FakeFs::new(executor.clone());
694 fs.insert_tree(
695 path!("/root"),
696 json!({
697 "src": {
698 "file_a.txt": "File A",
699 "file_b.txt": "File B",
700 },
701 "file_c.txt": "File C",
702 }),
703 )
704 .await;
705
706 // Providing a non-existent `TrashedEntry` should result in an error.
707 let id: OsString = "/trash/file_c.txt".into();
708 let name: OsString = "file_c.txt".into();
709 let original_parent = PathBuf::from(path!("/root"));
710 let trashed_entry = TrashedEntry {
711 id,
712 name,
713 original_parent,
714 };
715 let result = fs.restore(trashed_entry).await;
716 assert!(matches!(result, Err(TrashRestoreError::NotFound { .. })));
717
718 // Attempt deleting a file, asserting that the filesystem no longer reports
719 // it as part of its list of files, restore it and verify that the list of
720 // files and trash has been updated accordingly.
721 let path = path!("/root/src/file_a.txt").as_ref();
722 let trashed_entry = fs.trash_file(path).await.unwrap();
723
724 assert_eq!(fs.trash_entries().len(), 1);
725 assert_eq!(
726 fs.files(),
727 vec![
728 PathBuf::from(path!("/root/file_c.txt")),
729 PathBuf::from(path!("/root/src/file_b.txt"))
730 ]
731 );
732
733 fs.restore(trashed_entry).await.unwrap();
734
735 assert_eq!(fs.trash_entries().len(), 0);
736 assert_eq!(
737 fs.files(),
738 vec![
739 PathBuf::from(path!("/root/file_c.txt")),
740 PathBuf::from(path!("/root/src/file_a.txt")),
741 PathBuf::from(path!("/root/src/file_b.txt"))
742 ]
743 );
744
745 // Deleting and restoring a directory should also remove all of its files
746 // but create a single trashed entry, which should be removed after
747 // restoration.
748 let path = path!("/root/src/").as_ref();
749 let trashed_entry = fs.trash_dir(path).await.unwrap();
750
751 assert_eq!(fs.trash_entries().len(), 1);
752 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
753
754 fs.restore(trashed_entry).await.unwrap();
755
756 assert_eq!(
757 fs.files(),
758 vec![
759 PathBuf::from(path!("/root/file_c.txt")),
760 PathBuf::from(path!("/root/src/file_a.txt")),
761 PathBuf::from(path!("/root/src/file_b.txt"))
762 ]
763 );
764 assert_eq!(fs.trash_entries().len(), 0);
765
766 // A collision error should be returned in case a file is being restored to
767 // a path where a file already exists.
768 let path = path!("/root/src/file_a.txt").as_ref();
769 let trashed_entry = fs.trash_file(path).await.unwrap();
770
771 assert_eq!(fs.trash_entries().len(), 1);
772 assert_eq!(
773 fs.files(),
774 vec![
775 PathBuf::from(path!("/root/file_c.txt")),
776 PathBuf::from(path!("/root/src/file_b.txt"))
777 ]
778 );
779
780 fs.write(path, "New File A".as_bytes()).await.unwrap();
781
782 assert_eq!(fs.trash_entries().len(), 1);
783 assert_eq!(
784 fs.files(),
785 vec![
786 PathBuf::from(path!("/root/file_c.txt")),
787 PathBuf::from(path!("/root/src/file_a.txt")),
788 PathBuf::from(path!("/root/src/file_b.txt"))
789 ]
790 );
791
792 let file_contents = fs.files_with_contents(path);
793 assert!(fs.restore(trashed_entry).await.is_err());
794 assert_eq!(
795 file_contents,
796 vec![(PathBuf::from(path), b"New File A".to_vec())]
797 );
798
799 // A collision error should be returned in case a directory is being
800 // restored to a path where a directory already exists.
801 let path = path!("/root/src/").as_ref();
802 let trashed_entry = fs.trash_dir(path).await.unwrap();
803
804 assert_eq!(fs.trash_entries().len(), 2);
805 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
806
807 fs.create_dir(path).await.unwrap();
808
809 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
810 assert_eq!(fs.trash_entries().len(), 2);
811
812 let result = fs.restore(trashed_entry).await;
813 assert!(result.is_err());
814
815 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
816 assert_eq!(fs.trash_entries().len(), 2);
817}
818
819#[gpui::test]
820#[ignore = "stress test; run explicitly when needed"]
821async fn test_realfs_watch_stress_reports_missed_paths(
822 executor: BackgroundExecutor,
823 cx: &mut TestAppContext,
824) {
825 const FILE_COUNT: usize = 32000;
826 cx.executor().allow_parking();
827
828 let fs = RealFs::new(None, executor.clone());
829 let temp_dir = TempDir::new().expect("create temp dir");
830 let root = temp_dir.path();
831
832 let mut file_paths = Vec::with_capacity(FILE_COUNT);
833 let mut expected_paths = BTreeSet::new();
834
835 for index in 0..FILE_COUNT {
836 let dir_path = root.join(format!("dir-{index:04}"));
837 let file_path = dir_path.join("file.txt");
838 fs.create_dir(&dir_path).await.expect("create watched dir");
839 fs.write(&file_path, b"before")
840 .await
841 .expect("create initial file");
842 expected_paths.insert(file_path.clone());
843 file_paths.push(file_path);
844 }
845
846 let (mut events, watcher) = fs.watch(root, Duration::from_millis(10)).await;
847 let _watcher = watcher;
848
849 for file_path in &expected_paths {
850 _watcher
851 .add(file_path.parent().expect("file has parent"))
852 .expect("add explicit directory watch");
853 }
854
855 for (index, file_path) in file_paths.iter().enumerate() {
856 let content = format!("after-{index}");
857 fs.write(file_path, content.as_bytes())
858 .await
859 .expect("modify watched file");
860 }
861
862 let mut changed_paths = BTreeSet::new();
863 let mut rescan_count: u32 = 0;
864 let timeout = executor.timer(Duration::from_secs(10)).fuse();
865
866 futures::pin_mut!(timeout);
867
868 let mut ticks = 0;
869 while ticks < 1000 {
870 if let Some(batch) = events.next().fuse().now_or_never().flatten() {
871 for event in batch {
872 if event.kind == Some(PathEventKind::Rescan) {
873 rescan_count += 1;
874 }
875 if expected_paths.contains(&event.path) {
876 changed_paths.insert(event.path);
877 }
878 }
879 if changed_paths.len() == expected_paths.len() {
880 break;
881 }
882 ticks = 0;
883 } else {
884 ticks += 1;
885 executor.timer(Duration::from_millis(10)).await;
886 }
887 }
888
889 let missed_paths: BTreeSet<_> = expected_paths.difference(&changed_paths).cloned().collect();
890
891 eprintln!(
892 "realfs watch stress: expected={}, observed={}, missed={}, rescan={}",
893 expected_paths.len(),
894 changed_paths.len(),
895 missed_paths.len(),
896 rescan_count
897 );
898
899 assert!(
900 missed_paths.is_empty() || rescan_count > 0,
901 "missed {} paths without rescan being reported",
902 missed_paths.len()
903 );
904}