#![cfg(feature = "cli")]
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
fn luff_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("luff"))
}
#[test]
fn test_help_flag() {
luff_cmd()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Print files"));
}
#[test]
fn test_version_flag() {
luff_cmd()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_process_single_file() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.txt");
fs::write(&file_path, "Hello, World!").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--files")
.arg(file_path.to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("Hello, World!"))
.stdout(predicate::str::contains("```txt"));
}
#[test]
fn test_process_multiple_files() {
let temp = TempDir::new().unwrap();
let file1 = temp.path().join("file1.txt");
fs::write(&file1, "Content 1").unwrap();
let file2 = temp.path().join("file2.txt");
fs::write(&file2, "Content 2").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--files")
.arg(file1.to_str().unwrap())
.arg(file2.to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("Content 1"))
.stdout(predicate::str::contains("Content 2"));
}
#[test]
fn test_nonexistent_file_error() {
luff_cmd()
.arg("--files")
.arg("/nonexistent/file.txt")
.assert()
.failure() .stderr(predicate::str::contains(
"All 1 specified file(s) were invalid",
));
}
#[test]
fn test_directory_walk() {
let temp = TempDir::new().unwrap();
fs::create_dir(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}").unwrap();
fs::write(temp.path().join("README.md"), "# Test").unwrap();
luff_cmd()
.current_dir(temp.path())
.assert()
.success()
.stdout(predicate::str::contains("main.rs"))
.stdout(predicate::str::contains("README.md"));
}
#[test]
fn test_gitignore_respected_by_default() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join(".gitignore"), "ignored.txt\n").unwrap();
fs::write(temp.path().join("included.txt"), "include").unwrap();
fs::write(temp.path().join("ignored.txt"), "ignore").unwrap();
luff_cmd()
.current_dir(temp.path())
.assert()
.success()
.stdout(predicate::str::contains("included.txt"))
.stdout(predicate::str::contains("ignored.txt").not());
}
#[test]
fn test_add_flag_includes_gitignored_files() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join(".gitignore"), "ignored.txt\n").unwrap();
fs::write(temp.path().join("included.txt"), "include").unwrap();
fs::write(temp.path().join("ignored.txt"), "ignore").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--add")
.assert()
.success()
.stdout(predicate::str::contains("included.txt"))
.stdout(predicate::str::contains("ignored.txt"));
}
#[test]
fn test_add_flag_short_form() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join(".gitignore"), "secret.txt\n").unwrap();
fs::write(temp.path().join("public.txt"), "public").unwrap();
fs::write(temp.path().join("secret.txt"), "secret").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("-a")
.assert()
.success()
.stdout(predicate::str::contains("public.txt"))
.stdout(predicate::str::contains("secret.txt"));
}
#[test]
fn test_ignore_flag_excludes_files() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("keep.txt"), "keep").unwrap();
fs::write(temp.path().join("skip.log"), "skip").unwrap();
fs::create_dir(temp.path().join("target")).unwrap();
fs::write(temp.path().join("target/build.rs"), "build").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--ignore")
.arg("*.log")
.arg("--ignore")
.arg("target/**")
.assert()
.success()
.stdout(predicate::str::contains("keep.txt"))
.stdout(predicate::str::contains("skip.log").not())
.stdout(predicate::str::contains("build.rs").not());
}
#[test]
fn test_gitignore_with_patterns() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join(".gitignore"), "*.log\ntarget/\n").unwrap();
fs::create_dir(temp.path().join("target")).unwrap();
fs::write(temp.path().join("app.txt"), "app").unwrap();
fs::write(temp.path().join("debug.log"), "log").unwrap();
fs::write(temp.path().join("target/build.txt"), "build").unwrap();
luff_cmd()
.current_dir(temp.path())
.assert()
.success()
.stdout(predicate::str::contains("app.txt"))
.stdout(predicate::str::contains("debug.log").not())
.stdout(predicate::str::contains("target/build.txt").not());
}
#[test]
fn test_dotfiles_excluded_by_default() {
let temp = TempDir::new().unwrap();
fs::create_dir(temp.path().join(".hidden")).unwrap();
fs::write(temp.path().join(".hidden/file.txt"), "hidden").unwrap();
fs::write(temp.path().join("visible.txt"), "visible").unwrap();
luff_cmd()
.current_dir(temp.path())
.assert()
.success()
.stdout(predicate::str::contains("visible.txt"))
.stdout(predicate::str::contains(".hidden").not());
}
#[test]
fn test_dotfiles_included_with_flag() {
let temp = TempDir::new().unwrap();
fs::create_dir(temp.path().join(".hidden")).unwrap();
fs::write(temp.path().join(".hidden/file.txt"), "hidden").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--dotfiles")
.assert()
.success()
.stdout(predicate::str::contains(".hidden/file.txt"));
}
#[test]
fn test_binary_files_skipped() {
let temp = TempDir::new().unwrap();
let png_file = temp.path().join("image.png");
fs::write(&png_file, vec![0u8; 100]).unwrap();
let txt_file = temp.path().join("text.txt");
fs::write(&txt_file, "text content").unwrap();
luff_cmd()
.current_dir(temp.path())
.assert()
.success()
.stdout(predicate::str::contains("text.txt"))
.stdout(predicate::str::contains("image.png").not());
}
#[test]
fn test_verbose_logging() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("test.txt"), "test").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--verbose")
.assert()
.success();
}
#[test]
fn test_output_format_markdown() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("test.txt"), "content").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--format")
.arg("markdown")
.assert()
.success()
.stdout(predicate::str::contains("```"));
}
#[test]
fn test_output_format_tree() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("test.txt"), "content").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--format")
.arg("tree")
.assert()
.success()
.stdout(predicate::str::contains("."))
.stdout(predicate::str::contains("└── test.txt"));
}
#[test]
fn test_max_depth_option() {
let temp = TempDir::new().unwrap();
fs::create_dir_all(temp.path().join("a/b/c")).unwrap();
fs::write(temp.path().join("a/file1.txt"), "1").unwrap();
fs::write(temp.path().join("a/b/file2.txt"), "2").unwrap();
fs::write(temp.path().join("a/b/c/file3.txt"), "3").unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--max-depth")
.arg("2")
.assert()
.success()
.stdout(predicate::str::contains("file1.txt"))
.stdout(predicate::str::contains("file2.txt"))
.stdout(predicate::str::contains("file3.txt").not());
}
#[test]
fn test_tree_shows_empty_directories() {
let temp = TempDir::new().unwrap();
fs::create_dir_all(temp.path().join("parent/child")).unwrap();
fs::write(temp.path().join("parent/file.txt"), "content").unwrap();
fs::create_dir(temp.path().join("parent/empty")).unwrap();
luff_cmd()
.current_dir(temp.path())
.arg("--format")
.arg("tree")
.assert()
.success()
.stdout(predicate::str::contains("parent"))
.stdout(predicate::str::contains("empty"))
.stdout(predicate::str::contains("child"));
}
#[test]
fn test_file_snapshot_consistency() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("file1.txt"), "content1").unwrap();
fs::write(temp.path().join("file2.txt"), "content2").unwrap();
let output = luff_cmd().current_dir(temp.path()).output().unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("file1.txt"));
assert!(stdout.contains("file2.txt"));
let output_file = temp.path().join("output.txt");
fs::write(&output_file, &stdout).unwrap();
let output2 = luff_cmd().current_dir(temp.path()).output().unwrap();
let stdout2 = String::from_utf8(output2.stdout).unwrap();
assert!(stdout2.contains("file1.txt"));
assert!(stdout2.contains("file2.txt"));
assert!(
stdout2.contains("output.txt"),
"Files existing at walker construction should be included"
);
assert!(
stdout2.len() > stdout.len(),
"Second run should include the newly created output.txt"
);
}
#[test]
fn test_iteration_isolation() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("file1.txt"), "content1").unwrap();
fs::write(temp.path().join("file2.txt"), "content2").unwrap();
let output = luff_cmd().current_dir(temp.path()).output().unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("file1.txt"));
assert!(stdout.contains("file2.txt"));
let code_block_count = stdout.matches("```txt").count();
assert_eq!(code_block_count, 2, "Should have exactly 2 files processed");
}
#[test]
fn test_skips_recently_created_empty_files() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("with_content.txt"), "some content").unwrap();
fs::write(temp.path().join("empty_recent.txt"), "").unwrap();
let output = luff_cmd()
.current_dir(temp.path())
.env("LUFF_OUTPUT_PROTECTION_MS", "5000")
.output()
.unwrap();
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
stdout.contains("with_content.txt"),
"Should include files with content"
);
assert!(
!stdout.contains("empty_recent.txt"),
"Should skip recently created empty files (shell redirection protection)"
);
}
#[test]
fn test_streaming_output_file_exclusion() {
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("file1.txt"), "content1").unwrap();
let output_path = temp.path().join("output.txt");
let output_file = std::fs::File::create(&output_path).unwrap();
let status = std::process::Command::new(assert_cmd::cargo::cargo_bin!("luff"))
.current_dir(temp.path())
.stdout(output_file)
.status()
.unwrap();
assert!(status.success());
let output_content = fs::read_to_string(&output_path).unwrap();
assert!(output_content.contains("file1.txt"));
assert!(
!output_content.contains("output.txt"),
"Output file should not contain itself"
);
}