Skip to repository content1123 lines · 35.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:06:19.268Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
fs_tests.rs
1mod fake_git_repo_tests;
2
3use std::{
4 collections::BTreeSet,
5 io::Write,
6 path::{Path, PathBuf},
7 pin::Pin,
8 sync::Arc,
9 time::Duration,
10};
11
12use futures::{FutureExt, StreamExt};
13
14use fs::*;
15use gpui::{BackgroundExecutor, TestAppContext};
16use serde_json::json;
17use tempfile::TempDir;
18use util::path;
19
20#[gpui::test]
21async fn test_fake_fs(executor: BackgroundExecutor) {
22 let fs = FakeFs::new(executor.clone());
23 fs.insert_tree(
24 path!("/root"),
25 json!({
26 "dir1": {
27 "a": "A",
28 "b": "B"
29 },
30 "dir2": {
31 "c": "C",
32 "dir3": {
33 "d": "D"
34 }
35 }
36 }),
37 )
38 .await;
39
40 assert_eq!(
41 fs.files(),
42 vec![
43 PathBuf::from(path!("/root/dir1/a")),
44 PathBuf::from(path!("/root/dir1/b")),
45 PathBuf::from(path!("/root/dir2/c")),
46 PathBuf::from(path!("/root/dir2/dir3/d")),
47 ]
48 );
49
50 fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
51 .await
52 .unwrap();
53
54 assert_eq!(
55 fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
56 .await
57 .unwrap(),
58 PathBuf::from(path!("/root/dir2/dir3")),
59 );
60 assert_eq!(
61 fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
62 .await
63 .unwrap(),
64 PathBuf::from(path!("/root/dir2/dir3/d")),
65 );
66 assert_eq!(
67 fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
68 .await
69 .unwrap(),
70 "D",
71 );
72}
73
74#[gpui::test]
75async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
76 let fs = FakeFs::new(executor.clone());
77 fs.insert_tree(
78 path!("/outer"),
79 json!({
80 "a": "A",
81 "b": "B",
82 "inner": {}
83 }),
84 )
85 .await;
86
87 assert_eq!(
88 fs.files(),
89 vec![
90 PathBuf::from(path!("/outer/a")),
91 PathBuf::from(path!("/outer/b")),
92 ]
93 );
94
95 let source = Path::new(path!("/outer/a"));
96 let target = Path::new(path!("/outer/a copy"));
97 copy_recursive(fs.as_ref(), source, target, Default::default())
98 .await
99 .unwrap();
100
101 assert_eq!(
102 fs.files(),
103 vec![
104 PathBuf::from(path!("/outer/a")),
105 PathBuf::from(path!("/outer/a copy")),
106 PathBuf::from(path!("/outer/b")),
107 ]
108 );
109
110 let source = Path::new(path!("/outer/a"));
111 let target = Path::new(path!("/outer/inner/a copy"));
112 copy_recursive(fs.as_ref(), source, target, Default::default())
113 .await
114 .unwrap();
115
116 assert_eq!(
117 fs.files(),
118 vec![
119 PathBuf::from(path!("/outer/a")),
120 PathBuf::from(path!("/outer/a copy")),
121 PathBuf::from(path!("/outer/b")),
122 PathBuf::from(path!("/outer/inner/a copy")),
123 ]
124 );
125}
126
127#[gpui::test]
128async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
129 let fs = FakeFs::new(executor.clone());
130 fs.insert_tree(
131 path!("/outer"),
132 json!({
133 "a": "A",
134 "empty": {},
135 "non-empty": {
136 "b": "B",
137 }
138 }),
139 )
140 .await;
141
142 assert_eq!(
143 fs.files(),
144 vec![
145 PathBuf::from(path!("/outer/a")),
146 PathBuf::from(path!("/outer/non-empty/b")),
147 ]
148 );
149 assert_eq!(
150 fs.directories(false),
151 vec![
152 PathBuf::from(path!("/")),
153 PathBuf::from(path!("/outer")),
154 PathBuf::from(path!("/outer/empty")),
155 PathBuf::from(path!("/outer/non-empty")),
156 ]
157 );
158
159 let source = Path::new(path!("/outer/empty"));
160 let target = Path::new(path!("/outer/empty copy"));
161 copy_recursive(fs.as_ref(), source, target, Default::default())
162 .await
163 .unwrap();
164
165 assert_eq!(
166 fs.files(),
167 vec![
168 PathBuf::from(path!("/outer/a")),
169 PathBuf::from(path!("/outer/non-empty/b")),
170 ]
171 );
172 assert_eq!(
173 fs.directories(false),
174 vec![
175 PathBuf::from(path!("/")),
176 PathBuf::from(path!("/outer")),
177 PathBuf::from(path!("/outer/empty")),
178 PathBuf::from(path!("/outer/empty copy")),
179 PathBuf::from(path!("/outer/non-empty")),
180 ]
181 );
182
183 let source = Path::new(path!("/outer/non-empty"));
184 let target = Path::new(path!("/outer/non-empty copy"));
185 copy_recursive(fs.as_ref(), source, target, Default::default())
186 .await
187 .unwrap();
188
189 assert_eq!(
190 fs.files(),
191 vec![
192 PathBuf::from(path!("/outer/a")),
193 PathBuf::from(path!("/outer/non-empty/b")),
194 PathBuf::from(path!("/outer/non-empty copy/b")),
195 ]
196 );
197 assert_eq!(
198 fs.directories(false),
199 vec![
200 PathBuf::from(path!("/")),
201 PathBuf::from(path!("/outer")),
202 PathBuf::from(path!("/outer/empty")),
203 PathBuf::from(path!("/outer/empty copy")),
204 PathBuf::from(path!("/outer/non-empty")),
205 PathBuf::from(path!("/outer/non-empty copy")),
206 ]
207 );
208}
209
210#[gpui::test]
211async fn test_copy_recursive(executor: BackgroundExecutor) {
212 let fs = FakeFs::new(executor.clone());
213 fs.insert_tree(
214 path!("/outer"),
215 json!({
216 "inner1": {
217 "a": "A",
218 "b": "B",
219 "inner3": {
220 "d": "D",
221 },
222 "inner4": {}
223 },
224 "inner2": {
225 "c": "C",
226 }
227 }),
228 )
229 .await;
230
231 assert_eq!(
232 fs.files(),
233 vec![
234 PathBuf::from(path!("/outer/inner1/a")),
235 PathBuf::from(path!("/outer/inner1/b")),
236 PathBuf::from(path!("/outer/inner2/c")),
237 PathBuf::from(path!("/outer/inner1/inner3/d")),
238 ]
239 );
240 assert_eq!(
241 fs.directories(false),
242 vec![
243 PathBuf::from(path!("/")),
244 PathBuf::from(path!("/outer")),
245 PathBuf::from(path!("/outer/inner1")),
246 PathBuf::from(path!("/outer/inner2")),
247 PathBuf::from(path!("/outer/inner1/inner3")),
248 PathBuf::from(path!("/outer/inner1/inner4")),
249 ]
250 );
251
252 let source = Path::new(path!("/outer"));
253 let target = Path::new(path!("/outer/inner1/outer"));
254 copy_recursive(fs.as_ref(), source, target, Default::default())
255 .await
256 .unwrap();
257
258 assert_eq!(
259 fs.files(),
260 vec![
261 PathBuf::from(path!("/outer/inner1/a")),
262 PathBuf::from(path!("/outer/inner1/b")),
263 PathBuf::from(path!("/outer/inner2/c")),
264 PathBuf::from(path!("/outer/inner1/inner3/d")),
265 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
266 PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
267 PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
268 PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
269 ]
270 );
271 assert_eq!(
272 fs.directories(false),
273 vec![
274 PathBuf::from(path!("/")),
275 PathBuf::from(path!("/outer")),
276 PathBuf::from(path!("/outer/inner1")),
277 PathBuf::from(path!("/outer/inner2")),
278 PathBuf::from(path!("/outer/inner1/inner3")),
279 PathBuf::from(path!("/outer/inner1/inner4")),
280 PathBuf::from(path!("/outer/inner1/outer")),
281 PathBuf::from(path!("/outer/inner1/outer/inner1")),
282 PathBuf::from(path!("/outer/inner1/outer/inner2")),
283 PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
284 PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
285 ]
286 );
287}
288
289#[gpui::test]
290async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
291 let fs = FakeFs::new(executor.clone());
292 fs.insert_tree(
293 path!("/outer"),
294 json!({
295 "inner1": {
296 "a": "A",
297 "b": "B",
298 "outer": {
299 "inner1": {
300 "a": "B"
301 }
302 }
303 },
304 "inner2": {
305 "c": "C",
306 }
307 }),
308 )
309 .await;
310
311 assert_eq!(
312 fs.files(),
313 vec![
314 PathBuf::from(path!("/outer/inner1/a")),
315 PathBuf::from(path!("/outer/inner1/b")),
316 PathBuf::from(path!("/outer/inner2/c")),
317 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
318 ]
319 );
320 assert_eq!(
321 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
322 .await
323 .unwrap(),
324 "B",
325 );
326
327 let source = Path::new(path!("/outer"));
328 let target = Path::new(path!("/outer/inner1/outer"));
329 copy_recursive(
330 fs.as_ref(),
331 source,
332 target,
333 CopyOptions {
334 overwrite: true,
335 ..Default::default()
336 },
337 )
338 .await
339 .unwrap();
340
341 assert_eq!(
342 fs.files(),
343 vec![
344 PathBuf::from(path!("/outer/inner1/a")),
345 PathBuf::from(path!("/outer/inner1/b")),
346 PathBuf::from(path!("/outer/inner2/c")),
347 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
348 PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
349 PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
350 PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
351 ]
352 );
353 assert_eq!(
354 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
355 .await
356 .unwrap(),
357 "A"
358 );
359}
360
361#[gpui::test]
362async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
363 let fs = FakeFs::new(executor.clone());
364 fs.insert_tree(
365 path!("/outer"),
366 json!({
367 "inner1": {
368 "a": "A",
369 "b": "B",
370 "outer": {
371 "inner1": {
372 "a": "B"
373 }
374 }
375 },
376 "inner2": {
377 "c": "C",
378 }
379 }),
380 )
381 .await;
382
383 assert_eq!(
384 fs.files(),
385 vec![
386 PathBuf::from(path!("/outer/inner1/a")),
387 PathBuf::from(path!("/outer/inner1/b")),
388 PathBuf::from(path!("/outer/inner2/c")),
389 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
390 ]
391 );
392 assert_eq!(
393 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
394 .await
395 .unwrap(),
396 "B",
397 );
398
399 let source = Path::new(path!("/outer"));
400 let target = Path::new(path!("/outer/inner1/outer"));
401 copy_recursive(
402 fs.as_ref(),
403 source,
404 target,
405 CopyOptions {
406 ignore_if_exists: true,
407 ..Default::default()
408 },
409 )
410 .await
411 .unwrap();
412
413 assert_eq!(
414 fs.files(),
415 vec![
416 PathBuf::from(path!("/outer/inner1/a")),
417 PathBuf::from(path!("/outer/inner1/b")),
418 PathBuf::from(path!("/outer/inner2/c")),
419 PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
420 PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
421 PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
422 PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
423 ]
424 );
425 assert_eq!(
426 fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
427 .await
428 .unwrap(),
429 "B"
430 );
431}
432
433#[gpui::test]
434async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
435 // With the file handle still open, the file should be replaced
436 // https://github.com/zed-industries/zed/issues/30054
437 let fs = RealFs::new(None, executor);
438 let temp_dir = TempDir::new().unwrap();
439 let file_to_be_replaced = temp_dir.path().join("file.txt");
440 let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
441 file.write_all(b"Hello").unwrap();
442 // drop(file); // We still hold the file handle here
443 let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
444 assert_eq!(content, "Hello");
445 gpui::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap();
446 let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
447 assert_eq!(content, "World");
448}
449
450#[gpui::test]
451async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
452 let fs = RealFs::new(None, executor);
453 let temp_dir = TempDir::new().unwrap();
454 let file_to_be_replaced = temp_dir.path().join("file.txt");
455 gpui::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
456 let content = std::fs::read_to_string(&file_to_be_replaced).unwrap();
457 assert_eq!(content, "Hello");
458}
459
460#[gpui::test]
461#[cfg(target_os = "windows")]
462async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
463 use util::paths::SanitizedPath;
464
465 let fs = RealFs::new(None, executor);
466 let temp_dir = TempDir::new().unwrap();
467 let file = temp_dir.path().join("test (1).txt");
468 let file = SanitizedPath::new(&file);
469 std::fs::write(&file, "test").unwrap();
470
471 let canonicalized = fs.canonicalize(file.as_path()).await;
472 assert!(canonicalized.is_ok());
473}
474
475#[gpui::test]
476async fn test_rename(executor: BackgroundExecutor) {
477 let fs = FakeFs::new(executor.clone());
478 fs.insert_tree(
479 path!("/root"),
480 json!({
481 "src": {
482 "file_a.txt": "content a",
483 "file_b.txt": "content b"
484 }
485 }),
486 )
487 .await;
488
489 fs.rename(
490 Path::new(path!("/root/src/file_a.txt")),
491 Path::new(path!("/root/src/new/renamed_a.txt")),
492 RenameOptions {
493 create_parents: true,
494 ..Default::default()
495 },
496 )
497 .await
498 .unwrap();
499
500 // Assert that the `file_a.txt` file was being renamed and moved to a
501 // different directory that did not exist before.
502 assert_eq!(
503 fs.files(),
504 vec![
505 PathBuf::from(path!("/root/src/file_b.txt")),
506 PathBuf::from(path!("/root/src/new/renamed_a.txt")),
507 ]
508 );
509
510 let result = fs
511 .rename(
512 Path::new(path!("/root/src/file_b.txt")),
513 Path::new(path!("/root/src/old/renamed_b.txt")),
514 RenameOptions {
515 create_parents: false,
516 ..Default::default()
517 },
518 )
519 .await;
520
521 // Assert that the `file_b.txt` file was not renamed nor moved, as
522 // `create_parents` was set to `false`.
523 // different directory that did not exist before.
524 assert!(result.is_err());
525 assert_eq!(
526 fs.files(),
527 vec![
528 PathBuf::from(path!("/root/src/file_b.txt")),
529 PathBuf::from(path!("/root/src/new/renamed_a.txt")),
530 ]
531 );
532}
533
534#[gpui::test]
535#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
536async fn test_realfs_parallel_rename_without_overwrite_preserves_losing_source(
537 executor: BackgroundExecutor,
538) {
539 let temp_dir = TempDir::new().unwrap();
540 let root = temp_dir.path();
541 let source_a = root.join("dir_a/shared.txt");
542 let source_b = root.join("dir_b/shared.txt");
543 let target = root.join("shared.txt");
544
545 std::fs::create_dir_all(source_a.parent().unwrap()).unwrap();
546 std::fs::create_dir_all(source_b.parent().unwrap()).unwrap();
547 std::fs::write(&source_a, "from a").unwrap();
548 std::fs::write(&source_b, "from b").unwrap();
549
550 let fs = RealFs::new(None, executor);
551 let (first_result, second_result) = futures::future::join(
552 fs.rename(&source_a, &target, RenameOptions::default()),
553 fs.rename(&source_b, &target, RenameOptions::default()),
554 )
555 .await;
556
557 assert_ne!(first_result.is_ok(), second_result.is_ok());
558 assert!(target.exists());
559 assert_eq!(source_a.exists() as u8 + source_b.exists() as u8, 1);
560}
561
562#[gpui::test]
563#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
564async fn test_realfs_rename_ignore_if_exists_leaves_source_and_target_unchanged(
565 executor: BackgroundExecutor,
566) {
567 let temp_dir = TempDir::new().unwrap();
568 let root = temp_dir.path();
569 let source = root.join("source.txt");
570 let target = root.join("target.txt");
571
572 std::fs::write(&source, "from source").unwrap();
573 std::fs::write(&target, "from target").unwrap();
574
575 let fs = RealFs::new(None, executor);
576 let result = fs
577 .rename(
578 &source,
579 &target,
580 RenameOptions {
581 ignore_if_exists: true,
582 ..Default::default()
583 },
584 )
585 .await;
586
587 assert!(result.is_ok());
588
589 assert_eq!(std::fs::read_to_string(&source).unwrap(), "from source");
590 assert_eq!(std::fs::read_to_string(&target).unwrap(), "from target");
591}
592
593#[gpui::test]
594#[cfg(unix)]
595async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) {
596 let tempdir = TempDir::new().unwrap();
597 let path = tempdir.path();
598 let fs = RealFs::new(None, executor);
599 let symlink_path = path.join("symlink");
600 gpui::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap();
601 let metadata = fs
602 .metadata(&symlink_path)
603 .await
604 .expect("metadata call succeeds")
605 .expect("metadata returned");
606 assert!(metadata.is_symlink);
607 assert!(!metadata.is_dir);
608 assert!(!metadata.is_fifo);
609 assert!(!metadata.is_executable);
610 // don't care about len or mtime on symlinks?
611}
612
613#[gpui::test]
614#[cfg(unix)]
615async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) {
616 let tempdir = TempDir::new().unwrap();
617 let path = tempdir.path();
618 let fs = RealFs::new(None, executor);
619 let symlink_path = path.join("symlink");
620 gpui::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap();
621 let metadata = fs
622 .metadata(&symlink_path)
623 .await
624 .expect("metadata call succeeds")
625 .expect("metadata returned");
626 assert!(metadata.is_symlink);
627 assert!(!metadata.is_dir);
628 assert!(!metadata.is_fifo);
629 assert!(!metadata.is_executable);
630 // don't care about len or mtime on symlinks?
631}
632
633#[gpui::test]
634async fn test_fake_fs_trash(executor: BackgroundExecutor) {
635 let fs = FakeFs::new(executor.clone());
636 fs.insert_tree(
637 path!("/root"),
638 json!({
639 "src": {
640 "file_c.txt": "File C",
641 "file_d.txt": "File D"
642 },
643 "file_a.txt": "File A",
644 "file_b.txt": "File B",
645 }),
646 )
647 .await;
648
649 // Trashing a file.
650 let path = path!("/root/file_a.txt").as_ref();
651 fs.trash(path, Default::default())
652 .await
653 .expect("should be able to trash {path:?}");
654
655 assert_eq!(
656 fs.files(),
657 vec![
658 PathBuf::from(path!("/root/file_b.txt")),
659 PathBuf::from(path!("/root/src/file_c.txt")),
660 PathBuf::from(path!("/root/src/file_d.txt"))
661 ]
662 );
663
664 // Trashing a directory.
665 let path = path!("/root/src").as_ref();
666 fs.trash(
667 path,
668 RemoveOptions {
669 recursive: true,
670 ..Default::default()
671 },
672 )
673 .await
674 .expect("should be able to trash {path:?}");
675
676 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_b.txt"))]);
677}
678
679#[gpui::test]
680async fn test_fake_fs_restore(executor: BackgroundExecutor) {
681 let fs = FakeFs::new(executor.clone());
682 fs.insert_tree(
683 path!("/root"),
684 json!({
685 "src": {
686 "file_a.txt": "File A",
687 "file_b.txt": "File B",
688 },
689 "file_c.txt": "File C",
690 }),
691 )
692 .await;
693
694 // Attempt deleting a file, asserting that the filesystem no longer reports
695 // it as part of its list of files, restore it and verify that the list of
696 // files and trash has been updated accordingly.
697 let path = path!("/root/src/file_a.txt").as_ref();
698 let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
699
700 fs.restore(trashed_entry).await.unwrap();
701
702 assert_eq!(
703 fs.files(),
704 vec![
705 PathBuf::from(path!("/root/file_c.txt")),
706 PathBuf::from(path!("/root/src/file_a.txt")),
707 PathBuf::from(path!("/root/src/file_b.txt"))
708 ]
709 );
710
711 // Deleting and restoring a directory should also remove all of its files
712 // but create a single trashed entry, which should be removed after
713 // restoration.
714 let options = RemoveOptions {
715 recursive: true,
716 ..Default::default()
717 };
718 let path = path!("/root/src/").as_ref();
719 let trashed_entry = fs.trash(path, options).await.unwrap();
720
721 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
722
723 fs.restore(trashed_entry).await.unwrap();
724
725 assert_eq!(
726 fs.files(),
727 vec![
728 PathBuf::from(path!("/root/file_c.txt")),
729 PathBuf::from(path!("/root/src/file_a.txt")),
730 PathBuf::from(path!("/root/src/file_b.txt"))
731 ]
732 );
733
734 // A collision error should be returned in case a file is being restored to
735 // a path where a file already exists.
736 let path = path!("/root/src/file_a.txt").as_ref();
737 let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
738
739 assert_eq!(
740 fs.files(),
741 vec![
742 PathBuf::from(path!("/root/file_c.txt")),
743 PathBuf::from(path!("/root/src/file_b.txt"))
744 ]
745 );
746
747 fs.write(path, "New File A".as_bytes()).await.unwrap();
748
749 assert_eq!(
750 fs.files(),
751 vec![
752 PathBuf::from(path!("/root/file_c.txt")),
753 PathBuf::from(path!("/root/src/file_a.txt")),
754 PathBuf::from(path!("/root/src/file_b.txt"))
755 ]
756 );
757
758 let file_contents = fs.files_with_contents(path);
759 assert!(fs.restore(trashed_entry).await.is_err());
760 assert_eq!(
761 file_contents,
762 vec![(PathBuf::from(path), b"New File A".to_vec())]
763 );
764
765 // A collision error should be returned in case a directory is being
766 // restored to a path where a directory already exists.
767 let options = RemoveOptions {
768 recursive: true,
769 ..Default::default()
770 };
771 let path = path!("/root/src/").as_ref();
772 let trashed_entry = fs.trash(path, options).await.unwrap();
773
774 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
775
776 fs.create_dir(path).await.unwrap();
777
778 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
779
780 let result = fs.restore(trashed_entry).await;
781 assert!(result.is_err());
782
783 assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
784}
785
786/// Create a directory symlink (`link` -> `target`) in a cross-platform way.
787///
788/// Returns `Err` when the platform cannot create symlinks (e.g. Windows without
789/// the create-symlink privilege), so callers can skip a scenario gracefully
790/// rather than failing the whole test.
791fn make_dir_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
792 #[cfg(unix)]
793 {
794 std::os::unix::fs::symlink(target, link)
795 }
796 #[cfg(windows)]
797 {
798 std::os::windows::fs::symlink_dir(target, link)
799 }
800 #[cfg(not(any(unix, windows)))]
801 {
802 let _ = (target, link);
803 Err(std::io::Error::new(
804 std::io::ErrorKind::Unsupported,
805 "symlinks are not supported on this platform",
806 ))
807 }
808}
809
810/// Waits up to `timeout` for `events` to deliver something that covers the
811/// written file: either a path event whose path satisfies `path_matches`, or a
812/// `Rescan` (which tells the consumer to re-scan this watcher's whole tree, so
813/// it would discover the file anyway). Returns `false` if nothing relevant
814/// arrives before the timeout.
815async fn watcher_delivered_event(
816 events: &mut (impl futures::Stream<Item = Vec<PathEvent>> + Unpin),
817 executor: &BackgroundExecutor,
818 timeout: Duration,
819 path_matches: &(dyn Fn(&Path) -> bool + Send + Sync),
820) -> bool {
821 let timeout = executor.timer(timeout).fuse();
822 futures::pin_mut!(timeout);
823 loop {
824 futures::select_biased! {
825 batch = events.next().fuse() => {
826 let Some(batch) = batch else { return false };
827 let covered = batch.iter().any(|event| {
828 path_matches(&event.path) || event.kind == Some(PathEventKind::Rescan)
829 });
830 if covered {
831 return true;
832 }
833 }
834 _ = timeout => return false,
835 }
836 }
837}
838
839/// Exercises a spread of real watchers whose registered watch path is spelled
840/// differently from the path the OS reports events under. Each scenario watches
841/// some directory and then mutates the on-disk file; a correct watcher must
842/// deliver an event (or a rescan) for every scenario.
843///
844/// This asserts the residual path-aliasing bugs that real-casing the watch root
845/// at add-time does NOT fix. The headline failure is `symlink_ancestor`:
846/// watching a path that traverses a symlinked ancestor. On macOS FSEvents
847/// reports events under the resolved real path, which no longer has the
848/// symlinked prefix the watch root was registered with, so the events are
849/// filtered out and never delivered.
850///
851/// Platform notes (the test runs everywhere but scenarios self-skip when they
852/// cannot apply):
853/// - Case scenarios require a case-insensitive filesystem (macOS/Windows
854/// default; case-sensitive Linux/APFS skip them).
855/// - Symlink scenarios require symlink creation (skipped on Windows without the
856/// privilege).
857/// - `symlink_ancestor` fails on macOS (FSEvents canonicalizes) but is expected
858/// to pass on Linux (notify reconstructs paths from the watch path you pass),
859/// which is itself a useful demonstration that this is an FSEvents-specific
860/// bug.
861#[gpui::test]
862async fn test_realfs_watch_aliased_watch_paths_deliver_events(
863 executor: BackgroundExecutor,
864 cx: &mut TestAppContext,
865) {
866 cx.executor().allow_parking();
867
868 let fs = RealFs::new(None, executor.clone());
869 let temp_dir = TempDir::new().expect("create temp dir");
870 let root = temp_dir.path().to_path_buf();
871 let latency = Duration::from_millis(10);
872
873 // Probe the real filesystem for case sensitivity rather than guessing from
874 // the platform.
875 std::fs::create_dir_all(root.join("CaseProbe")).expect("create case probe dir");
876 let case_insensitive = root.join("caseprobe").exists();
877 eprintln!("filesystem is case-insensitive: {case_insensitive}");
878
879 struct Scenario {
880 name: &'static str,
881 events: Pin<Box<dyn Send + futures::Stream<Item = Vec<PathEvent>>>>,
882 _watcher: Arc<dyn Watcher>,
883 path_matches: Box<dyn Fn(&Path) -> bool + Send + Sync>,
884 action: Option<Box<dyn FnOnce() + Send>>,
885 }
886
887 let mut scenarios: Vec<Scenario> = Vec::new();
888 let mut skipped: Vec<String> = Vec::new();
889
890 // --- Headline residual bug: watch path traverses a symlinked ancestor. ---
891 {
892 let real = root.join("ancestor_real");
893 let inner = real.join("inner");
894 std::fs::create_dir_all(&inner).expect("create symlinked-ancestor target");
895 let link = root.join("ancestor_link");
896 match make_dir_symlink(&real, &link) {
897 Ok(()) => {
898 let (events, watcher) = fs.watch(&link.join("inner"), latency).await;
899 let file = inner.join("symlink_ancestor.txt");
900 scenarios.push(Scenario {
901 name: "symlink_ancestor",
902 events,
903 _watcher: watcher,
904 path_matches: Box::new(|path| {
905 path.ends_with(Path::new("inner/symlink_ancestor.txt"))
906 }),
907 action: Some(Box::new(move || {
908 std::fs::write(&file, b"x").expect("write symlink-ancestor file");
909 })),
910 });
911 }
912 Err(error) => skipped.push(format!("symlink_ancestor (cannot symlink: {error})")),
913 }
914 }
915
916 // --- Control: watching a symlinked root IS handled (RealFs::watch follows
917 // the root symlink and also watches the target). ---
918 {
919 let real = root.join("root_real");
920 std::fs::create_dir_all(&real).expect("create symlinked-root target");
921 let link = root.join("root_link");
922 match make_dir_symlink(&real, &link) {
923 Ok(()) => {
924 let (events, watcher) = fs.watch(&link, latency).await;
925 let file = real.join("symlink_root.txt");
926 scenarios.push(Scenario {
927 name: "symlink_root",
928 events,
929 _watcher: watcher,
930 path_matches: Box::new(|path| path.ends_with(Path::new("symlink_root.txt"))),
931 action: Some(Box::new(move || {
932 std::fs::write(&file, b"x").expect("write symlink-root file");
933 })),
934 });
935 }
936 Err(error) => skipped.push(format!("symlink_root (cannot symlink: {error})")),
937 }
938 }
939
940 // --- Control: wrong-case watch root (the originally-reported bug, which the
941 // real-casing fix already addresses). ---
942 if case_insensitive {
943 let real = root.join("CaseAlpha");
944 std::fs::create_dir_all(&real).expect("create wrong-case root");
945 let lower = PathBuf::from(real.to_string_lossy().to_lowercase());
946 let (events, watcher) = fs.watch(&lower, latency).await;
947 let file = real.join("alpha.txt");
948 scenarios.push(Scenario {
949 name: "wrong_case_root",
950 events,
951 _watcher: watcher,
952 path_matches: Box::new(|path| path.ends_with(Path::new("alpha.txt"))),
953 action: Some(Box::new(move || {
954 std::fs::write(&file, b"x").expect("write wrong-case-root file");
955 })),
956 });
957 } else {
958 skipped.push("wrong_case_root (case-sensitive fs)".to_owned());
959 }
960
961 // --- Control: wrong-case nested watch path. ---
962 if case_insensitive {
963 let real = root.join("CaseBravo").join("Inner");
964 std::fs::create_dir_all(&real).expect("create wrong-case nested dir");
965 let lower = PathBuf::from(real.to_string_lossy().to_lowercase());
966 let (events, watcher) = fs.watch(&lower, latency).await;
967 let file = real.join("bravo.txt");
968 scenarios.push(Scenario {
969 name: "nested_wrong_case",
970 events,
971 _watcher: watcher,
972 path_matches: Box::new(|path| path.ends_with(Path::new("bravo.txt"))),
973 action: Some(Box::new(move || {
974 std::fs::write(&file, b"x").expect("write nested-wrong-case file");
975 })),
976 });
977 } else {
978 skipped.push("nested_wrong_case (case-sensitive fs)".to_owned());
979 }
980
981 // --- Residual bug: the watched root is renamed to a different casing after
982 // the watch is established, so later events arrive under a spelling the
983 // registered (old-case) root no longer matches. ---
984 if case_insensitive {
985 let real = root.join("CaseEcho");
986 std::fs::create_dir_all(&real).expect("create case-rename dir");
987 let (events, watcher) = fs.watch(&real, latency).await;
988 let renamed = root.join("CASEECHO");
989 let file = renamed.join("echo.txt");
990 scenarios.push(Scenario {
991 name: "case_rename_root",
992 events,
993 _watcher: watcher,
994 path_matches: Box::new(|path| path.ends_with(Path::new("echo.txt"))),
995 action: Some(Box::new(move || {
996 std::fs::rename(&real, &renamed).expect("case-only rename of watched root");
997 std::fs::write(&file, b"x").expect("write case-rename file");
998 })),
999 });
1000 } else {
1001 skipped.push("case_rename_root (case-sensitive fs)".to_owned());
1002 }
1003
1004 // Let every watch settle before mutating, then perform all mutations.
1005 executor.timer(Duration::from_millis(250)).await;
1006 for scenario in &mut scenarios {
1007 if let Some(action) = scenario.action.take() {
1008 action();
1009 }
1010 }
1011
1012 let mut failures = Vec::new();
1013 for scenario in &mut scenarios {
1014 let delivered = watcher_delivered_event(
1015 &mut scenario.events,
1016 &executor,
1017 Duration::from_secs(3),
1018 scenario.path_matches.as_ref(),
1019 )
1020 .await;
1021 eprintln!("scenario {}: delivered={delivered}", scenario.name);
1022 if !delivered {
1023 failures.push(scenario.name);
1024 }
1025 }
1026
1027 for name in &skipped {
1028 eprintln!("scenario skipped: {name}");
1029 }
1030
1031 assert!(
1032 failures.is_empty(),
1033 "watchers failed to deliver events for {failures:?} (skipped: {skipped:?})"
1034 );
1035}
1036
1037#[gpui::test]
1038#[ignore = "stress test; run explicitly when needed"]
1039async fn test_realfs_watch_stress_reports_missed_paths(
1040 executor: BackgroundExecutor,
1041 cx: &mut TestAppContext,
1042) {
1043 const FILE_COUNT: usize = 32000;
1044 cx.executor().allow_parking();
1045
1046 let fs = RealFs::new(None, executor.clone());
1047 let temp_dir = TempDir::new().expect("create temp dir");
1048 let root = temp_dir.path();
1049
1050 let mut file_paths = Vec::with_capacity(FILE_COUNT);
1051 let mut expected_paths = BTreeSet::new();
1052
1053 for index in 0..FILE_COUNT {
1054 let dir_path = root.join(format!("dir-{index:04}"));
1055 let file_path = dir_path.join("file.txt");
1056 fs.create_dir(&dir_path).await.expect("create watched dir");
1057 fs.write(&file_path, b"before")
1058 .await
1059 .expect("create initial file");
1060 expected_paths.insert(file_path.clone());
1061 file_paths.push(file_path);
1062 }
1063
1064 let (mut events, watcher) = fs.watch(root, Duration::from_millis(10)).await;
1065 let _watcher = watcher;
1066
1067 for file_path in &expected_paths {
1068 _watcher
1069 .add(file_path.parent().expect("file has parent"))
1070 .expect("add explicit directory watch");
1071 }
1072
1073 for (index, file_path) in file_paths.iter().enumerate() {
1074 let content = format!("after-{index}");
1075 fs.write(file_path, content.as_bytes())
1076 .await
1077 .expect("modify watched file");
1078 }
1079
1080 let mut changed_paths = BTreeSet::new();
1081 let mut rescan_count: u32 = 0;
1082 let timeout = executor.timer(Duration::from_secs(10)).fuse();
1083
1084 futures::pin_mut!(timeout);
1085
1086 let mut ticks = 0;
1087 while ticks < 1000 {
1088 if let Some(batch) = events.next().fuse().now_or_never().flatten() {
1089 for event in batch {
1090 if event.kind == Some(PathEventKind::Rescan) {
1091 rescan_count += 1;
1092 }
1093 if expected_paths.contains(&event.path) {
1094 changed_paths.insert(event.path);
1095 }
1096 }
1097 if changed_paths.len() == expected_paths.len() {
1098 break;
1099 }
1100 ticks = 0;
1101 } else {
1102 ticks += 1;
1103 executor.timer(Duration::from_millis(10)).await;
1104 }
1105 }
1106
1107 let missed_paths: BTreeSet<_> = expected_paths.difference(&changed_paths).cloned().collect();
1108
1109 eprintln!(
1110 "realfs watch stress: expected={}, observed={}, missed={}, rescan={}",
1111 expected_paths.len(),
1112 changed_paths.len(),
1113 missed_paths.len(),
1114 rescan_count
1115 );
1116
1117 assert!(
1118 missed_paths.is_empty() || rescan_count > 0,
1119 "missed {} paths without rescan being reported",
1120 missed_paths.len()
1121 );
1122}
1123