1use anyhow::{Result, bail};
2use futures::try_join;
3use rustc_hash::FxHashMap;
4use turbo_rcstr::RcStr;
5use turbo_tasks::{Completion, ResolvedVc, TryJoinIterExt, Vc, turbobail};
6
7use crate::{
8 DirectoryContent, DirectoryEntry, FileSystem, FileSystemPath, LinkContent, LinkType, glob::Glob,
9};
10
11#[turbo_tasks::value]
12#[derive(Default, Debug)]
13pub struct ReadGlobResult {
14 pub results: FxHashMap<RcStr, DirectoryEntry>,
15 pub inner: FxHashMap<RcStr, ResolvedVc<ReadGlobResult>>,
16}
17
18#[turbo_tasks::function(fs)]
23pub async fn read_glob(directory: FileSystemPath, glob: Vc<Glob>) -> Result<Vc<ReadGlobResult>> {
24 read_glob_internal("", directory, glob).await
25}
26
27#[turbo_tasks::function(fs)]
28async fn read_glob_inner(
29 prefix: RcStr,
30 directory: FileSystemPath,
31 glob: Vc<Glob>,
32) -> Result<Vc<ReadGlobResult>> {
33 read_glob_internal(&prefix, directory, glob).await
34}
35
36async fn read_glob_internal(
38 prefix: &str,
39 directory: FileSystemPath,
40 glob: Vc<Glob>,
41) -> Result<Vc<ReadGlobResult>> {
42 let dir = directory.read_dir().await?;
43 let mut result = ReadGlobResult::default();
44 let glob_value = glob.await?;
45 let handle_file = |result: &mut ReadGlobResult,
46 entry_path: &RcStr,
47 segment: &RcStr,
48 entry: &DirectoryEntry| {
49 if glob_value.matches(entry_path) {
50 result.results.insert(segment.clone(), entry.clone());
51 }
52 };
53 let handle_dir = async |result: &mut ReadGlobResult,
54 entry_path: RcStr,
55 segment: &RcStr,
56 path: &FileSystemPath| {
57 if glob_value.can_match_in_directory(&entry_path) {
58 result.inner.insert(
59 segment.clone(),
60 read_glob_inner(entry_path, path.clone(), glob)
61 .to_resolved()
62 .await?,
63 );
64 }
65 anyhow::Ok(())
66 };
67
68 match &*dir {
69 DirectoryContent::Entries(entries) => {
70 for (segment, entry) in entries.iter() {
71 let entry_path: RcStr = if prefix.is_empty() {
72 segment.clone()
73 } else {
74 format!("{prefix}/{segment}").into()
75 };
76
77 match entry {
78 DirectoryEntry::File(_) => {
79 handle_file(&mut result, &entry_path, segment, entry);
80 }
81 DirectoryEntry::Directory(path) => {
82 handle_file(&mut result, &entry_path, segment, entry);
84 handle_dir(&mut result, entry_path, segment, path).await?;
86 }
87 DirectoryEntry::Symlink(path) => {
88 if let LinkContent::Link { link_type, .. } = &*path.read_link().await? {
89 if link_type.contains(LinkType::DIRECTORY) {
90 resolve_symlink_safely(entry.clone()).await?;
92
93 handle_file(&mut result, &entry_path, segment, entry);
95 handle_dir(&mut result, entry_path, segment, path).await?;
97 } else {
98 handle_file(&mut result, &entry_path, segment, entry);
99 }
100 }
101 }
102 DirectoryEntry::Other(_) | DirectoryEntry::Error(_) => continue,
103 }
104 }
105 }
106 DirectoryContent::NotFound => {}
107 }
108 Ok(ReadGlobResult::cell(result))
109}
110
111async fn resolve_symlink_safely(entry: DirectoryEntry) -> Result<DirectoryEntry> {
113 let resolved_entry = entry.clone().resolve_symlink().await?;
114 if resolved_entry != entry && matches!(&resolved_entry, DirectoryEntry::Directory(_)) {
115 let source_path = entry.path().unwrap();
124 if source_path.is_inside_or_equal(&resolved_entry.clone().path().unwrap()) {
125 bail!("'{source_path}' is a symlink causes that causes an infinite loop!",)
126 }
127 }
128 Ok(resolved_entry)
129}
130
131#[turbo_tasks::function(fs)]
137pub async fn track_glob(
138 directory: FileSystemPath,
139 glob: Vc<Glob>,
140 include_dot_files: bool,
141) -> Result<Vc<Completion>> {
142 track_glob_internal("", directory, glob, include_dot_files).await
143}
144
145#[turbo_tasks::function(fs)]
146async fn track_glob_inner(
147 prefix: RcStr,
148 directory: FileSystemPath,
149 glob: Vc<Glob>,
150 include_dot_files: bool,
151) -> Result<Vc<Completion>> {
152 track_glob_internal(&prefix, directory, glob, include_dot_files).await
153}
154
155async fn track_glob_internal(
156 prefix: &str,
157 directory: FileSystemPath,
158 glob: Vc<Glob>,
159 include_dot_files: bool,
160) -> Result<Vc<Completion>> {
161 let dir = directory.read_dir().await?;
162 let glob_value = glob.await?;
163 let fs = directory.fs().to_resolved().await?;
164 let mut reads = Vec::new();
165 let mut completions = Vec::new();
166 let mut types = Vec::new();
167 match &*dir {
168 DirectoryContent::Entries(entries) => {
169 for (segment, entry) in entries.iter() {
170 if !include_dot_files && segment.starts_with('.') {
171 continue;
172 }
173 let entry_path = if prefix.is_empty() {
176 segment.clone()
177 } else {
178 format!("{prefix}/{segment}").into()
179 };
180
181 match resolve_symlink_safely(entry.clone()).await? {
182 DirectoryEntry::Directory(path) => {
183 if glob_value.can_match_in_directory(&entry_path) {
184 completions.push(track_glob_inner(
185 entry_path,
186 path.clone(),
187 glob,
188 include_dot_files,
189 ));
190 }
191 }
192 DirectoryEntry::File(path) => {
193 if glob_value.matches(&entry_path) {
194 reads.push(fs.read(path.clone()))
195 }
196 }
197 DirectoryEntry::Symlink(symlink_path) => turbobail!(
198 "resolve_symlink_safely() should have resolved all symlinks or returned \
199 an error, but found unresolved symlink at path: '{entry_path}'. Found \
200 path: '{symlink_path}'. Please report this as a bug.",
201 ),
202 DirectoryEntry::Other(path) => {
203 if glob_value.matches(&entry_path) {
204 types.push(path.get_type())
205 }
206 }
207 DirectoryEntry::Error(_) => {}
212 }
213 }
214 }
215 DirectoryContent::NotFound => {}
216 }
217 try_join!(
218 reads.iter().try_join(),
219 types.iter().try_join(),
220 completions.iter().try_join()
221 )?;
222 Ok(Completion::new())
223}
224
225#[cfg(test)]
226pub mod tests {
227
228 use std::{
229 collections::HashMap,
230 fs::{File, create_dir},
231 io::prelude::*,
232 };
233
234 use turbo_rcstr::{RcStr, rcstr};
235 use turbo_tasks::{
236 Completion, Effects, OperationVc, ReadRef, Vc, read_strongly_consistent_and_apply_effects,
237 take_effects,
238 };
239 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
240
241 use crate::{
242 DirectoryEntry, DiskFileSystem, FileContent, FileSystem, FileSystemPath,
243 glob::{Glob, GlobOptions},
244 };
245
246 fn symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
247 target: Q,
248 path: P,
249 ) -> std::io::Result<()> {
250 assert!(target.as_ref().is_absolute());
251 let _ = std::fs::remove_dir(&path);
252 let _ = std::fs::remove_file(&path);
253
254 #[cfg(unix)]
255 {
256 std::os::unix::fs::symlink(target, path)
257 }
258 #[cfg(windows)]
259 {
260 let metadata = std::fs::metadata(&target).ok();
261 if metadata.is_none_or(|m| m.is_file()) {
262 std::os::windows::fs::symlink_file(target, path)
263 } else {
264 std::os::windows::fs::junction_point(target, path)
265 }
266 }
267 }
268
269 #[turbo_tasks::function(operation, root)]
270 async fn assert_read_glob_basic_operation(path: RcStr) -> anyhow::Result<()> {
271 let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));
272 let root = fs.root().await?;
273 let read_dir = root
274 .read_glob(Glob::new(rcstr!("**"), GlobOptions::default()))
275 .await
276 .unwrap();
277 assert_eq!(read_dir.results.len(), 2);
278 assert_eq!(
279 read_dir.results.get("foo"),
280 Some(&DirectoryEntry::File(fs.root().await?.join("foo")?))
281 );
282 assert_eq!(
283 read_dir.results.get("sub"),
284 Some(&DirectoryEntry::Directory(fs.root().await?.join("sub")?))
285 );
286 assert_eq!(read_dir.inner.len(), 1);
287 let inner = &*read_dir.inner.get("sub").unwrap().await?;
288 assert_eq!(inner.results.len(), 1);
289 assert_eq!(
290 inner.results.get("bar"),
291 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
292 );
293 assert_eq!(inner.inner.len(), 0);
294
295 let read_dir = root
296 .read_glob(Glob::new(rcstr!("**/bar"), GlobOptions::default()))
297 .await
298 .unwrap();
299 assert_eq!(read_dir.results.len(), 0);
300 assert_eq!(read_dir.inner.len(), 1);
301 let inner = &*read_dir.inner.get("sub").unwrap().await?;
302 assert_eq!(inner.results.len(), 1);
303 assert_eq!(
304 inner.results.get("bar"),
305 Some(&DirectoryEntry::File(fs.root().await?.join("sub/bar")?))
306 );
307 assert_eq!(inner.inner.len(), 0);
308
309 Ok(())
310 }
311
312 #[turbo_tasks::function(operation, root)]
313 async fn assert_read_glob_symlinks_operation(path: RcStr) -> anyhow::Result<()> {
314 let fs = DiskFileSystem::new(rcstr!("temp"), Vc::cell(path));
315 let root = fs.root().await?;
316 let read_dir = root
318 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
319 .await
320 .unwrap();
321 assert_eq!(read_dir.results.len(), 0);
322 let inner = &*read_dir.inner.get("sub").unwrap().await?;
323 assert_eq!(
324 inner.results,
325 HashMap::from_iter([
326 (
327 "link-foo.js".into(),
328 DirectoryEntry::Symlink(root.join("sub/link-foo.js")?),
329 ),
330 (
331 "link-root.js".into(),
332 DirectoryEntry::Symlink(root.join("sub/link-root.js")?),
333 ),
334 (
335 "foo.js".into(),
336 DirectoryEntry::File(root.join("sub/foo.js")?),
337 ),
338 ])
339 );
340 assert_eq!(inner.inner.len(), 0);
341
342 let read_dir = root
344 .read_glob(Glob::new(rcstr!("sub/dir/*"), GlobOptions::default()))
345 .await
346 .unwrap();
347 assert_eq!(read_dir.results.len(), 0);
348 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
349 assert_eq!(inner_sub.results.len(), 0);
350 let inner_sub_dir = &*inner_sub.inner.get("dir").unwrap().await?;
351 assert_eq!(
352 inner_sub_dir.results,
353 HashMap::from_iter([(
354 "index.js".into(),
355 DirectoryEntry::File(root.join("sub/dir/index.js")?),
356 )])
357 );
358 assert_eq!(inner_sub_dir.inner.len(), 0);
359
360 Ok(())
361 }
362
363 #[turbo_tasks::function(operation, root)]
364 async fn assert_dead_symlink_read_glob_operation(path: RcStr) -> anyhow::Result<()> {
365 let fs =
366 Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)));
367 let root = fs.root().owned().await?;
368 let read_dir = root
369 .read_glob(Glob::new(rcstr!("sub/*.js"), GlobOptions::default()))
370 .await?;
371 assert_eq!(read_dir.results.len(), 0);
372 assert_eq!(read_dir.inner.len(), 1);
373 let inner_sub = &*read_dir.inner.get("sub").unwrap().await?;
374 assert_eq!(inner_sub.inner.len(), 0);
375 assert_eq!(
376 inner_sub.results,
377 HashMap::from_iter([
378 (
379 "foo.js".into(),
380 DirectoryEntry::File(root.join("sub/foo.js")?),
381 ),
382 (
383 "dead_link.js".into(),
384 DirectoryEntry::Symlink(root.join("sub/dead_link.js")?),
385 )
386 ])
387 );
388
389 Ok(())
390 }
391
392 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
393 async fn read_glob_basic() {
394 let scratch = tempfile::tempdir().unwrap();
395 {
396 let path = scratch.path();
398 File::create_new(path.join("foo"))
399 .unwrap()
400 .write_all(b"foo")
401 .unwrap();
402 create_dir(path.join("sub")).unwrap();
403 File::create_new(path.join("sub/bar"))
404 .unwrap()
405 .write_all(b"bar")
406 .unwrap();
407 }
408 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
409 BackendOptions::default(),
410 noop_backing_storage(),
411 ));
412 let path: RcStr = scratch.path().to_str().unwrap().into();
413 tt.run_once(async {
414 assert_read_glob_basic_operation(path)
415 .read_strongly_consistent()
416 .await?;
417
418 anyhow::Ok(())
419 })
420 .await
421 .unwrap();
422 }
423
424 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
425 async fn read_glob_symlinks() {
426 let scratch = tempfile::tempdir().unwrap();
427 {
428 let path = scratch.path();
433 create_dir(path.join("sub")).unwrap();
434 let foo = path.join("sub/foo.js");
435 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
436 symlink(&foo, path.join("sub/link-foo.js")).unwrap();
437
438 let root = path.join("root.js");
439 File::create_new(&root).unwrap().write_all(b"root").unwrap();
440 symlink(&root, path.join("sub/link-root.js")).unwrap();
441
442 let dir = path.join("dir");
443 create_dir(&dir).unwrap();
444 File::create_new(dir.join("index.js"))
445 .unwrap()
446 .write_all(b"dir index")
447 .unwrap();
448 symlink(&dir, path.join("sub/dir")).unwrap();
449 }
450 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
451 BackendOptions::default(),
452 noop_backing_storage(),
453 ));
454 let path: RcStr = scratch.path().to_str().unwrap().into();
455 tt.run_once(async {
456 assert_read_glob_symlinks_operation(path)
457 .read_strongly_consistent()
458 .await?;
459
460 anyhow::Ok(())
461 })
462 .await
463 .unwrap();
464 }
465
466 #[turbo_tasks::function(operation, root)]
467 pub async fn delete(path: FileSystemPath) -> anyhow::Result<()> {
468 path.write(FileContent::NotFound.cell()).await?;
469 Ok(())
470 }
471
472 #[turbo_tasks::function(operation, root)]
473 pub async fn write(path: FileSystemPath, contents: RcStr) -> anyhow::Result<()> {
474 path.write(
475 FileContent::Content(crate::File::from_bytes(contents.to_string().into_bytes())).cell(),
476 )
477 .await?;
478 Ok(())
479 }
480
481 #[turbo_tasks::function(operation, root)]
482 pub fn track_star_star_glob(path: FileSystemPath) -> Vc<Completion> {
483 path.track_glob(Glob::new(rcstr!("**"), GlobOptions::default()), false)
484 }
485
486 #[turbo_tasks::function(operation, root)]
487 fn disk_file_system_root_operation(path: RcStr) -> Vc<FileSystemPath> {
488 let fs =
489 Vc::upcast::<Box<dyn FileSystem>>(DiskFileSystem::new(rcstr!("temp"), Vc::cell(path)));
490 fs.root()
491 }
492
493 #[turbo_tasks::function(operation, root)]
494 async fn extract_effects_operation(op: OperationVc<()>) -> anyhow::Result<Vc<Effects>> {
495 let _ = op.resolve().strongly_consistent().await?;
496 Ok(take_effects(op).await?.cell())
497 }
498
499 #[turbo_tasks::function(operation, root)]
500 async fn track_glob_operation(path: RcStr, glob: RcStr) -> anyhow::Result<()> {
501 let root = disk_file_system_root_operation(path)
502 .read_strongly_consistent()
503 .await?;
504 root.track_glob(Glob::new(glob, GlobOptions::default()), false)
505 .await?;
506 Ok(())
507 }
508
509 #[turbo_tasks::function(operation, root)]
510 async fn read_glob_operation(path: RcStr, glob: RcStr) -> anyhow::Result<()> {
511 let root = disk_file_system_root_operation(path)
512 .read_strongly_consistent()
513 .await?;
514 root.read_glob(Glob::new(glob, GlobOptions::default()))
515 .await?;
516 Ok(())
517 }
518
519 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
520 async fn track_glob_invalidations() {
521 let scratch = tempfile::tempdir().unwrap();
522
523 let path = scratch.path();
525 let dir = path.join("dir");
526 create_dir(&dir).unwrap();
527 File::create_new(dir.join("foo"))
528 .unwrap()
529 .write_all(b"foo")
530 .unwrap();
531 create_dir(dir.join("sub")).unwrap();
532 File::create_new(dir.join("sub/bar"))
533 .unwrap()
534 .write_all(b"bar")
535 .unwrap();
536 create_dir(dir.join("sub/.vim")).unwrap();
538 let gitignore = dir.join("sub/.vim/.gitignore");
539 File::create_new(&gitignore)
540 .unwrap()
541 .write_all(b"ignore")
542 .unwrap();
543 let link_target = path.join("link_target.js");
545 File::create_new(&link_target)
546 .unwrap()
547 .write_all(b"link_target")
548 .unwrap();
549 symlink(&link_target, dir.join("link.js")).unwrap();
550
551 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
552 BackendOptions::default(),
553 noop_backing_storage(),
554 ));
555 let path: RcStr = scratch.path().to_str().unwrap().into();
556 tt.run_once(async {
557 let root = disk_file_system_root_operation(path)
558 .read_strongly_consistent()
559 .await?;
560 let dir = root.join("dir")?;
561 let read_dir = track_star_star_glob(dir.clone())
562 .read_strongly_consistent()
563 .await?;
564
565 read_strongly_consistent_and_apply_effects(
567 extract_effects_operation(delete(root.join("dir/sub/.vim/.gitignore")?)),
568 |e| e,
569 )
570 .await?;
571
572 let read_dir2 = track_star_star_glob(dir.clone())
573 .read_strongly_consistent()
574 .await?;
575 assert!(ReadRef::ptr_eq(&read_dir, &read_dir2));
576
577 read_strongly_consistent_and_apply_effects(
579 extract_effects_operation(delete(root.join("dir/foo")?)),
580 |e| e,
581 )
582 .await?;
583
584 let read_dir2 = track_star_star_glob(dir.clone())
585 .read_strongly_consistent()
586 .await?;
587
588 assert!(!ReadRef::ptr_eq(&read_dir, &read_dir2));
589
590 read_strongly_consistent_and_apply_effects(
592 extract_effects_operation(write(
593 root.join("link_target.js")?,
594 rcstr!("new_contents"),
595 )),
596 |e| e,
597 )
598 .await?;
599 let read_dir3 = track_star_star_glob(dir.clone())
600 .read_strongly_consistent()
601 .await?;
602
603 assert!(!ReadRef::ptr_eq(&read_dir3, &read_dir2));
604
605 anyhow::Ok(())
606 })
607 .await
608 .unwrap();
609 }
610
611 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
612 async fn track_glob_symlinks_loop() {
613 let scratch = tempfile::tempdir().unwrap();
614 {
615 let path = scratch.path();
618 let sub = &path.join("sub");
619 create_dir(sub).unwrap();
620 let foo = sub.join("foo.js");
621 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
622 symlink(sub, sub.join("link")).unwrap();
624 }
625 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
626 BackendOptions::default(),
627 noop_backing_storage(),
628 ));
629 let path: RcStr = scratch.path().to_str().unwrap().into();
630 tt.run_once(async {
631 let err = track_glob_operation(path.clone(), rcstr!("**"))
632 .read_strongly_consistent()
633 .await
634 .expect_err("Should have detected an infinite loop");
635
636 assert_eq!(
637 "'sub/link' is a symlink causes that causes an infinite loop!",
638 format!("{}", err.root_cause())
639 );
640
641 let err = track_glob_operation(path, rcstr!("**"))
643 .read_strongly_consistent()
644 .await
645 .expect_err("Should have detected an infinite loop");
646
647 assert_eq!(
648 "'sub/link' is a symlink causes that causes an infinite loop!",
649 format!("{}", err.root_cause())
650 );
651
652 anyhow::Ok(())
653 })
654 .await
655 .unwrap();
656 }
657
658 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
660 async fn dead_symlinks() {
661 let scratch = tempfile::tempdir().unwrap();
662 {
663 let path = scratch.path();
665 let sub = &path.join("sub");
666 create_dir(sub).unwrap();
667 let foo = sub.join("foo.js");
668 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
669 symlink(sub.join("doesntexist.js"), sub.join("dead_link.js")).unwrap();
671 }
672 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
673 BackendOptions::default(),
674 noop_backing_storage(),
675 ));
676 let path: RcStr = scratch.path().to_str().unwrap().into();
677 tt.run_once(async {
678 track_glob_operation(path, rcstr!("sub/*.js"))
679 .read_strongly_consistent()
680 .await?;
681 anyhow::Ok(())
682 })
683 .await
684 .unwrap();
685 let path: RcStr = scratch.path().to_str().unwrap().into();
686 tt.run_once(async {
687 assert_dead_symlink_read_glob_operation(path)
688 .read_strongly_consistent()
689 .await?;
690 anyhow::Ok(())
691 })
692 .await
693 .unwrap();
694 }
695
696 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
698 async fn symlink_escapes_fs_root() {
699 let scratch = tempfile::tempdir().unwrap();
700 {
701 let path = scratch.path();
703 let sub = &path.join("sub");
704 create_dir(sub).unwrap();
705 let foo = scratch.path().join("foo.js");
706 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
707 symlink(foo, sub.join("escape.js")).unwrap();
709 }
710 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
711 BackendOptions::default(),
712 noop_backing_storage(),
713 ));
714 let root: RcStr = scratch.path().join("sub").to_str().unwrap().into();
715 tt.run_once(async {
716 track_glob_operation(root, rcstr!("*.js"))
717 .read_strongly_consistent()
718 .await?;
719 anyhow::Ok(())
720 })
721 .await
722 .unwrap();
723 }
724
725 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
726 async fn read_glob_symlinks_loop() {
727 let scratch = tempfile::tempdir().unwrap();
728 {
729 let path = scratch.path();
732 let sub = &path.join("sub");
733 create_dir(sub).unwrap();
734 let foo = sub.join("foo.js");
735 File::create_new(&foo).unwrap().write_all(b"foo").unwrap();
736 symlink(sub, sub.join("link")).unwrap();
738 }
739 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
740 BackendOptions::default(),
741 noop_backing_storage(),
742 ));
743 let path: RcStr = scratch.path().to_str().unwrap().into();
744 tt.run_once(async {
745 let err = read_glob_operation(path.clone(), rcstr!("**"))
746 .read_strongly_consistent()
747 .await
748 .expect_err("Should have detected an infinite loop");
749
750 assert_eq!(
751 "'sub/link' is a symlink causes that causes an infinite loop!",
752 format!("{}", err.root_cause())
753 );
754
755 let err = track_glob_operation(path, rcstr!("**"))
757 .read_strongly_consistent()
758 .await
759 .expect_err("Should have detected an infinite loop");
760
761 assert_eq!(
762 "'sub/link' is a symlink causes that causes an infinite loop!",
763 format!("{}", err.root_cause())
764 );
765
766 anyhow::Ok(())
767 })
768 .await
769 .unwrap();
770 }
771}