#![cfg(all(feature = "fs", target_os = "linux"))]
use std::path::PathBuf;
use xpct::{be_ok, be_some, consist_of, eq_diff, equal, expect, match_pattern, pattern};
use crate::{
Connection, Error, FileOrigin,
file_metadata::{FileKind, Owner},
settings::Settings,
walk::{WalkOptions, WalkPredicate, WalkVisit},
};
fn open() -> crate::Result<Connection> {
Connection::open_for_testing(&Settings::default())
}
fn format_events(events: &[String]) -> String {
events.join("\n")
}
#[test]
fn visits_only_base_when_empty() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff("enter:/dir\nleave:/dir"));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn visits_files_and_subdirs_in_alphabetical_order() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/b.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/sub", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/sub/c.txt", FileKind::Regular, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff(
"enter:/dir\n\
file:/dir/a.txt\n\
file:/dir/b.txt\n\
enter:/dir/sub\n\
file:/dir/sub/c.txt\n\
leave:/dir/sub\n\
leave:/dir",
));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn leave_dir_pairs_with_enter_dir() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/a", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/c", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b/d", FileKind::Dir, Owner::ROOT)?;
let mut enter: Vec<PathBuf> = Vec::new();
let mut leave: Vec<PathBuf> = Vec::new();
fs.walk(
"/a",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => enter.push(e.path().to_path_buf()),
WalkVisit::LeaveDir(p) => leave.push(p.to_path_buf()),
WalkVisit::File(_) => {}
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(&enter).to(consist_of(&leave));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn walks_root_directory() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/a.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/b.txt", FileKind::Regular, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff("enter:/\nfile:/a.txt\nfile:/b.txt\nleave:/"));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn errors_when_path_does_not_exist() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
let result = fs.walk(
"/nonexistent",
&WalkOptions::new(),
|_| -> crate::Result<WalkPredicate<()>> { Ok(WalkPredicate::Continue) },
);
expect!(result).to(match_pattern(pattern!(Err(Error::FileNotFound { .. }))));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn errors_when_path_is_not_a_directory() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/file.txt", FileKind::Regular, Owner::ROOT)?;
let result = fs.walk(
"/file.txt",
&WalkOptions::new(),
|_| -> crate::Result<WalkPredicate<()>> { Ok(WalkPredicate::Continue) },
);
expect!(result).to(match_pattern(pattern!(Err(Error::NotADirectory {
file: FileOrigin::Litebox { .. },
}))));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn stop_returns_wrapped_value_and_short_circuits() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/b.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/c.txt", FileKind::Regular, Owner::ROOT)?;
let mut visits = 0usize;
let result = fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<u32>> {
visits += 1;
if matches!(visit, WalkVisit::File(_)) {
Ok(WalkPredicate::Stop(42))
} else {
Ok(WalkPredicate::Continue)
}
},
)?;
expect!(result).to(be_some()).to(equal(42));
expect!(visits).to(equal(2));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn skip_descendants_skips_children_but_still_emits_leave_dir() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/sub", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/sub/inner.txt", FileKind::Regular, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => {
events.push(format!("enter:{}", e.path().display()));
if e.path() == std::path::Path::new("/dir") {
return Ok(WalkPredicate::SkipDescendants);
}
}
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff("enter:/dir\nleave:/dir"));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn skip_siblings_on_enter_dir_keeps_descendants() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a/inner.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/b", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/b/inner.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/c", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/c/inner.txt", FileKind::Regular, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => {
events.push(format!("enter:{}", e.path().display()));
if e.path() == std::path::Path::new("/dir/a") {
return Ok(WalkPredicate::SkipSiblings);
}
}
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff(
"enter:/dir\n\
enter:/dir/a\n\
file:/dir/a/inner.txt\n\
leave:/dir/a\n\
leave:/dir",
));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn skip_siblings_on_file_drops_pending_sibling_dirs() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a/inside.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/b.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/c", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/c/inside.txt", FileKind::Regular, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => {
events.push(format!("file:{}", e.path().display()));
return Ok(WalkPredicate::SkipSiblings);
}
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff("enter:/dir\nfile:/dir/b.txt\nleave:/dir"));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn max_depth_zero_visits_only_base() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/sub", FileKind::Dir, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new().max_depth(Some(0)),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff("enter:/dir\nleave:/dir"));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn max_depth_one_visits_immediate_children_only() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir/sub", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/sub/deep.txt", FileKind::Regular, Owner::ROOT)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new().max_depth(Some(1)),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff(
"enter:/dir\n\
file:/dir/a.txt\n\
enter:/dir/sub\n\
leave:/dir/sub\n\
leave:/dir",
));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn max_depth_unlimited_walks_full_tree() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/a", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b/c", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b/c/d", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b/c/d/leaf.txt", FileKind::Regular, Owner::ROOT)?;
let mut deepest_seen = 0usize;
fs.walk(
"/a",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
if let WalkVisit::File(e) = visit {
deepest_seen = e.path().components().count();
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(deepest_seen).to(equal(6));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn entry_reports_path_kind_and_file_id() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
let file = fs.create("/dir/a.txt", FileKind::Regular, Owner::ROOT)?;
let file_id = file.file_id();
drop(file);
let mut seen: Vec<(String, FileKind)> = Vec::new();
let mut seen_file_id = None;
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
if let WalkVisit::File(e) = visit {
seen.push((e.path().display().to_string(), e.kind().clone()));
seen_file_id = Some(e.file_id());
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(seen).to(consist_of([("/dir/a.txt".to_string(), FileKind::Regular)]));
expect!(seen_file_id).to(be_some()).to(equal(file_id));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_visited_as_file_when_not_following() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/target", FileKind::Dir, Owner::ROOT)?;
fs.create("/target/inside.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create(
"/dir/sym",
FileKind::Symlink {
target: PathBuf::from("/target"),
},
Owner::ROOT,
)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new(),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff("enter:/dir\nfile:/dir/sym\nleave:/dir"));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_to_dir_followed_when_enabled() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/target", FileKind::Dir, Owner::ROOT)?;
fs.create("/target/inside.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create(
"/dir/sym",
FileKind::Symlink {
target: PathBuf::from("/target"),
},
Owner::ROOT,
)?;
let mut events: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new().follow_symlinks(true),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::EnterDir(e) => events.push(format!("enter:{}", e.path().display())),
WalkVisit::LeaveDir(p) => events.push(format!("leave:{}", p.display())),
WalkVisit::File(e) => events.push(format!("file:{}", e.path().display())),
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(format_events(&events)).to(eq_diff(
"enter:/dir\n\
enter:/dir/sym\n\
file:/dir/sym/inside.txt\n\
leave:/dir/sym\n\
leave:/dir",
));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn followed_symlink_entry_reports_symlink_identity() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/target", FileKind::Dir, Owner::ROOT)?;
let symlink = fs.create(
"/sym",
FileKind::Symlink {
target: PathBuf::from("/target"),
},
Owner::ROOT,
)?;
let symlink_id = symlink.file_id();
drop(symlink);
let mut seen_kind: Option<FileKind> = None;
let mut seen_id = None;
fs.walk(
"/",
&WalkOptions::new().follow_symlinks(true),
|visit| -> crate::Result<WalkPredicate<()>> {
if let WalkVisit::EnterDir(e) = visit
&& e.path() == std::path::Path::new("/sym")
{
seen_kind = Some(e.kind().clone());
seen_id = Some(e.file_id());
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(seen_kind)
.to(be_some())
.to(equal(FileKind::Symlink {
target: PathBuf::from("/target"),
}));
expect!(seen_id).to(be_some()).to(equal(symlink_id));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_to_file_falls_through_to_file_visit_when_following() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/target.txt", FileKind::Regular, Owner::ROOT)?;
fs.create(
"/sym",
FileKind::Symlink {
target: PathBuf::from("/target.txt"),
},
Owner::ROOT,
)?;
let mut sym_kinds: Vec<String> = Vec::new();
fs.walk(
"/",
&WalkOptions::new().follow_symlinks(true),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::File(e) if e.path() == std::path::Path::new("/sym") => {
sym_kinds.push("file".to_string());
}
WalkVisit::EnterDir(e) if e.path() == std::path::Path::new("/sym") => {
sym_kinds.push("enter".to_string());
}
_ => {}
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(sym_kinds).to(consist_of(["file".to_string()]));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn broken_symlink_falls_through_to_file_visit_when_following() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create(
"/sym",
FileKind::Symlink {
target: PathBuf::from("/does/not/exist"),
},
Owner::ROOT,
)?;
let mut sym_seen_as = String::new();
fs.walk(
"/",
&WalkOptions::new().follow_symlinks(true),
|visit| -> crate::Result<WalkPredicate<()>> {
match visit {
WalkVisit::File(e) if e.path() == std::path::Path::new("/sym") => {
sym_seen_as = "file".to_string();
}
WalkVisit::EnterDir(e) if e.path() == std::path::Path::new("/sym") => {
sym_seen_as = "enter".to_string();
}
_ => {}
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(sym_seen_as).to(equal("file".to_string()));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn relative_symlink_resolves_against_parent_directory() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/inner", FileKind::Dir, Owner::ROOT)?;
fs.create("/inner/wrong.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/inner", FileKind::Dir, Owner::ROOT)?;
fs.create("/dir/inner/right.txt", FileKind::Regular, Owner::ROOT)?;
fs.create(
"/dir/sym",
FileKind::Symlink {
target: PathBuf::from("inner"),
},
Owner::ROOT,
)?;
let mut sym_descendants: Vec<String> = Vec::new();
fs.walk(
"/dir",
&WalkOptions::new().follow_symlinks(true),
|visit| -> crate::Result<WalkPredicate<()>> {
if let WalkVisit::File(e) = visit
&& e.path().starts_with("/dir/sym")
{
sym_descendants.push(e.path().display().to_string());
}
Ok(WalkPredicate::Continue)
},
)?;
expect!(sym_descendants).to(consist_of(["/dir/sym/right.txt".to_string()]));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_targeting_ancestor_errors_with_symlink_loop() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/a", FileKind::Dir, Owner::ROOT)?;
fs.create(
"/a/back",
FileKind::Symlink {
target: PathBuf::from("/a"),
},
Owner::ROOT,
)?;
let result = fs.walk(
"/a",
&WalkOptions::new().follow_symlinks(true),
|_| -> crate::Result<WalkPredicate<()>> { Ok(WalkPredicate::Continue) },
);
expect!(result).to(match_pattern(pattern!(Err(Error::SymlinkLoop {
path: FileOrigin::Litebox { .. },
}))));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_targeting_distant_ancestor_errors_with_symlink_loop() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/a", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b", FileKind::Dir, Owner::ROOT)?;
fs.create("/a/b/c", FileKind::Dir, Owner::ROOT)?;
fs.create(
"/a/b/c/back_to_a",
FileKind::Symlink {
target: PathBuf::from("/a"),
},
Owner::ROOT,
)?;
let result = fs.walk(
"/a",
&WalkOptions::new().follow_symlinks(true),
|_| -> crate::Result<WalkPredicate<()>> { Ok(WalkPredicate::Continue) },
);
expect!(result).to(match_pattern(pattern!(Err(Error::SymlinkLoop { .. }))));
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn symlink_to_self_visited_as_file_not_loop() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create(
"/sym",
FileKind::Symlink {
target: PathBuf::from("/sym"),
},
Owner::ROOT,
)?;
let result = fs.walk(
"/",
&WalkOptions::new().follow_symlinks(true),
|_| -> crate::Result<WalkPredicate<()>> { Ok(WalkPredicate::Continue) },
);
expect!(result).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}
#[test]
fn sibling_symlinks_to_same_dir_do_not_falsely_trigger_loop() -> crate::Result<()> {
let mut conn = open()?;
conn.exec(|fs| {
fs.create("/target", FileKind::Dir, Owner::ROOT)?;
fs.create("/target/leaf.txt", FileKind::Regular, Owner::ROOT)?;
fs.create("/dir", FileKind::Dir, Owner::ROOT)?;
fs.create(
"/dir/sym1",
FileKind::Symlink {
target: PathBuf::from("/target"),
},
Owner::ROOT,
)?;
fs.create(
"/dir/sym2",
FileKind::Symlink {
target: PathBuf::from("/target"),
},
Owner::ROOT,
)?;
let result = fs.walk(
"/dir",
&WalkOptions::new().follow_symlinks(true),
|_| -> crate::Result<WalkPredicate<()>> { Ok(WalkPredicate::Continue) },
);
expect!(result).to(be_ok());
crate::Result::Ok(())
})?;
Ok(())
}