extern crate dumpfiles;
extern crate tempfile;
use dumpfiles::{write_directory_contents_yaml, GitignoreMode};
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use tempfile::tempdir;
fn create_file(path: &Path, content: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
fn read_yaml_output(path: &Path) -> std::io::Result<String> {
fs::read_to_string(path)
}
fn assert_file_in_yaml(yaml_content: &str, file_path: &str, expected_content: Option<&str>) {
assert!(
yaml_content.contains(&format!(" - path: {:?}", file_path)),
"Expected file '{}' not found in YAML output",
file_path
);
if let Some(content) = expected_content {
assert!(
yaml_content.contains(content),
"Expected content '{}' not found for file '{}'",
content,
file_path
);
}
}
fn assert_file_not_in_yaml(yaml_content: &str, file_path: &str) {
assert!(
!yaml_content.contains(&format!(" - path: {:?}", file_path)),
"File '{}' should not be in YAML output",
file_path
);
}
#[test]
fn test_basic_single_file() {
let dir = tempdir().unwrap();
let test_file = dir.path().join("test.txt");
create_file(&test_file, "Hello, world!").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert!(output.starts_with("project:"));
assert!(output.contains("files:"));
assert_file_in_yaml(&output, "test.txt", Some("Hello, world!"));
assert!(output.contains("lines: 1"));
assert!(output.contains("size: \"13 B\""));
}
#[test]
fn test_multiple_files() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("file1.txt"), "Content 1").unwrap();
create_file(&dir.path().join("file2.txt"), "Content 2").unwrap();
create_file(&dir.path().join("file3.md"), "# Markdown").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "file1.txt", Some("Content 1"));
assert_file_in_yaml(&output, "file2.txt", Some("Content 2"));
assert_file_in_yaml(&output, "file3.md", Some("# Markdown"));
}
#[test]
fn test_nested_directories() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("root.txt"), "Root file").unwrap();
create_file(&dir.path().join("subdir/nested.txt"), "Nested file").unwrap();
create_file(&dir.path().join("subdir/deep/deeper.txt"), "Deep file").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "root.txt", Some("Root file"));
#[cfg(windows)]
{
assert_file_in_yaml(&output, "subdir/nested.txt", Some("Nested file"));
assert_file_in_yaml(&output, "subdir/deep/deeper.txt", Some("Deep file"));
}
#[cfg(not(windows))]
{
assert_file_in_yaml(&output, "subdir/nested.txt", Some("Nested file"));
assert_file_in_yaml(&output, "subdir/deep/deeper.txt", Some("Deep file"));
}
}
#[test]
fn test_ignore_patterns_basic() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("include.txt"), "Include me").unwrap();
create_file(&dir.path().join("exclude.log"), "Exclude me").unwrap();
create_file(&dir.path().join("test.tmp"), "Also exclude").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(
dir.path(),
&output_path,
&["*.log".to_string(), "*.tmp".to_string()],
GitignoreMode::Auto,
None,
)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "include.txt", Some("Include me"));
assert_file_not_in_yaml(&output, "exclude.log");
assert_file_not_in_yaml(&output, "test.tmp");
}
#[test]
fn test_ignore_patterns_directories() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("visible.txt"), "Visible").unwrap();
create_file(&dir.path().join("node_modules/package.json"), "Package").unwrap();
create_file(&dir.path().join("build/output.js"), "Built").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(
dir.path(),
&output_path,
&["node_modules/**".to_string(), "build/**".to_string()],
GitignoreMode::Auto,
None,
)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "visible.txt", Some("Visible"));
assert_file_not_in_yaml(&output, "node_modules/package.json");
assert_file_not_in_yaml(&output, "build/output.js");
}
#[test]
fn test_empty_directory() {
let dir = tempdir().unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
let project_name = dir
.path()
.canonicalize()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let expected = format!("project: {}\nfiles:\n", project_name);
assert_eq!(
output, expected,
"YAML header for empty directory should be exactly two lines"
);
assert_eq!(output.lines().count(), 2);
}
#[test]
fn test_empty_file() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("empty.txt"), "").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "empty.txt", None);
assert!(output.contains("lines: 0"));
assert!(output.contains("size: \"0 B\""));
assert!(output.contains("tokens: 0"));
}
#[test]
fn test_multiline_content() {
let dir = tempdir().unwrap();
let content = "Line 1\nLine 2\nLine 3\n\nLine 5 with spaces ";
create_file(&dir.path().join("multiline.txt"), content).unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "multiline.txt", None);
assert!(output.contains("lines: 5"));
assert!(output.contains(" Line 1"));
assert!(output.contains(" Line 2"));
assert!(output.contains(" Line 3"));
assert!(output.contains(" Line 5 with spaces "));
}
#[test]
fn test_special_characters_in_content() {
let dir = tempdir().unwrap();
let content = "Special: !@#$%^&*()\n\"Quotes\" and 'apostrophes'\nBackslash: \\ and pipe: |";
create_file(&dir.path().join("special.txt"), content).unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "special.txt", None);
assert!(output.contains("Special: !@#$%^&*()"));
assert!(output.contains("\"Quotes\" and 'apostrophes'"));
assert!(output.contains("Backslash: \\ and pipe: |"));
}
#[test]
fn test_file_size_formatting() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("small.txt"), "Hi").unwrap();
let large_content = "x".repeat(2048);
create_file(&dir.path().join("large.txt"), &large_content).unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
fn section_for<'a>(output: &'a str, filename: &'a str) -> &'a str {
let start = output
.find(&format!("path: {}", filename))
.or_else(|| output.find(&format!("path: \"{}\"", filename)))
.unwrap();
let rest = &output[start..];
rest.split("\n - path:").next().unwrap()
}
let small_section = section_for(&output, "small.txt");
assert!(small_section.contains("size: \"2 B\"") || small_section.contains("size: 2 B"));
assert!(output.contains("size: \"2.0 KB\"") || output.contains("size: 2.0 KB"));
}
#[test]
fn test_binary_file_handling() {
let dir = tempdir().unwrap();
let binary_path = dir.path().join("binary.bin");
let binary_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10]; fs::write(&binary_path, binary_data).unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "binary.bin", None);
assert!(output.contains("Binary or inaccessible file"));
assert!(output.contains("lines: 0"));
assert!(output.contains("tokens: 0"));
}
#[test]
fn test_output_file_excluded() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("test.txt"), "Test content").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "test.txt", Some("Test content"));
assert_file_not_in_yaml(&output, "output.yaml");
}
#[test]
fn test_unicode_filenames_and_content() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("日本語.txt"), "こんにちは").unwrap();
create_file(&dir.path().join("emoji😀.txt"), "Hello 👋 World 🌍").unwrap();
create_file(&dir.path().join("français.txt"), "Café résumé naïve").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "日本語.txt", Some("こんにちは"));
assert_file_in_yaml(&output, "emoji😀.txt", Some("Hello 👋 World 🌍"));
assert_file_in_yaml(&output, "français.txt", Some("Café résumé naïve"));
}
#[test]
fn test_dumpignore_file() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("include.txt"), "Include").unwrap();
create_file(&dir.path().join("exclude.txt"), "Exclude").unwrap();
create_file(&dir.path().join("also_exclude.log"), "Exclude log").unwrap();
create_file(&dir.path().join(".dumpignore"), "exclude.txt\n*.log").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(
dir.path(),
&output_path,
&[],
GitignoreMode::Auto,
Some(&dir.path().join(".dumpignore")),
)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "include.txt", Some("Include"));
assert_file_not_in_yaml(&output, "exclude.txt");
assert_file_not_in_yaml(&output, "also_exclude.log");
}
#[test]
fn test_token_counting() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("test.txt"), "Hello, world!").unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert!(output.contains("tokens: "));
if let Some(token_line) = output.lines().find(|l| l.contains("tokens: ")) {
let token_str = token_line.split("tokens: ").nth(1).unwrap();
let token_count: usize = token_str.parse().unwrap();
assert!(token_count > 0, "Token count should be greater than 0");
assert!(
token_count < 20,
"Token count seems unreasonably high for 'Hello, world!'"
);
}
}
#[test]
fn test_project_name_from_directory() {
let dir = tempdir().unwrap();
let named_dir = dir.path().join("my_project");
fs::create_dir_all(&named_dir).unwrap();
create_file(&named_dir.join("file.txt"), "content").unwrap();
let output_path = named_dir.join("output.yaml");
write_directory_contents_yaml(&named_dir, &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert!(output.starts_with("project: my_project"));
}
#[test]
fn test_nonexistent_directory() {
let dir = tempdir().unwrap();
let nonexistent = dir.path().join("does_not_exist");
let output_path = dir.path().join("output.yaml");
let result =
write_directory_contents_yaml(&nonexistent, &output_path, &[], GitignoreMode::Auto, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to get absolute path"));
}
#[test]
fn test_permission_denied() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let dir = tempdir().unwrap();
let restricted_file = dir.path().join("restricted.txt");
create_file(&restricted_file, "Secret").unwrap();
let mut perms = fs::metadata(&restricted_file).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&restricted_file, perms).unwrap();
let output_path = dir.path().join("output.yaml");
let result =
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None);
let mut perms = fs::metadata(&restricted_file).unwrap().permissions();
perms.set_mode(0o644);
fs::set_permissions(&restricted_file, perms).unwrap();
assert!(result.is_ok());
let output = read_yaml_output(&output_path).unwrap();
assert!(output.contains("restricted.txt"));
assert!(output.contains("Binary or inaccessible file"));
}
}
#[test]
fn test_gitignore_on_off() {
let dir = tempdir().unwrap();
create_file(&dir.path().join("keep.txt"), "keep").unwrap();
create_file(&dir.path().join("secret.log"), "top secret").unwrap();
let gitignore_path = dir.path().join(".gitignore");
create_file(&gitignore_path, "*.log\n").unwrap();
let output_with = dir.path().join("with_gitignore.yaml");
write_directory_contents_yaml(
dir.path(),
&output_with,
&[],
GitignoreMode::Path(gitignore_path),
None,
)
.unwrap();
let out_with = read_yaml_output(&output_with).unwrap();
assert_file_in_yaml(&out_with, "keep.txt", Some("keep"));
assert_file_not_in_yaml(&out_with, "secret.log");
let output_without = dir.path().join("without_gitignore.yaml");
write_directory_contents_yaml(
dir.path(),
&output_without,
&[],
GitignoreMode::Disabled,
None,
)
.unwrap();
let out_without = read_yaml_output(&output_without).unwrap();
assert_file_in_yaml(&out_without, "keep.txt", Some("keep"));
assert_file_in_yaml(&out_without, "secret.log", Some("top secret"));
}
#[test]
fn test_symlink_handling() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let dir = tempdir().unwrap();
create_file(&dir.path().join("target.txt"), "real file").unwrap();
symlink(
dir.path().join("target.txt"),
dir.path().join("symlink.txt"),
)
.unwrap();
create_file(&dir.path().join("realdir/sub.txt"), "inside realdir").unwrap();
symlink(dir.path().join("realdir"), dir.path().join("linkdir")).unwrap();
let output_path = dir.path().join("output.yaml");
write_directory_contents_yaml(dir.path(), &output_path, &[], GitignoreMode::Auto, None)
.unwrap();
let output = read_yaml_output(&output_path).unwrap();
assert_file_in_yaml(&output, "target.txt", Some("real file"));
assert_file_in_yaml(&output, "realdir/sub.txt", Some("inside realdir"));
assert_file_not_in_yaml(&output, "symlink.txt");
assert!(
!output.contains("linkdir/"),
"Should not traverse into symlinked directories (linkdir/*)"
);
}
#[cfg(not(unix))]
{
eprintln!("symlink test skipped on non-Unix platforms");
}
}