use std::io::Write;
use std::process::Command;
use tempfile::NamedTempFile;
fn create_test_file(content: &str, extension: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(extension)
.tempfile()
.unwrap();
file.write_all(content.as_bytes()).unwrap();
file
}
fn run_batless(args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_batless"))
.args(args)
.output()
.expect("Failed to execute batless")
}
#[test]
fn test_help_command() {
let output = run_batless(&["--help"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("A fast, non-blocking code and text viewer"));
assert!(stdout.contains("--mode <MODE>"));
assert!(stdout.contains("--max-lines"));
}
#[test]
fn test_version_command() {
let output = run_batless(&["--version"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("batless"));
}
#[test]
fn test_version_json_command() {
let output = run_batless(&["--version-json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).expect("valid json");
assert_eq!(json["name"], "batless");
assert_eq!(json["version"], env!("CARGO_PKG_VERSION"));
assert!(json["git_hash"].is_string());
assert_ne!(json["git_hash"].as_str().unwrap_or("unknown"), "unknown");
assert!(json["build_timestamp"].is_string());
assert_ne!(
json["build_timestamp"].as_str().unwrap_or("unknown"),
"unknown"
);
}
#[test]
fn test_plain_mode() {
let content = "fn main() {\n println!(\"Hello, world!\");\n}\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=plain"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("fn main()"));
assert!(stdout.contains("println!"));
}
#[test]
fn test_plain_output() {
let content = "fn main() {\n println!(\"Hello, world!\");\n}\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=plain"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("main"));
assert!(stdout.contains("println"));
}
#[test]
fn test_json_mode() {
let content = "fn main() {\n println!(\"Hello, world!\");\n}\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["mode"], "json");
assert_eq!(json["language"], "Rust");
assert_eq!(
json["processed_lines"]
.as_u64()
.expect("processed_lines should be numeric"),
json["lines"]
.as_array()
.expect("lines should be an array")
.len() as u64
);
assert!(json["lines"].is_array());
assert!(json["total_lines"].is_number());
assert!(json["total_lines_exact"].is_boolean());
assert!(json["total_bytes"].is_number());
}
#[test]
fn test_json_pretty_flag_changes_formatting() {
let content = "fn main() { println!(\"Hello\"); }\n";
let file = create_test_file(content, ".rs");
let compact = run_batless(&[file.path().to_str().unwrap(), "--mode=json"]);
assert!(compact.status.success());
let compact_out = String::from_utf8(compact.stdout).unwrap();
assert!(!compact_out.contains("\n \"file\""));
let pretty = run_batless(&[
file.path().to_str().unwrap(),
"--mode=json",
"--json-pretty",
]);
assert!(pretty.status.success());
let pretty_out = String::from_utf8(pretty.stdout).unwrap();
assert!(pretty_out.contains("\n \"file\""));
assert!(pretty_out.len() > compact_out.len());
}
#[test]
fn test_max_lines_limit() {
let content = "line 1\nline 2\nline 3\nline 4\nline 5\n";
let file = create_test_file(content, ".txt");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--mode=plain",
"--max-lines=3",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4);
assert!(lines[3].contains("Output truncated after 3 lines"));
}
#[test]
fn test_max_bytes_limit() {
let content = "Short line 1\nShort line 2\nShort line 3\nShort line 4\n";
let file = create_test_file(content, ".txt");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--mode=plain",
"--max-bytes=25",
"--max-lines=100", ]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Output truncated after") && stdout.contains("bytes"));
}
#[test]
fn test_language_detection() {
let python_content = "def hello():\n print('Hello, world!')\n";
let file = create_test_file(python_content, ".py");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["language"], "Python");
}
#[test]
fn test_explicit_language() {
let content = "function hello() {\n console.log('Hello');\n}\n";
let file = create_test_file(content, ".unknown");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--language=javascript",
"--mode=json",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["language"], "javascript");
}
#[test]
fn test_plain_no_ansi() {
let content = "fn main() {\n println!(\"Hello\");\n}\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=plain"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(!stdout.contains("\x1b["));
}
#[test]
fn test_strip_ansi() {
let content = "fn main() {\n println!(\"Hello\");\n}\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--mode=plain",
"--strip-ansi",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("fn main()"));
}
#[test]
fn test_empty_file() {
let file = create_test_file("", ".txt");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["total_lines"], 0);
assert_eq!(json["total_bytes"], 0);
assert_eq!(json["truncated"], false);
}
#[test]
fn test_nonexistent_file() {
let output = run_batless(&["/nonexistent/file.txt", "--mode=plain"]);
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(
stderr.contains("No such file")
|| stderr.contains("not found")
|| stderr.contains("cannot find")
|| stderr.contains("system cannot find")
);
}
#[test]
fn test_multiple_options_combined() {
let content = "def func1():\n pass\n\ndef func2():\n pass\n\ndef func3():\n pass\n";
let file = create_test_file(content, ".py");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--mode=json",
"--max-lines=2",
"--language=python",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["language"], "python");
assert_eq!(json["processed_lines"], 2);
assert!(json["total_lines"].as_i64().unwrap() >= json["processed_lines"].as_i64().unwrap());
assert_eq!(json["truncated"], true);
assert_eq!(json["lines"].as_array().unwrap().len(), 2);
}
#[test]
fn test_list_languages() {
let output = run_batless(&["--list-languages"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Rust"));
assert!(stdout.contains("Python"));
assert!(stdout.contains("JavaScript"));
}
#[test]
fn test_total_lines_exact_flag() {
let content = "line1\nline2\nline3\nline4\nline5\n";
let file = create_test_file(content, ".txt");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--mode=json",
"--max-lines=2",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["truncated_by_lines"], true);
assert_eq!(json["total_lines_exact"], false);
}
#[test]
fn test_shell_completion_generation_bash() {
let output = run_batless(&["--generate-completions", "bash"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("_batless()"));
assert!(stdout.contains("COMPREPLY=()"));
}
#[test]
fn test_shell_completion_generation_zsh() {
let output = run_batless(&["--generate-completions", "zsh"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("#compdef batless"));
}
#[test]
fn test_shell_completion_generation_fish() {
let output = run_batless(&["--generate-completions", "fish"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("complete -c batless"));
}
#[test]
fn test_shell_completion_generation_powershell() {
let output = run_batless(&["--generate-completions", "power-shell"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Register-ArgumentCompleter"));
}
#[test]
fn test_stdin_processing() {
let output = Command::new("sh")
.arg("-c")
.arg("echo 'test line 1\\ntest line 2' | cargo run -- --mode=json")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("test line 1"));
assert!(stdout.contains("test line 2"));
assert!(
stdout.contains("file\": \"-") || stdout.contains("file\":\"-"),
"Expected file path '-' marker in JSON output, got: {stdout}"
);
}
#[test]
fn test_stdin_processing_with_max_lines() {
let output = Command::new("sh")
.arg("-c")
.arg("printf 'line1\\nline2\\nline3\\nline4\\nline5\\n' | cargo run -- --mode=json --max-lines=3")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("line1"));
assert!(stdout.contains("line3"));
assert!(!stdout.contains("line4"), "line4 should be truncated");
assert!(
stdout.contains("\"truncated\": true") || stdout.contains("\"truncated\":true"),
"Expected truncated flag in JSON output"
);
}
#[test]
fn test_stdin_processing_with_max_bytes() {
let output = Command::new("sh")
.arg("-c")
.arg("printf 'short\\nthis is a longer line\\nanother line\\n' | cargo run -- --mode=json --max-bytes=100 --max-lines=1")
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("short"));
assert!(
!stdout.contains("another line"),
"Content beyond max-lines should be truncated"
);
assert!(
stdout.contains("\"truncated\": true") || stdout.contains("\"truncated\":true"),
"Expected truncated flag in JSON output"
);
}
#[test]
fn test_invalid_language_error() {
let output = run_batless(&["src/main.rs", "--language=invalid-language"]);
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("Language 'invalid-language' not found"));
assert!(stderr.contains("E203"));
}
#[test]
fn test_once_lock_performance() {
let content = "fn main() { println!(\"test\"); }";
let file = create_test_file(content, ".rs");
for _ in 0..3 {
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=plain"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("fn main()"));
}
}
#[test]
fn test_error_handling_with_suggestions() {
let output = run_batless(&["nonexistent_file.txt"]);
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("File not found"));
assert!(stderr.contains("E101"));
}
#[test]
fn test_large_file_processing() {
let large_content = "fn test_function() {\n println!(\"line\");\n}\n".repeat(100);
let file = create_test_file(&large_content, ".rs");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--mode=json",
"--max-lines=50",
]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(json["truncated"].as_bool().unwrap_or(false));
}
#[test]
fn test_configuration_validation_edge_cases() {
let content = "test";
let file = create_test_file(content, ".txt");
let output = run_batless(&[file.path().to_str().unwrap(), "--max-lines=0"]);
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("E302") || stderr.contains("Configuration error"));
}
#[test]
fn test_language_validation() {
let output = run_batless(&["--list-languages"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Rust"));
assert!(stdout.contains("Python"));
}
#[test]
fn test_compatibility_flags() {
let content = "line1\nline2\nline3\n";
let file = create_test_file(content, ".txt");
let output = run_batless(&[file.path().to_str().unwrap(), "--plain"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert_eq!(stdout.trim(), content.trim());
let output = run_batless(&[file.path().to_str().unwrap(), "--number", "--mode=plain"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("line1") && stdout.contains("line2") && stdout.contains("line3"));
}
#[test]
fn test_encoding_detection_robustness() {
let test_cases = vec![
("ASCII content", ".txt"),
("UTF-8 content with émojis 🦀", ".txt"),
("Binary-like content \x00\x01\x02", ".bin"),
];
for (content, ext) in test_cases {
let file = create_test_file(content, ext);
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=json"]);
if output.status.success() {
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(json["encoding"].is_string());
}
}
}
#[test]
fn test_strip_blank_lines() {
let content = "def foo():\n pass\n\n\ndef bar():\n pass\n";
let file = create_test_file(content, ".py");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--strip-blank-lines",
"--mode=plain",
]);
assert!(
output.status.success(),
"batless --strip-blank-lines should succeed"
);
let stdout = String::from_utf8(output.stdout).unwrap();
let has_consecutive_blanks = stdout.contains("\n\n");
assert!(
!has_consecutive_blanks,
"Output should not contain consecutive blank lines after --strip-blank-lines"
);
assert!(
stdout.contains("def foo():"),
"def foo(): should be retained"
);
assert!(
stdout.contains("def bar():"),
"def bar(): should be retained"
);
}
#[test]
fn test_strip_comments() {
let content = "# this is a comment\ndef foo():\n # inline comment\n pass\n";
let file = create_test_file(content, ".py");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--strip-comments",
"--mode=plain",
]);
assert!(
output.status.success(),
"batless --strip-comments should succeed"
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
!stdout.contains("# this is a comment"),
"Top-level comment should be stripped"
);
assert!(
!stdout.contains("# inline comment"),
"Inline comment line should be stripped"
);
assert!(
stdout.contains("def foo():"),
"Function definition should be retained"
);
}
#[test]
fn test_strip_compression_ratio_in_json() {
let content = "# comment\ndef foo():\n pass\n\n# comment2\n";
let file = create_test_file(content, ".py");
let output = run_batless(&[
file.path().to_str().unwrap(),
"--strip-comments",
"--strip-blank-lines",
"--mode=json",
]);
assert!(
output.status.success(),
"batless --strip-comments --strip-blank-lines --mode=json should succeed"
);
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("Output should be valid JSON");
assert!(
json.get("compression_ratio").is_some(),
"JSON output should contain compression_ratio field"
);
let ratio = json["compression_ratio"]
.as_f64()
.expect("compression_ratio should be a float");
assert!(
ratio > 1.0,
"compression_ratio should be > 1.0 when content was stripped, got {ratio}"
);
}
#[test]
fn test_mode_index_rust() {
let content = "pub fn foo() -> i32 { 1 }\nstruct Bar { x: i32 }\npub fn baz() {}\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=index"]);
assert!(
output.status.success(),
"batless --mode=index should succeed for a Rust file"
);
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("--mode=index output should be valid JSON");
assert!(
json["symbols"].is_array(),
"JSON output should contain a symbols array"
);
let symbol_count = json["symbol_count"]
.as_u64()
.expect("symbol_count should be a number");
assert!(symbol_count > 0, "symbol_count should be > 0");
let symbols = json["symbols"].as_array().unwrap();
assert!(
symbols
.iter()
.any(|s| s["kind"].as_str().is_some_and(|k| k == "function")),
"At least one symbol should have kind == function"
);
assert!(
symbols.iter().any(|s| s["name"].as_str() == Some("foo")),
"At least one symbol should have name == foo"
);
assert!(
symbols.iter().all(|s| s.get("line_start").is_some()),
"All symbols should have a line_start field"
);
}
#[test]
fn test_mode_index_has_file_metadata() {
let content = "pub fn main() {}\nstruct Config { value: i32 }\n";
let file = create_test_file(content, ".rs");
let output = run_batless(&[file.path().to_str().unwrap(), "--mode=index"]);
assert!(
output.status.success(),
"batless --mode=index should succeed"
);
let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("--mode=index output should be valid JSON");
assert!(
json.get("file").is_some(),
"JSON output should contain file field"
);
assert!(
json.get("language").is_some(),
"JSON output should contain language field"
);
assert!(
json["total_lines"].is_number(),
"JSON output should contain total_lines field"
);
assert!(
json["symbol_count"].is_number(),
"JSON output should contain symbol_count field"
);
assert_eq!(
json["mode"].as_str(),
Some("index"),
"mode field should equal index"
);
}