use crate::diagnostics::{CoreError, FileError, LintResult};
use std::collections::HashMap;
use std::fs::Metadata;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
#[derive(Debug, Clone)]
pub struct FileMetadata {
pub is_file: bool,
pub is_dir: bool,
pub is_symlink: bool,
pub len: u64,
}
impl FileMetadata {
pub fn file(len: u64) -> Self {
Self {
is_file: true,
is_dir: false,
is_symlink: false,
len,
}
}
pub fn directory() -> Self {
Self {
is_file: false,
is_dir: true,
is_symlink: false,
len: 0,
}
}
pub fn symlink() -> Self {
Self {
is_file: false,
is_dir: false,
is_symlink: true,
len: 0,
}
}
}
impl From<&Metadata> for FileMetadata {
fn from(meta: &Metadata) -> Self {
Self {
is_file: meta.is_file(),
is_dir: meta.is_dir(),
is_symlink: meta.file_type().is_symlink(),
len: meta.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub path: PathBuf,
pub metadata: FileMetadata,
}
pub trait FileSystem: Send + Sync + std::fmt::Debug {
fn exists(&self, path: &Path) -> bool;
fn is_file(&self, path: &Path) -> bool;
fn is_dir(&self, path: &Path) -> bool;
fn is_symlink(&self, path: &Path) -> bool;
fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
fn read_to_string(&self, path: &Path) -> LintResult<String>;
fn write(&self, path: &Path, content: &str) -> LintResult<()>;
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RealFileSystem;
impl FileSystem for RealFileSystem {
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn is_file(&self, path: &Path) -> bool {
path.is_file()
}
fn is_dir(&self, path: &Path) -> bool {
path.is_dir()
}
fn is_symlink(&self, path: &Path) -> bool {
path.is_symlink()
}
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
std::fs::metadata(path).map(|m| FileMetadata::from(&m))
}
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
std::fs::symlink_metadata(path).map(|m| FileMetadata::from(&m))
}
fn read_to_string(&self, path: &Path) -> LintResult<String> {
crate::file_utils::safe_read_file(path)
}
fn write(&self, path: &Path, content: &str) -> LintResult<()> {
crate::file_utils::safe_write_file(path, content)
}
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
std::fs::canonicalize(path)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
Ok(std::fs::read_dir(path)?
.filter_map(|entry_res| {
let entry = entry_res.ok()?;
let path = entry.path();
let metadata = std::fs::symlink_metadata(&path).ok()?;
Some(DirEntry {
path,
metadata: FileMetadata::from(&metadata),
})
})
.collect())
}
}
#[derive(Debug, Clone)]
enum MockEntry {
File { content: String },
Directory,
Symlink { target: PathBuf },
}
#[derive(Debug, Default)]
pub struct MockFileSystem {
entries: RwLock<HashMap<PathBuf, MockEntry>>,
}
impl MockFileSystem {
pub fn new() -> Self {
Self {
entries: RwLock::new(HashMap::new()),
}
}
pub fn add_file(&self, path: impl AsRef<Path>, content: impl Into<String>) {
let path = normalize_mock_path(path.as_ref());
let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
entries.insert(
path,
MockEntry::File {
content: content.into(),
},
);
}
pub fn add_dir(&self, path: impl AsRef<Path>) {
let path = normalize_mock_path(path.as_ref());
let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
entries.insert(path, MockEntry::Directory);
}
pub fn add_symlink(&self, path: impl AsRef<Path>, target: impl AsRef<Path>) {
let path = normalize_mock_path(path.as_ref());
let target = normalize_mock_path(target.as_ref());
let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
entries.insert(path, MockEntry::Symlink { target });
}
pub fn remove(&self, path: impl AsRef<Path>) {
let path = normalize_mock_path(path.as_ref());
let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
entries.remove(&path);
}
pub fn clear(&self) {
let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
entries.clear();
}
fn get_entry(&self, path: &Path) -> Option<MockEntry> {
let path = normalize_mock_path(path);
let entries = self.entries.read().expect("MockFileSystem lock poisoned");
entries.get(&path).cloned()
}
fn resolve_symlink(&self, path: &Path) -> Option<PathBuf> {
let path = normalize_mock_path(path);
let entries = self.entries.read().expect("MockFileSystem lock poisoned");
match entries.get(&path) {
Some(MockEntry::Symlink { target }) => Some(target.clone()),
_ => None,
}
}
pub const MAX_SYMLINK_DEPTH: u32 = 40;
fn metadata_with_depth(&self, path: &Path, depth: u32) -> io::Result<FileMetadata> {
if depth > Self::MAX_SYMLINK_DEPTH {
return Err(io::Error::other("too many levels of symbolic links"));
}
enum MetaResult {
Found(FileMetadata),
FollowSymlink(PathBuf),
}
let path = normalize_mock_path(path);
let result: io::Result<MetaResult> = {
let entries = self.entries.read().expect("MockFileSystem lock poisoned");
match entries.get(&path) {
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
)),
Some(MockEntry::File { content }) => {
Ok(MetaResult::Found(FileMetadata::file(content.len() as u64)))
}
Some(MockEntry::Directory) => Ok(MetaResult::Found(FileMetadata::directory())),
Some(MockEntry::Symlink { target }) => {
Ok(MetaResult::FollowSymlink(target.clone()))
}
}
};
match result? {
MetaResult::Found(meta) => Ok(meta),
MetaResult::FollowSymlink(target) => self.metadata_with_depth(&target, depth + 1),
}
}
fn canonicalize_with_depth(&self, path: &Path, depth: u32) -> io::Result<PathBuf> {
if depth > Self::MAX_SYMLINK_DEPTH {
return Err(io::Error::other("too many levels of symbolic links"));
}
let path_normalized = normalize_mock_path(path);
if !self.exists(&path_normalized) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
));
}
if let Some(target) = self.resolve_symlink(&path_normalized) {
self.canonicalize_with_depth(&target, depth + 1)
} else {
Ok(path_normalized)
}
}
}
fn normalize_mock_path(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
PathBuf::from(path_str.replace('\\', "/"))
}
impl FileSystem for MockFileSystem {
fn exists(&self, path: &Path) -> bool {
self.get_entry(path).is_some()
}
fn is_file(&self, path: &Path) -> bool {
matches!(self.get_entry(path), Some(MockEntry::File { .. }))
}
fn is_dir(&self, path: &Path) -> bool {
matches!(self.get_entry(path), Some(MockEntry::Directory))
}
fn is_symlink(&self, path: &Path) -> bool {
matches!(self.get_entry(path), Some(MockEntry::Symlink { .. }))
}
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
self.metadata_with_depth(path, 0)
}
fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
let entry = self.get_entry(path).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
)
})?;
match entry {
MockEntry::File { content } => Ok(FileMetadata::file(content.len() as u64)),
MockEntry::Directory => Ok(FileMetadata::directory()),
MockEntry::Symlink { .. } => Ok(FileMetadata::symlink()),
}
}
fn read_to_string(&self, path: &Path) -> LintResult<String> {
let path_normalized = normalize_mock_path(path);
let entries = self.entries.read().expect("MockFileSystem lock poisoned");
let entry = entries.get(&path_normalized).ok_or_else(|| {
CoreError::File(FileError::Read {
path: path.to_path_buf(),
source: io::Error::new(io::ErrorKind::NotFound, "file not found"),
})
})?;
match entry {
MockEntry::File { content } => Ok(content.clone()),
MockEntry::Directory => Err(CoreError::File(FileError::NotRegular {
path: path.to_path_buf(),
})),
MockEntry::Symlink { target } => {
let target_entry = entries.get(target).ok_or_else(|| {
CoreError::File(FileError::Read {
path: path.to_path_buf(),
source: io::Error::new(io::ErrorKind::NotFound, "symlink target not found"),
})
})?;
match target_entry {
MockEntry::File { content } => Ok(content.clone()),
_ => Err(CoreError::File(FileError::NotRegular {
path: path.to_path_buf(),
})),
}
}
}
}
fn write(&self, path: &Path, content: &str) -> LintResult<()> {
let path_normalized = normalize_mock_path(path);
let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
match entries.get(&path_normalized) {
Some(MockEntry::File { .. }) => {
entries.insert(
path_normalized,
MockEntry::File {
content: content.to_string(),
},
);
Ok(())
}
Some(MockEntry::Directory) => Err(CoreError::File(FileError::NotRegular {
path: path.to_path_buf(),
})),
Some(MockEntry::Symlink { .. }) => Err(CoreError::File(FileError::Symlink {
path: path.to_path_buf(),
})),
None => {
Err(CoreError::File(FileError::Write {
path: path.to_path_buf(),
source: io::Error::new(io::ErrorKind::NotFound, "file not found"),
}))
}
}
}
fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
self.canonicalize_with_depth(path, 0)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
let path_normalized = normalize_mock_path(path);
match self.get_entry(&path_normalized) {
Some(MockEntry::Directory) => {}
Some(_) => {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
"not a directory",
));
}
None => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"directory not found",
));
}
}
let entries = self.entries.read().expect("MockFileSystem lock poisoned");
let mut result = Vec::new();
let prefix = if path_normalized.to_string_lossy().ends_with('/') {
path_normalized.to_string_lossy().to_string()
} else {
format!("{}/", path_normalized.display())
};
for (entry_path, entry) in entries.iter() {
let entry_str = entry_path.to_string_lossy();
if let Some(rest) = entry_str.strip_prefix(&prefix) {
if !rest.contains('/') && !rest.is_empty() {
let metadata = match entry {
MockEntry::File { content } => FileMetadata::file(content.len() as u64),
MockEntry::Directory => FileMetadata::directory(),
MockEntry::Symlink { .. } => FileMetadata::symlink(),
};
result.push(DirEntry {
path: entry_path.clone(),
metadata,
});
}
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_real_fs_exists() {
let fs = RealFileSystem;
assert!(fs.exists(Path::new("Cargo.toml")));
assert!(!fs.exists(Path::new("nonexistent_file_xyz.txt")));
}
#[test]
fn test_real_fs_is_file() {
let fs = RealFileSystem;
assert!(fs.is_file(Path::new("Cargo.toml")));
assert!(!fs.is_file(Path::new("src")));
}
#[test]
fn test_real_fs_is_dir() {
let fs = RealFileSystem;
assert!(fs.is_dir(Path::new("src")));
assert!(!fs.is_dir(Path::new("Cargo.toml")));
}
#[test]
fn test_real_fs_read_to_string() {
let fs = RealFileSystem;
let content = fs.read_to_string(Path::new("Cargo.toml"));
assert!(content.is_ok());
assert!(content.unwrap().contains("[package]"));
}
#[test]
fn test_real_fs_read_nonexistent() {
let fs = RealFileSystem;
let result = fs.read_to_string(Path::new("nonexistent_file_xyz.txt"));
assert!(result.is_err());
}
#[test]
fn test_mock_fs_add_and_exists() {
let fs = MockFileSystem::new();
assert!(!fs.exists(Path::new("/test/file.txt")));
fs.add_file("/test/file.txt", "content");
assert!(fs.exists(Path::new("/test/file.txt")));
}
#[test]
fn test_mock_fs_is_file() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_dir("/test/dir");
assert!(fs.is_file(Path::new("/test/file.txt")));
assert!(!fs.is_file(Path::new("/test/dir")));
}
#[test]
fn test_mock_fs_is_dir() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_dir("/test/dir");
assert!(!fs.is_dir(Path::new("/test/file.txt")));
assert!(fs.is_dir(Path::new("/test/dir")));
}
#[test]
fn test_mock_fs_is_symlink() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_symlink("/test/link.txt", "/test/file.txt");
assert!(!fs.is_symlink(Path::new("/test/file.txt")));
assert!(fs.is_symlink(Path::new("/test/link.txt")));
}
#[test]
fn test_mock_fs_read_to_string() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "hello world");
let content = fs.read_to_string(Path::new("/test/file.txt"));
assert!(content.is_ok());
assert_eq!(content.unwrap(), "hello world");
}
#[test]
fn test_mock_fs_read_nonexistent() {
let fs = MockFileSystem::new();
let result = fs.read_to_string(Path::new("/test/file.txt"));
assert!(result.is_err());
}
#[test]
fn test_mock_fs_read_directory_fails() {
let fs = MockFileSystem::new();
fs.add_dir("/test/dir");
let result = fs.read_to_string(Path::new("/test/dir"));
assert!(matches!(
result,
Err(CoreError::File(FileError::NotRegular { .. }))
));
}
#[test]
fn test_mock_fs_read_symlink_follows_target() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_symlink("/test/link.txt", "/test/file.txt");
let result = fs.read_to_string(Path::new("/test/link.txt"));
assert!(result.is_ok());
assert_eq!(result.unwrap(), "content");
}
#[test]
fn test_mock_fs_write() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "original");
let result = fs.write(Path::new("/test/file.txt"), "updated");
assert!(result.is_ok());
let content = fs.read_to_string(Path::new("/test/file.txt")).unwrap();
assert_eq!(content, "updated");
}
#[test]
fn test_mock_fs_write_nonexistent_fails() {
let fs = MockFileSystem::new();
let result = fs.write(Path::new("/test/file.txt"), "content");
assert!(matches!(
result,
Err(CoreError::File(FileError::Write { .. }))
));
}
#[test]
fn test_mock_fs_metadata_file() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "12345");
let meta = fs.metadata(Path::new("/test/file.txt")).unwrap();
assert!(meta.is_file);
assert!(!meta.is_dir);
assert!(!meta.is_symlink);
assert_eq!(meta.len, 5);
}
#[test]
fn test_mock_fs_metadata_directory() {
let fs = MockFileSystem::new();
fs.add_dir("/test/dir");
let meta = fs.metadata(Path::new("/test/dir")).unwrap();
assert!(!meta.is_file);
assert!(meta.is_dir);
assert!(!meta.is_symlink);
}
#[test]
fn test_mock_fs_symlink_metadata() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_symlink("/test/link.txt", "/test/file.txt");
let meta = fs.symlink_metadata(Path::new("/test/link.txt")).unwrap();
assert!(meta.is_symlink);
let meta = fs.metadata(Path::new("/test/link.txt")).unwrap();
assert!(meta.is_file);
assert!(!meta.is_symlink);
}
#[test]
fn test_mock_fs_read_dir() {
let fs = MockFileSystem::new();
fs.add_dir("/test");
fs.add_file("/test/file1.txt", "content1");
fs.add_file("/test/file2.txt", "content2");
fs.add_dir("/test/subdir");
let entries = fs.read_dir(Path::new("/test")).unwrap();
assert_eq!(entries.len(), 3);
let names: Vec<_> = entries
.iter()
.map(|e| e.path.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(names.contains(&"file1.txt".to_string()));
assert!(names.contains(&"file2.txt".to_string()));
assert!(names.contains(&"subdir".to_string()));
}
#[test]
fn test_mock_fs_read_dir_nonexistent() {
let fs = MockFileSystem::new();
let result = fs.read_dir(Path::new("/nonexistent"));
assert!(result.is_err());
}
#[test]
fn test_mock_fs_read_dir_not_directory() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
let result = fs.read_dir(Path::new("/test/file.txt"));
assert!(result.is_err());
}
#[test]
fn test_mock_fs_canonicalize() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
let canonical = fs.canonicalize(Path::new("/test/file.txt")).unwrap();
assert_eq!(canonical, PathBuf::from("/test/file.txt"));
}
#[test]
fn test_mock_fs_canonicalize_follows_symlink() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_symlink("/test/link.txt", "/test/file.txt");
let canonical = fs.canonicalize(Path::new("/test/link.txt")).unwrap();
assert_eq!(canonical, PathBuf::from("/test/file.txt"));
}
#[test]
fn test_mock_fs_clear() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
assert!(fs.exists(Path::new("/test/file.txt")));
fs.clear();
assert!(!fs.exists(Path::new("/test/file.txt")));
}
#[test]
fn test_mock_fs_remove() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
assert!(fs.exists(Path::new("/test/file.txt")));
fs.remove("/test/file.txt");
assert!(!fs.exists(Path::new("/test/file.txt")));
}
#[test]
fn test_mock_fs_windows_path_normalization() {
let fs = MockFileSystem::new();
fs.add_file("C:/test/file.txt", "content");
assert!(fs.exists(Path::new("C:/test/file.txt")));
assert!(fs.exists(Path::new("C:\\test\\file.txt")));
}
#[test]
fn test_mock_fs_thread_safety() {
use std::sync::Arc;
use std::thread;
let fs = Arc::new(MockFileSystem::new());
let mut handles = vec![];
for i in 0..10 {
let fs_clone = Arc::clone(&fs);
let handle = thread::spawn(move || {
let path = format!("/test/file{}.txt", i);
fs_clone.add_file(&path, format!("content{}", i));
assert!(fs_clone.exists(Path::new(&path)));
let content = fs_clone.read_to_string(Path::new(&path)).unwrap();
assert_eq!(content, format!("content{}", i));
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
for i in 0..10 {
let path = format!("/test/file{}.txt", i);
assert!(fs.exists(Path::new(&path)));
}
}
#[test]
fn test_mock_fs_circular_symlink_metadata() {
let fs = MockFileSystem::new();
fs.add_symlink("/test/a", "/test/b");
fs.add_symlink("/test/b", "/test/a");
let result = fs.metadata(Path::new("/test/a"));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("too many levels of symbolic links")
);
}
#[test]
fn test_mock_fs_circular_symlink_canonicalize() {
let fs = MockFileSystem::new();
fs.add_symlink("/test/a", "/test/b");
fs.add_symlink("/test/b", "/test/a");
let result = fs.canonicalize(Path::new("/test/a"));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("too many levels of symbolic links")
);
}
#[test]
fn test_mock_fs_chained_symlinks() {
let fs = MockFileSystem::new();
fs.add_file("/test/file.txt", "content");
fs.add_symlink("/test/link3", "/test/file.txt");
fs.add_symlink("/test/link2", "/test/link3");
fs.add_symlink("/test/link1", "/test/link2");
let meta = fs.metadata(Path::new("/test/link1")).unwrap();
assert!(meta.is_file);
assert_eq!(meta.len, 7);
let canonical = fs.canonicalize(Path::new("/test/link1")).unwrap();
assert_eq!(canonical, PathBuf::from("/test/file.txt"));
}
#[test]
fn test_mock_fs_max_symlink_depth_boundary() {
let fs = MockFileSystem::new();
fs.add_file("/test/target.txt", "content");
let mut prev = PathBuf::from("/test/target.txt");
for i in 0..MockFileSystem::MAX_SYMLINK_DEPTH {
let link = PathBuf::from(format!("/test/link{}", i));
fs.add_symlink(&link, &prev);
prev = link;
}
let result = fs.metadata(&prev);
assert!(result.is_ok(), "Should handle MAX_SYMLINK_DEPTH links");
}
#[test]
fn test_mock_fs_exceeds_max_symlink_depth() {
let fs = MockFileSystem::new();
fs.add_file("/test/target.txt", "content");
let mut prev = PathBuf::from("/test/target.txt");
for i in 0..=MockFileSystem::MAX_SYMLINK_DEPTH {
let link = PathBuf::from(format!("/test/link{}", i));
fs.add_symlink(&link, &prev);
prev = link;
}
let result = fs.metadata(&prev);
assert!(
result.is_err(),
"Should fail when exceeding MAX_SYMLINK_DEPTH"
);
assert!(
result
.unwrap_err()
.to_string()
.contains("too many levels of symbolic links")
);
}
#[cfg(unix)]
mod unix_tests {
use super::*;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
#[test]
fn test_real_fs_follows_symlink_read() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("target.txt");
let link = temp.path().join("link.txt");
std::fs::write(&target, "content").unwrap();
symlink(&target, &link).unwrap();
let fs = RealFileSystem;
let result = fs.read_to_string(&link);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "content");
}
#[test]
fn test_real_fs_symlink_metadata() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("target.txt");
let link = temp.path().join("link.txt");
std::fs::write(&target, "content").unwrap();
symlink(&target, &link).unwrap();
let fs = RealFileSystem;
let meta = fs.symlink_metadata(&link).unwrap();
assert!(meta.is_symlink);
let meta = fs.metadata(&link).unwrap();
assert!(meta.is_file);
assert!(!meta.is_symlink);
}
#[test]
fn test_real_fs_dangling_symlink() {
let temp = TempDir::new().unwrap();
let link = temp.path().join("dangling.txt");
symlink("/nonexistent/target", &link).unwrap();
let fs = RealFileSystem;
let result = fs.read_to_string(&link);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CoreError::File(FileError::Read { .. })
));
}
#[test]
fn test_real_fs_is_symlink() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("target.txt");
let link = temp.path().join("link.txt");
std::fs::write(&target, "content").unwrap();
symlink(&target, &link).unwrap();
let fs = RealFileSystem;
assert!(!fs.is_symlink(&target));
assert!(fs.is_symlink(&link));
}
#[test]
fn test_real_fs_read_dir_skips_symlinks_in_metadata() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("file.txt"), "content").unwrap();
symlink(temp.path().join("file.txt"), temp.path().join("link.txt")).unwrap();
let fs = RealFileSystem;
let entries = fs.read_dir(temp.path()).unwrap();
assert_eq!(entries.len(), 2);
let symlink_entry = entries
.iter()
.find(|e| e.path.file_name().unwrap().to_str().unwrap() == "link.txt");
assert!(symlink_entry.is_some());
assert!(symlink_entry.unwrap().metadata.is_symlink);
let file_entry = entries
.iter()
.find(|e| e.path.file_name().unwrap().to_str().unwrap() == "file.txt");
assert!(file_entry.is_some());
assert!(file_entry.unwrap().metadata.is_file);
}
}
}