use anyhow::Result;
use normalize_path::NormalizePath;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
use yek::config::YekConfig;
use yek::parallel::process_files_parallel;
#[cfg(unix)]
fn make_unreadable(path: &Path) -> std::io::Result<()> {
let mut permissions = fs::metadata(path)?.permissions();
permissions.set_mode(0o000);
fs::set_permissions(path, permissions)
}
#[cfg(not(unix))]
fn make_unreadable(_path: &Path) -> std::io::Result<()> {
Ok(())
}
#[cfg(unix)]
fn make_readable(path: &Path) -> std::io::Result<()> {
let mut permissions = fs::metadata(path)?.permissions();
permissions.set_mode(0o644);
fs::set_permissions(path, permissions)
}
#[cfg(not(unix))]
fn make_readable(_path: &Path) -> std::io::Result<()> {
Ok(())
}
#[test]
fn test_normalize_path_unix_style() {
let input = Path::new("/usr/local/bin");
let base = Path::new("/"); let expected = "usr/local/bin".to_string();
assert_eq!(
input
.strip_prefix(base)
.unwrap()
.normalize()
.to_string_lossy()
.to_string(),
expected
);
}
#[test]
fn test_normalize_path_windows_style() {
let input = Path::new("C:\\Program Files\\Yek");
let base = Path::new("C:\\"); let expected = if cfg!(windows) {
"Program Files\\Yek".to_string()
} else {
"C:/Program Files/Yek".to_string()
};
let stripped_path = input.strip_prefix(base).unwrap_or(input);
let normalized = stripped_path
.normalize()
.to_string_lossy()
.to_string()
.replace("\\", "/");
let expected_normalized = expected.replace("\\", "/");
assert_eq!(normalized, expected_normalized);
}
#[test]
fn test_process_files_parallel_empty() {
let temp_dir = tempdir().expect("failed to create temp dir");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 0);
}
#[test]
fn test_process_files_parallel_with_files() {
let temp_dir = tempdir().expect("failed to create temp dir");
let file_names = vec!["a.txt", "b.txt", "c.txt"];
for &file in &file_names {
let file_path = temp_dir.path().join(file);
fs::write(file_path, "dummy content").expect("failed to write dummy file");
}
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let base = temp_dir.path();
let result =
process_files_parallel(base, &config, &boosts).expect("process_files_parallel failed");
assert_eq!(result.len(), file_names.len());
let names: Vec<&str> = result.iter().map(|pf| pf.rel_path.as_str()).collect();
for file in file_names {
assert!(names.contains(&file), "Missing file: {}", file);
}
}
#[test]
fn test_process_files_parallel_file_read_error() {
let temp_dir = tempdir().expect("failed to create temp dir");
let file_path = temp_dir.path().join("unreadable.txt");
fs::write(&file_path, "content").expect("failed to write file");
if cfg!(unix) {
make_unreadable(&file_path).unwrap();
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 0);
make_readable(&file_path).unwrap();
} else {
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 1);
}
}
#[test]
fn test_process_files_parallel_walk_error() {
let temp_dir = tempdir().expect("failed to create temp dir");
let subdir = temp_dir.path().join("subdir");
fs::create_dir(&subdir).expect("failed to create subdir");
if cfg!(unix) {
make_unreadable(&subdir).unwrap();
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts);
assert!(result.is_ok()); let processed_files = result.unwrap();
assert_eq!(processed_files.len(), 0);
make_readable(&subdir).unwrap();
} else {
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts);
assert!(result.is_ok());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_pattern_single_file() -> Result<()> {
let temp_dir = tempdir()?;
let file_path = temp_dir.path().join("test.txt");
let mut file = File::create(&file_path)?;
writeln!(file, "Test content")?;
let glob_pattern = temp_dir.path().join("*.txt").to_string_lossy().to_string();
let config = YekConfig::default();
let boost_map = HashMap::new();
let result = process_files_parallel(&PathBuf::from(&glob_pattern), &config, &boost_map)?;
assert_eq!(result.len(), 1);
assert_eq!(result[0].rel_path, file_path.to_string_lossy().to_string());
Ok(())
}
#[test]
fn test_glob_pattern_multiple_files() -> Result<()> {
let temp_dir = tempdir()?;
let files = vec!["test1.txt", "test2.txt", "other.md"];
for fname in &files {
let file_path = temp_dir.path().join(fname);
let mut file = File::create(&file_path)?;
writeln!(file, "Test content for {}", fname)?;
}
let glob_pattern = temp_dir.path().join("*.txt").to_string_lossy().to_string();
let config = YekConfig::default();
let boost_map = HashMap::new();
let result = process_files_parallel(&PathBuf::from(&glob_pattern), &config, &boost_map)?;
assert_eq!(result.len(), 2);
let paths: Vec<String> = result.iter().map(|f| f.rel_path.clone()).collect();
let test1_path = temp_dir
.path()
.join("test1.txt")
.to_string_lossy()
.to_string();
let test2_path = temp_dir
.path()
.join("test2.txt")
.to_string_lossy()
.to_string();
assert!(paths.contains(&test1_path));
assert!(paths.contains(&test2_path));
Ok(())
}
#[test]
fn test_glob_pattern_nested_directories() -> Result<()> {
let temp_dir = tempdir()?;
let nested_dir = temp_dir.path().join("nested");
fs::create_dir(&nested_dir)?;
let root_file = temp_dir.path().join("root.rs"); let nested_file = nested_dir.join("nested.rs");
let other_file = temp_dir.path().join("other.md");
for (path, content) in [
(&root_file, "Root content"),
(&nested_file, "Nested content"),
(&other_file, "Other content"),
] {
let mut file = File::create(path)?;
writeln!(file, "{}", content)?;
}
let glob_pattern = temp_dir
.path()
.join("**/*.rs") .to_string_lossy()
.to_string();
let config = YekConfig::default();
let boost_map = HashMap::new();
let result = process_files_parallel(&PathBuf::from(&glob_pattern), &config, &boost_map)?;
assert_eq!(result.len(), 2);
let paths: Vec<String> = result.iter().map(|f| f.rel_path.clone()).collect();
let root_path = root_file.to_string_lossy().to_string();
let nested_path = nested_file.to_string_lossy().to_string();
assert!(paths.contains(&root_path));
assert!(paths.contains(&nested_path));
Ok(())
}
#[test]
fn test_glob_pattern_no_matches() -> Result<()> {
let temp_dir = tempdir()?;
let glob_pattern = temp_dir.path().join("*.txt").to_string_lossy().to_string();
let config = YekConfig::default();
let boost_map = HashMap::new();
let result = process_files_parallel(&PathBuf::from(&glob_pattern), &config, &boost_map)?;
assert!(result.is_empty());
Ok(())
}
#[test]
fn test_process_files_parallel_single_file() {
let temp_dir = tempdir().expect("failed to create temp dir");
let file_path = temp_dir.path().join("single.txt");
fs::write(&file_path, "single file content").expect("failed to write file");
let config = YekConfig::extend_config_with_defaults(
vec![file_path.to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(&file_path, &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 1);
assert_eq!(result[0].rel_path, "single.txt");
assert_eq!(result[0].content, "single file content");
}
#[test]
fn test_process_files_parallel_glob_pattern() {
let temp_dir = tempdir().expect("failed to create temp dir");
fs::write(temp_dir.path().join("file1.rs"), "content1").expect("failed to write file1");
fs::write(temp_dir.path().join("file2.rs"), "content2").expect("failed to write file2");
fs::write(temp_dir.path().join("file.md"), "markdown").expect("failed to write md file");
let glob_pattern = temp_dir.path().join("*.rs").to_string_lossy().to_string();
let config = YekConfig::default();
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(Path::new(&glob_pattern), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 2);
let rel_paths: Vec<&str> = result.iter().map(|f| f.rel_path.as_str()).collect();
assert!(rel_paths.iter().any(|&p| p.ends_with("file1.rs")));
assert!(rel_paths.iter().any(|&p| p.ends_with("file2.rs")));
}
#[test]
fn test_process_files_parallel_with_gitignore() {
let temp_dir = tempdir().expect("failed to create temp dir");
fs::write(temp_dir.path().join("included.txt"), "included")
.expect("failed to write included");
fs::write(temp_dir.path().join("ignored.txt"), "ignored").expect("failed to write ignored");
fs::write(temp_dir.path().join(".gitignore"), "*.txt\n")
.expect("failed to write gitignore");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 0);
}
#[test]
fn test_process_files_parallel_binary_file() {
let temp_dir = tempdir().expect("failed to create temp dir");
let binary_path = temp_dir.path().join("binary.bin");
fs::write(&binary_path, [0u8, 1, 2, 3]).expect("failed to write binary file");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 0);
}
#[test]
fn test_normalize_path_edge_cases() {
use yek::parallel::normalize_path;
let base = Path::new("/base");
let path = Path::new("/base/sub/file.txt");
assert_eq!(normalize_path(path, base), "sub/file.txt");
let path = Path::new("/other/file.txt");
assert_eq!(normalize_path(path, base), "/other/file.txt");
let path = Path::new("");
assert_eq!(normalize_path(path, base), "");
}
}
#[test]
fn test_process_files_parallel_with_gitignore_parse_error() {
let temp_dir = tempdir().expect("failed to create temp dir");
fs::write(temp_dir.path().join(".gitignore"), "[[invalid pattern")
.expect("failed to write gitignore");
fs::write(temp_dir.path().join("test.txt"), "content").expect("failed to write test file");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts);
assert!(result.is_ok());
let files = result.unwrap();
assert!(!files.is_empty());
}
#[test]
fn test_process_files_parallel_with_large_binary_file() {
let temp_dir = tempdir().expect("failed to create temp dir");
let large_binary = vec![0u8; 1024 * 1024 + 1];
fs::write(temp_dir.path().join("large.bin"), large_binary)
.expect("failed to write large binary");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 0);
}
#[test]
fn test_process_files_parallel_with_utf8_bom() {
let temp_dir = tempdir().expect("failed to create temp dir");
let mut content = vec![0xEF, 0xBB, 0xBF]; content.extend_from_slice(b"Hello World");
fs::write(temp_dir.path().join("bom.txt"), content).expect("failed to write BOM file");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 1);
assert!(result[0].content.starts_with('\u{FEFF}'));
}
#[test]
fn test_process_files_parallel_with_mixed_encoding() {
let temp_dir = tempdir().expect("failed to create temp dir");
let invalid_utf8 = vec![
0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFF, 0xFE, 0x57, 0x6F, 0x72, 0x6C, 0x64,
];
fs::write(temp_dir.path().join("mixed.txt"), invalid_utf8)
.expect("failed to write mixed encoding file");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts)
.expect("process_files_parallel failed");
assert_eq!(result.len(), 1);
assert!(result[0].content.contains("Hello"));
assert!(result[0].content.contains("World"));
}
#[test]
fn test_process_files_parallel_with_symlink_loop() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let temp_dir = tempdir().expect("failed to create temp dir");
let dir1 = temp_dir.path().join("dir1");
let dir2 = temp_dir.path().join("dir2");
fs::create_dir(&dir1).expect("failed to create dir1");
fs::create_dir(&dir2).expect("failed to create dir2");
symlink(&dir2, dir1.join("link_to_dir2")).expect("failed to create symlink");
symlink(&dir1, dir2.join("link_to_dir1")).expect("failed to create symlink");
fs::write(dir1.join("file.txt"), "content").expect("failed to write file");
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts);
assert!(result.is_ok());
let files = result.unwrap();
assert_eq!(
files
.iter()
.filter(|f| f.rel_path.ends_with("file.txt"))
.count(),
1
);
}
}
#[test]
fn test_process_files_parallel_with_channel_error_simulation() {
let temp_dir = tempdir().expect("failed to create temp dir");
for i in 0..100 {
fs::write(
temp_dir.path().join(format!("file{}.txt", i)),
format!("content{}", i),
)
.expect("failed to write file");
}
let config = YekConfig::extend_config_with_defaults(
vec![temp_dir.path().to_string_lossy().to_string()],
".".to_string(),
);
let boosts: HashMap<String, i32> = HashMap::new();
let result = process_files_parallel(temp_dir.path(), &config, &boosts);
assert!(result.is_ok());
let files = result.unwrap();
assert_eq!(files.len(), 100);
}