use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::sync::Mutex;
use tempfile::{tempdir, TempDir};
use yek::defaults::{BINARY_FILE_EXTENSIONS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTPUT_TEMPLATE};
use yek::config::YekConfig;
use yek::is_text_file;
use yek::priority::PriorityRule;
static CONFIG_TEST_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_validate_config_valid() {
let mut config =
YekConfig::extend_config_with_defaults(vec![".".to_string()], "output".to_string());
config.ignore_patterns = vec!["*.log".to_string()];
config.priority_rules = vec![PriorityRule {
pattern: ".*".to_string(),
score: 10,
}];
config.binary_extensions = vec!["bin".to_string()];
let result = config.validate();
assert!(result.is_ok(), "Expected no validation errors");
}
#[test]
fn test_validate_config_invalid_max_size() {
let mut config =
YekConfig::extend_config_with_defaults(vec![".".to_string()], "output".to_string());
config.max_size = "0".to_string();
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("max_size"));
}
#[test]
fn test_validate_config_invalid_priority_rule_score() {
let mut config = YekConfig::extend_config_with_defaults(vec![], "/tmp/yek".to_string());
config.priority_rules = vec![PriorityRule {
pattern: "foo".to_string(),
score: 1001,
}];
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("priority_rules"));
assert!(err.contains("Priority score 1001 must be between 0 and 1000"));
}
#[test]
fn test_validate_config_invalid_priority_rule_pattern() {
let mut config = YekConfig::extend_config_with_defaults(vec![], "/tmp/yek".to_string());
config.priority_rules = vec![PriorityRule {
pattern: "[".to_string(), score: 100,
}];
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("priority_rules"));
assert!(err.contains("Invalid pattern"));
}
#[test]
fn test_validate_config_invalid_ignore_pattern() {
let mut config = YekConfig::extend_config_with_defaults(vec![], "/tmp/yek".to_string());
config.ignore_patterns = vec!["[".to_string()];
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("ignore_patterns"));
assert!(err.contains("Invalid pattern"));
}
#[test]
fn test_validate_config_tree_header_mutual_exclusivity() {
let mut config = YekConfig::extend_config_with_defaults(vec![], "/tmp/yek".to_string());
config.tree_header = true;
config.tree_only = true;
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("tree_header and tree_only cannot both be enabled"));
}
#[test]
fn test_validate_config_json_with_tree_header() {
let mut config = YekConfig::extend_config_with_defaults(vec![], "/tmp/yek".to_string());
config.json = true;
config.tree_header = true;
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("JSON output not supported with tree header mode"));
}
#[test]
fn test_validate_config_json_with_tree_only() {
let mut config = YekConfig::extend_config_with_defaults(vec![], "/tmp/yek".to_string());
config.json = true;
config.tree_only = true;
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("JSON output not supported in tree-only mode"));
}
#[test]
fn test_validate_invalid_output_template() {
let cfg = YekConfig {
output_template: Some(">>>> FILE_PATH\n".to_string()),
..YekConfig::default()
};
let result = cfg.validate();
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"output_template: must contain FILE_PATH and FILE_CONTENT"
);
let cfg = YekConfig {
output_template: Some(">>>> FILE_CONTENT\n".to_string()),
..YekConfig::default()
};
let result = cfg.validate();
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"output_template: must contain FILE_PATH and FILE_CONTENT"
);
}
#[test]
fn test_validate_max_size_zero() {
let cfg = YekConfig {
max_size: "0".to_string(),
..YekConfig::default()
};
let result = cfg.validate();
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "max_size: cannot be 0");
}
#[test]
fn test_validate_invalid_tokens() {
let mut cfg = YekConfig {
token_mode: true,
tokens: "0".to_string(),
..YekConfig::default()
};
let result = cfg.validate();
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "tokens: cannot be 0");
cfg.tokens = "-100".to_string();
let result = cfg.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("tokens: Invalid token size:"));
cfg.tokens = "abc".to_string();
let result = cfg.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("tokens: Invalid token size:"));
}
#[test]
fn test_validate_invalid_ignore_patterns() {
let mut cfg = YekConfig {
ignore_patterns: vec!["**/*".to_string()],
..YekConfig::default()
};
let result = cfg.validate();
assert!(result.is_ok());
cfg.ignore_patterns.push("**[[".to_string()); let result = cfg.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
println!("Actual error message: {}", err);
assert!(err.contains("ignore_patterns: Invalid pattern"));
}
#[test]
fn test_validate_invalid_priority_rules() {
let mut cfg = YekConfig::default();
cfg.priority_rules.push(PriorityRule {
pattern: "*.rs".to_string(),
score: 500,
});
let result = cfg.validate();
assert!(result.is_ok());
let mut cfg = YekConfig::default();
cfg.priority_rules.push(PriorityRule {
pattern: "*.rs".to_string(),
score: -10,
});
let result = cfg.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
println!("Actual error message: {}", err);
assert!(err.contains("Priority score -10 must be between 0 and 1000"));
let mut cfg = YekConfig::default();
cfg.priority_rules.push(PriorityRule {
pattern: "[[[".to_string(),
score: 500,
});
let result = cfg.validate();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
println!("Actual error message: {}", err);
assert!(err.contains("priority_rules: Invalid pattern '[[[':"));
}
#[test]
fn test_ensure_output_dir_output_dir_is_file() {
let temp_dir = std::env::temp_dir();
let temp_file_path = temp_dir.join("yek_test_temp_file");
let mut temp_file = File::create(&temp_file_path).unwrap();
writeln!(temp_file, "test").unwrap();
let temp_file_path_str = temp_file_path.to_string_lossy().to_string();
let cfg = YekConfig {
output_dir: Some(temp_file_path_str.clone()),
stream: false,
..YekConfig::default()
};
let result = cfg.ensure_output_dir();
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!(
"output_dir: '{}' exists but is not a directory",
temp_file_path_str
)
);
std::fs::remove_file(&temp_file_path).unwrap();
}
#[test]
fn test_ensure_output_dir_valid_output_dir() {
let temp_dir = std::env::temp_dir().join("yek_test_output_dir");
let temp_dir_str = temp_dir.to_string_lossy().to_string();
if temp_dir.exists() {
fs::remove_dir_all(&temp_dir).unwrap();
}
let cfg = YekConfig {
output_dir: Some(temp_dir_str.clone()),
stream: false,
..YekConfig::default()
};
let result = cfg.ensure_output_dir();
assert!(result.is_ok());
assert_eq!(result.unwrap(), temp_dir_str);
assert!(temp_dir.is_dir());
fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_ensure_output_dir_output_dir_none() {
let cfg = YekConfig {
output_dir: None,
stream: false,
..YekConfig::default()
};
let result = cfg.ensure_output_dir();
assert!(result.is_ok());
let output_dir = result.unwrap();
assert!(output_dir.contains("yek-output"));
}
#[test]
fn test_ensure_output_dir_streaming() {
let cfg = YekConfig {
stream: true,
..Default::default()
};
let result = cfg.ensure_output_dir();
assert!(result.is_ok());
assert_eq!(result.unwrap(), String::new());
}
#[test]
fn test_get_checksum_consistency() {
let temp_dir = std::env::temp_dir().join("yek_test_checksum_dir");
if temp_dir.exists() {
fs::remove_dir_all(&temp_dir).unwrap();
}
fs::create_dir(&temp_dir).unwrap();
let file_path = temp_dir.join("test_file.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "Hello, world!").unwrap();
let input_dirs = vec![temp_dir.to_string_lossy().to_string()];
let checksum1 = YekConfig::get_checksum(&input_dirs);
std::thread::sleep(std::time::Duration::from_millis(100));
let checksum2 = YekConfig::get_checksum(&input_dirs);
assert_eq!(checksum1, checksum2);
let mut file = File::create(&file_path).unwrap();
writeln!(file, "Modified content").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
file.write_at(b" ", 0).unwrap();
}
#[cfg(windows)]
{
use std::os::windows::fs::FileExt;
file.seek_write(b" ", 0).unwrap();
}
let checksum3 = YekConfig::get_checksum(&input_dirs);
assert_ne!(checksum1, checksum3);
drop(file); fs::remove_dir_all(&temp_dir).unwrap_or_else(|e| eprintln!("Failed to remove temp dir: {}", e));
}
#[test]
fn test_extend_config_with_defaults() {
let input_paths = vec!["dir1".to_string(), "dir2".to_string()];
let output_dir = "output".to_string();
let cfg = YekConfig::extend_config_with_defaults(input_paths.clone(), output_dir.clone());
assert_eq!(cfg.input_paths, input_paths);
assert_eq!(cfg.output_dir, Some(output_dir));
assert!(!cfg.version);
assert_eq!(cfg.max_size, "10MB".to_string());
assert_eq!(cfg.tokens, String::new());
assert!(!cfg.json);
assert!(!cfg.debug);
assert_eq!(
cfg.output_template,
Some(DEFAULT_OUTPUT_TEMPLATE.to_string())
);
assert_eq!(cfg.ignore_patterns, Vec::<String>::new());
assert_eq!(cfg.unignore_patterns, Vec::<String>::new());
assert_eq!(cfg.priority_rules, Vec::<PriorityRule>::new());
assert_eq!(
cfg.binary_extensions,
BINARY_FILE_EXTENSIONS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
);
assert_eq!(cfg.git_boost_max, Some(100));
assert!(!cfg.stream);
assert!(!cfg.token_mode);
assert_eq!(cfg.output_file_full_path, None);
assert_eq!(cfg.max_git_depth, 100);
}
#[test]
fn test_validate_valid_config() {
let mut cfg = YekConfig {
output_template: Some(">>>> FILE_PATH\nFILE_CONTENT".to_string()),
max_size: "5MB".to_string(),
tokens: String::new(),
token_mode: false,
..YekConfig::default()
};
cfg.ignore_patterns.push("**/*.tmp".to_string());
cfg.unignore_patterns.push("**/important.tmp".to_string());
cfg.priority_rules.push(PriorityRule {
pattern: "*.rs".to_string(),
score: 500,
});
cfg.binary_extensions.push("bin".to_string());
cfg.git_boost_max = Some(500);
cfg.max_git_depth = 200;
let result = cfg.validate();
assert!(result.is_ok());
}
#[test]
fn test_merge_binary_extensions() {
let mut cfg = YekConfig {
binary_extensions: vec!["custom_ext".to_string(), "exe".to_string()],
..YekConfig::default()
};
let mut merged_bins = BINARY_FILE_EXTENSIONS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
merged_bins.append(&mut cfg.binary_extensions.clone());
cfg.binary_extensions = merged_bins
.into_iter()
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
let mut expected_extensions = BINARY_FILE_EXTENSIONS
.iter()
.map(|s| s.to_string())
.collect::<std::collections::HashSet<_>>();
expected_extensions.insert("custom_ext".to_string());
expected_extensions.insert("exe".to_string());
let extensions_set: std::collections::HashSet<_> = cfg.binary_extensions.into_iter().collect();
assert_eq!(extensions_set, expected_extensions);
}
#[test]
fn test_merge_ignore_patterns() {
let mut cfg = YekConfig {
ignore_patterns: vec!["**/*.log".to_string(), "**/*.tmp".to_string()],
unignore_patterns: vec!["**/important.log".to_string()],
..YekConfig::default()
};
let mut ignore = DEFAULT_IGNORE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
ignore.extend(cfg.ignore_patterns.clone());
cfg.ignore_patterns = ignore;
cfg.ignore_patterns
.extend(cfg.unignore_patterns.iter().map(|pat| format!("!{}", pat)));
let mut expected_patterns = DEFAULT_IGNORE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
expected_patterns.extend(vec!["**/*.log".to_string(), "**/*.tmp".to_string()]);
expected_patterns.push("!**/important.log".to_string());
assert_eq!(cfg.ignore_patterns, expected_patterns);
}
#[test]
fn test_input_paths_default() {
let mut cfg = YekConfig::default();
if cfg.input_paths.is_empty() {
cfg.input_paths.push(".".to_string());
}
assert_eq!(cfg.input_paths, vec![".".to_string()]);
}
#[test]
fn test_get_checksum_empty_dirs() {
let input_dirs: Vec<String> = vec![];
let checksum = YekConfig::get_checksum(&input_dirs);
assert!(!checksum.is_empty());
let input_dirs = vec!["non_existent_dir".to_string()];
let checksum = YekConfig::get_checksum(&input_dirs);
assert!(!checksum.is_empty());
}
#[test]
fn test_get_checksum_empty_directory() {
let temp_dir = std::env::temp_dir().join("yek_test_empty_dir");
if temp_dir.exists() {
fs::remove_dir_all(&temp_dir).unwrap();
}
fs::create_dir(&temp_dir).unwrap();
let input_dirs = vec![temp_dir.to_string_lossy().to_string()];
let checksum = YekConfig::get_checksum(&input_dirs);
assert!(!checksum.is_empty());
fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_validate_invalid_max_size_format() {
let cfg = YekConfig {
max_size: "invalid_size".to_string(),
..YekConfig::default()
};
let result = cfg.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_size: Invalid size format:"));
}
#[test]
fn test_validate_valid_tokens() {
let mut cfg = YekConfig {
token_mode: true,
tokens: "1000".to_string(),
..YekConfig::default()
};
cfg.tokens = "2000".to_string();
let result = cfg.validate();
assert!(result.is_ok());
}
#[test]
fn test_is_text_file_nonexistent() {
let path = Path::new("this_file_should_not_exist_1234567890.txt");
let result = is_text_file(path, &[]);
assert!(result.is_err(), "Expected error for nonexistent file");
}
#[test]
fn test_is_text_file_with_valid_text() {
let temp_dir = TempDir::new().expect("failed to create temp dir");
let file_path = temp_dir.path().join("sample.txt");
fs::write(&file_path, "This is a valid text file.").expect("failed to write file");
let result = is_text_file(&file_path, &[]);
assert!(result.is_ok());
assert!(
result.unwrap(),
"Expected a text file to be detected as text"
);
}
#[test]
fn test_is_text_file_with_binary_content() {
let temp_dir = TempDir::new().expect("failed to create temp dir");
let file_path = temp_dir.path().join("binary.dat");
fs::write(&file_path, [0, 159, 146, 150]).expect("failed to write binary file");
let result = is_text_file(&file_path, &[]);
assert!(result.is_ok());
assert!(
!result.unwrap(),
"Expected a binary file to be detected as binary"
);
}
#[test]
fn test_config_files_ignored_by_default() {
use yek::defaults::DEFAULT_IGNORE_PATTERNS;
use yek::serialize_repo;
let temp_dir = TempDir::new().expect("failed to create temp dir");
fs::write(temp_dir.path().join("README.md"), "# Test Project").expect("failed to write README");
fs::write(temp_dir.path().join("main.rs"), "fn main() {}").expect("failed to write main.rs");
fs::write(temp_dir.path().join("yek.yaml"), "output_dir: \"./output\"")
.expect("failed to write yek.yaml");
fs::write(
temp_dir.path().join("yek.json"),
"{\"output_dir\": \"./output\"}",
)
.expect("failed to write yek.json");
fs::write(
temp_dir.path().join("yek.toml"),
"output_dir = \"./output\"",
)
.expect("failed to write yek.toml");
let mut config = YekConfig {
input_paths: vec![temp_dir.path().to_string_lossy().to_string()],
stream: true, ..YekConfig::default()
};
config.ignore_patterns = DEFAULT_IGNORE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let (output, files) = serialize_repo(&config).expect("failed to serialize repo");
assert!(
!output.contains("yek.yaml"),
"Output should not contain yek.yaml content"
);
assert!(
!output.contains("yek.json"),
"Output should not contain yek.json content"
);
assert!(
!output.contains("yek.toml"),
"Output should not contain yek.toml content"
);
let file_paths: Vec<&str> = files.iter().map(|f| f.rel_path.as_str()).collect();
assert!(
!file_paths.iter().any(|&path| path.ends_with("yek.yaml")),
"yek.yaml should be ignored"
);
assert!(
!file_paths.iter().any(|&path| path.ends_with("yek.json")),
"yek.json should be ignored"
);
assert!(
!file_paths.iter().any(|&path| path.ends_with("yek.toml")),
"yek.toml should be ignored"
);
assert!(
file_paths.iter().any(|&path| path.ends_with("README.md")),
"README.md should be included"
);
assert!(
file_paths.iter().any(|&path| path.ends_with("main.rs")),
"main.rs should be included"
);
}
#[test]
fn test_output_template_from_toml_config() {
let _guard = CONFIG_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let temp_dir = TempDir::new().expect("failed to create temp dir");
let config_path = temp_dir.path().join("yek.toml");
let config_content = "output_template = \"==== FILE_PATH ====\\n\\nFILE_CONTENT\"";
fs::write(&config_path, config_content).expect("failed to write config file");
let old_dir = std::env::current_dir().expect("failed to get current dir");
std::env::set_current_dir(temp_dir.path()).expect("failed to change dir");
let settings = config::Config::builder()
.add_source(config::File::from(config_path.clone()).required(false))
.build()
.unwrap_or_else(|_| config::Config::builder().build().unwrap());
let output_template: Option<String> = settings.get("output_template").ok();
let mut config = YekConfig::parse();
if let Some(template) = output_template {
config.output_template = Some(template);
} else if config.output_template.is_none() {
config.output_template = Some(DEFAULT_OUTPUT_TEMPLATE.to_string());
}
std::env::set_current_dir(&old_dir).expect("failed to restore dir");
assert_eq!(
config.output_template,
Some("==== FILE_PATH ====\n\nFILE_CONTENT".to_string())
);
drop(temp_dir);
}
#[test]
fn test_output_template_from_yaml_config() {
let _guard = CONFIG_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let temp_dir = TempDir::new().expect("failed to create temp dir");
let config_path = temp_dir.path().join("yek.yaml");
let config_content = "output_template: \"### FILE_PATH ###\\n\\nFILE_CONTENT\"";
fs::write(&config_path, config_content).expect("failed to write config file");
let old_dir = std::env::current_dir().expect("failed to get current dir");
std::env::set_current_dir(temp_dir.path()).expect("failed to change dir");
let settings = config::Config::builder()
.add_source(config::File::from(config_path.clone()).required(false))
.build()
.unwrap_or_else(|_| config::Config::builder().build().unwrap());
let output_template: Option<String> = settings.get("output_template").ok();
let mut config = YekConfig::parse();
if let Some(template) = output_template {
config.output_template = Some(template);
} else if config.output_template.is_none() {
config.output_template = Some(DEFAULT_OUTPUT_TEMPLATE.to_string());
}
std::env::set_current_dir(&old_dir).expect("failed to restore dir");
assert_eq!(
config.output_template,
Some("### FILE_PATH ###\n\nFILE_CONTENT".to_string())
);
drop(temp_dir);
}
#[test]
fn test_output_template_from_json_config() {
let _guard = CONFIG_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let temp_dir = TempDir::new().expect("failed to create temp dir");
let config_path = temp_dir.path().join("yek.json");
let config_content = r#"{"output_template": "@@@ FILE_PATH @@@\n\nFILE_CONTENT"}"#;
fs::write(&config_path, config_content).expect("failed to write config file");
let old_dir = std::env::current_dir().expect("failed to get current dir");
std::env::set_current_dir(temp_dir.path()).expect("failed to change dir");
let settings = config::Config::builder()
.add_source(config::File::from(config_path.clone()).required(false))
.build()
.unwrap_or_else(|_| config::Config::builder().build().unwrap());
let output_template: Option<String> = settings.get("output_template").ok();
let mut config = YekConfig::parse();
if let Some(template) = output_template {
config.output_template = Some(template);
} else if config.output_template.is_none() {
config.output_template = Some(DEFAULT_OUTPUT_TEMPLATE.to_string());
}
std::env::set_current_dir(&old_dir).expect("failed to restore dir");
assert_eq!(
config.output_template,
Some("@@@ FILE_PATH @@@\n\nFILE_CONTENT".to_string())
);
drop(temp_dir);
}
#[test]
fn test_output_template_defaults_when_no_config() {
let _guard = CONFIG_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let temp_dir = TempDir::new().expect("failed to create temp dir");
let old_dir = std::env::current_dir().expect("failed to get current dir");
std::env::set_current_dir(temp_dir.path()).expect("failed to change dir");
let mut config = YekConfig::parse();
if config.output_template.is_none() {
config.output_template = Some(DEFAULT_OUTPUT_TEMPLATE.to_string());
}
std::env::set_current_dir(&old_dir).expect("failed to restore dir");
assert_eq!(
config.output_template,
Some(DEFAULT_OUTPUT_TEMPLATE.to_string())
);
drop(temp_dir);
}
#[test]
fn test_read_input_paths_from_stdin() {
use std::process::{Command, Stdio};
let child = Command::new("echo")
.arg("")
.stdout(Stdio::piped())
.spawn()
.unwrap();
let _output = child.wait_with_output().unwrap();
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_init_config_with_stdin_input() {
let mut config = YekConfig::default();
config.input_paths = vec![];
if config.input_paths.is_empty() {
config.input_paths.push(".".to_string());
}
assert_eq!(config.input_paths, vec![".".to_string()]);
}
#[test]
fn test_ensure_output_dir_permission_denied() {
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_validate_config_with_tree_options() {
let mut config = YekConfig::default();
config.tree_header = true;
config.json = true;
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("JSON output not supported with tree header mode"));
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_validate_config_with_tree_only() {
let mut config = YekConfig::default();
config.tree_only = true;
config.json = true;
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("JSON output not supported in tree-only mode"));
}
#[test]
fn test_get_checksum_with_nonexistent_files() {
let input_paths = vec!["nonexistent_file.txt".to_string()];
let checksum = YekConfig::get_checksum(&input_paths);
assert!(!checksum.is_empty());
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_get_checksum_with_mixed_paths() {
use std::fs;
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "test content").unwrap();
let input_paths = vec![
file_path.to_string_lossy().to_string(),
"nonexistent.txt".to_string(),
temp_dir.path().to_string_lossy().to_string(),
];
let checksum1 = YekConfig::get_checksum(&input_paths);
let checksum2 = YekConfig::get_checksum(&input_paths);
assert_eq!(checksum1, checksum2);
assert!(!checksum1.is_empty());
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_init_config_binary_extensions_merge() {
let mut config = YekConfig::default();
config.binary_extensions = vec!["custom".to_string()];
let mut merged_bins = BINARY_FILE_EXTENSIONS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
merged_bins.append(&mut config.binary_extensions.clone());
config.binary_extensions = merged_bins
.into_iter()
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
assert!(config.binary_extensions.contains(&"custom".to_string()));
assert!(config.binary_extensions.contains(&"exe".to_string()));
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_init_config_ignore_patterns_merge() {
let mut config = YekConfig::default();
config.ignore_patterns = vec!["custom_ignore".to_string()];
config.unignore_patterns = vec!["important".to_string()];
let mut ignore = DEFAULT_IGNORE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
ignore.extend(config.ignore_patterns.clone());
config.ignore_patterns = ignore;
config.ignore_patterns.extend(
config
.unignore_patterns
.iter()
.map(|pat| format!("!{}", pat)),
);
assert!(config
.ignore_patterns
.contains(&"custom_ignore".to_string()));
assert!(config.ignore_patterns.contains(&"!important".to_string()));
}
#[test]
fn test_read_input_paths_from_stdin_with_error() {
let config = YekConfig::default();
assert!(config.input_paths.is_empty());
}
#[test]
fn test_ensure_output_dir_creation_failure() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let temp_dir = tempdir().unwrap();
let readonly_dir = temp_dir.path().join("readonly");
fs::create_dir(&readonly_dir).unwrap();
let mut perms = fs::metadata(&readonly_dir).unwrap().permissions();
perms.set_mode(0o444);
fs::set_permissions(&readonly_dir, perms).unwrap();
let config = YekConfig {
output_dir: Some(readonly_dir.join("subdir").to_string_lossy().to_string()),
stream: false,
..Default::default()
};
let result = config.ensure_output_dir();
let mut perms = fs::metadata(&readonly_dir).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&readonly_dir, perms).unwrap();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot create"));
}
}
#[test]
fn test_get_checksum_with_permission_denied() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let temp_dir = tempdir().unwrap();
let restricted_dir = temp_dir.path().join("restricted");
fs::create_dir(&restricted_dir).unwrap();
fs::write(restricted_dir.join("file.txt"), "content").unwrap();
let mut perms = fs::metadata(&restricted_dir).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&restricted_dir, perms).unwrap();
let input_paths = vec![restricted_dir.to_string_lossy().to_string()];
let checksum = YekConfig::get_checksum(&input_paths);
let mut perms = fs::metadata(&restricted_dir).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&restricted_dir, perms).unwrap();
assert!(!checksum.is_empty());
}
}
#[test]
fn test_validate_config_with_invalid_token_format() {
let config = YekConfig {
token_mode: true,
tokens: "k".to_string(), ..Default::default()
};
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid token format"));
}
#[test]
fn test_validate_config_with_invalid_max_size_format() {
let config = YekConfig {
max_size: "10XB".to_string(), token_mode: false,
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid size format"));
}
#[test]
fn test_get_checksum_with_file_metadata_errors() {
let temp_dir = tempdir().unwrap();
let symlink_path = temp_dir.path().join("broken_symlink");
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
symlink("/nonexistent/target", &symlink_path).unwrap();
}
#[cfg(windows)]
{
fs::write(&symlink_path, "content").unwrap();
}
let input_paths = vec![symlink_path.to_string_lossy().to_string()];
let checksum = YekConfig::get_checksum(&input_paths);
assert!(!checksum.is_empty());
}
#[test]
fn test_read_input_paths_trimming() {
use std::io::{BufRead, BufReader, Cursor};
let input = " path1.txt \n\tpath2.txt\t\n \npath3.txt";
let cursor = Cursor::new(input);
let reader = BufReader::new(cursor);
let mut paths = Vec::new();
for line in reader.lines() {
let line = line.unwrap();
let trimmed = line.trim(); if !trimmed.is_empty() {
paths.push(trimmed.to_string());
}
}
assert_eq!(paths, vec!["path1.txt", "path2.txt", "path3.txt"]);
}
#[test]
fn test_ensure_output_dir_path_new() {
use std::path::Path;
let output_dir = "/tmp/test_output";
let path = Path::new(&output_dir);
assert_eq!(path.to_str().unwrap(), "/tmp/test_output");
}
#[test]
fn test_init_config_empty_input_paths() {
let mut config = YekConfig::default();
if config.input_paths.is_empty() {
config.input_paths.push(".".to_string());
}
assert_eq!(config.input_paths, vec!["."]);
}
#[test]
fn test_stdin_read_error_fallback() {
let mut config = YekConfig::default();
let error_msg = "Failed to read from stdin: test error";
eprintln!("Warning: {}", error_msg); config.input_paths.push(".".to_string());
assert_eq!(config.input_paths, vec!["."]);
}
#[test]
fn test_default_to_current_dir_when_no_stdin() {
let mut config = YekConfig::default();
config.input_paths.push(".".to_string());
assert_eq!(config.input_paths, vec!["."]);
}
#[test]
fn test_binary_extensions_merging_complete() {
use std::collections::HashSet;
let mut config = YekConfig {
binary_extensions: vec!["custom1".to_string(), "exe".to_string()], ..Default::default()
};
let mut merged_bins = BINARY_FILE_EXTENSIONS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
merged_bins.append(&mut config.binary_extensions.clone());
config.binary_extensions = merged_bins
.into_iter()
.collect::<HashSet<_>>()
.into_iter()
.collect();
let unique_count = config
.binary_extensions
.iter()
.collect::<HashSet<_>>()
.len();
assert_eq!(unique_count, config.binary_extensions.len());
assert!(config.binary_extensions.contains(&"exe".to_string()));
assert!(config.binary_extensions.contains(&"custom1".to_string()));
}
#[test]
fn test_default_ignore_patterns_init() {
let mut config = YekConfig::default();
let mut ignore = DEFAULT_IGNORE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
ignore.extend(config.ignore_patterns.clone());
config.ignore_patterns = ignore;
for pattern in DEFAULT_IGNORE_PATTERNS {
assert!(config.ignore_patterns.contains(&pattern.to_string()));
}
}
#[test]
fn test_unignore_patterns_processing() {
let mut config = YekConfig {
ignore_patterns: DEFAULT_IGNORE_PATTERNS
.iter()
.map(|s| s.to_string())
.collect(),
unignore_patterns: vec!["important.log".to_string()],
..Default::default()
};
let custom_patterns = vec!["*.tmp".to_string()];
config.ignore_patterns.extend(custom_patterns);
config.ignore_patterns.extend(
config
.unignore_patterns
.iter()
.map(|pat| format!("!{}", pat)),
);
assert!(config.ignore_patterns.contains(&"*.tmp".to_string()));
assert!(config
.ignore_patterns
.contains(&"!important.log".to_string()));
}
#[test]
fn test_config_update_flag_default() {
let config = YekConfig::default();
assert!(!config.update, "update flag should default to false");
}
#[test]
fn test_get_target_triple() {
let result = YekConfig::get_target_triple();
assert!(result.is_ok(), "Should be able to determine target triple");
let target = result.unwrap();
assert!(!target.is_empty(), "Target triple should not be empty");
let supported_targets = [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
];
assert!(
supported_targets.contains(&target.as_str()),
"Target triple '{}' should be supported",
target
);
}
#[test]
fn test_extract_version_tag() {
let mock_json = r#"{
"url": "https://api.github.com/repos/bodo-run/yek/releases/123",
"tag_name": "v1.2.3",
"name": "Release v1.2.3",
"draft": false
}"#;
let result = YekConfig::extract_version_tag(mock_json);
assert!(result.is_ok(), "Should extract version successfully");
assert_eq!(result.unwrap(), "1.2.3", "Should remove 'v' prefix");
let mock_json_no_v = r#"{
"tag_name": "2.0.0",
"name": "Release 2.0.0"
}"#;
let result = YekConfig::extract_version_tag(mock_json_no_v);
assert!(result.is_ok(), "Should extract version without 'v' prefix");
assert_eq!(result.unwrap(), "2.0.0", "Should return version as-is");
}
#[test]
fn test_extract_download_url() {
let mock_json = r#"{
"assets": [
{
"name": "yek-x86_64-unknown-linux-musl.tar.gz",
"browser_download_url": "https://github.com/bodo-run/yek/releases/download/v1.2.3/yek-x86_64-unknown-linux-musl.tar.gz"
},
{
"name": "yek-aarch64-apple-darwin.tar.gz",
"browser_download_url": "https://github.com/bodo-run/yek/releases/download/v1.2.3/yek-aarch64-apple-darwin.tar.gz"
}
]
}"#;
let result = YekConfig::extract_download_url(mock_json, "yek-x86_64-unknown-linux-musl.tar.gz");
assert!(result.is_ok(), "Should extract download URL successfully");
assert_eq!(
result.unwrap(),
"https://github.com/bodo-run/yek/releases/download/v1.2.3/yek-x86_64-unknown-linux-musl.tar.gz"
);
let result = YekConfig::extract_download_url(mock_json, "nonexistent-asset.tar.gz");
assert!(result.is_err(), "Should fail when asset not found");
}