use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tempfile::TempDir;
fn lz4_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_lz4"))
}
fn setup_input(content: &[u8]) -> (TempDir, PathBuf) {
let dir = TempDir::new().expect("TempDir::new");
let input = dir.path().join("input.txt");
fs::write(&input, content).expect("write input");
(dir, input)
}
fn compress_file(input: &PathBuf) -> PathBuf {
let output = input.with_extension("txt.lz4");
let status = Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap(), output.to_str().unwrap()])
.status()
.expect("spawn lz4");
assert!(status.success(), "compression failed: {status}");
output
}
#[test]
fn help_flag_exits_zero() {
let status = Command::new(lz4_bin())
.arg("--help")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 --help");
assert_eq!(status.code(), Some(0));
}
#[test]
fn version_flag_exits_zero() {
let status = Command::new(lz4_bin())
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 --version");
assert_eq!(status.code(), Some(0));
}
#[test]
fn version_output_contains_version_string() {
let output = Command::new(lz4_bin())
.arg("--version")
.output()
.expect("spawn lz4 --version");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1.10.0") || stdout.contains("lz4") || stdout.contains("LZ4"),
"unexpected version output: {stdout}"
);
}
#[test]
fn compress_single_file_explicit_output() {
let (_dir, input) = setup_input(b"hello world compress test");
let output = input.with_extension("txt.lz4");
let status = Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap(), output.to_str().unwrap()])
.status()
.expect("spawn lz4");
assert!(status.success());
assert!(output.exists(), ".lz4 output file must exist");
assert!(
output.metadata().unwrap().len() > 0,
".lz4 file must be non-empty"
);
}
#[test]
fn compress_exit_code_zero_on_success() {
let (_dir, input) = setup_input(b"exit code test");
let output = input.with_extension("txt.lz4");
let status = Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap(), output.to_str().unwrap()])
.status()
.expect("spawn");
assert_eq!(status.code(), Some(0));
}
#[test]
fn decompress_single_file_explicit_output() {
let (_dir, input) = setup_input(b"decompress test content");
let compressed = compress_file(&input);
let recovered = compressed.with_file_name("recovered.txt");
let status = Command::new(lz4_bin())
.args([
"-d",
"-f",
compressed.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.status()
.expect("spawn lz4 -d");
assert!(status.success());
assert_eq!(
fs::read(&recovered).expect("read recovered"),
b"decompress test content"
);
}
#[test]
fn decompress_exit_code_zero_on_success() {
let (_dir, input) = setup_input(b"exit code test for decompress");
let compressed = compress_file(&input);
let recovered = compressed.with_file_name("recovered2.txt");
let status = Command::new(lz4_bin())
.args([
"-d",
"-f",
compressed.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.status()
.expect("spawn");
assert_eq!(status.code(), Some(0));
}
#[test]
fn compress_decompress_round_trip_small() {
let original = b"The quick brown fox jumps over the lazy dog.";
let (_dir, input) = setup_input(original);
let compressed = compress_file(&input);
let recovered = compressed.with_file_name("round_trip.txt");
Command::new(lz4_bin())
.args([
"-d",
"-f",
compressed.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.status()
.expect("decompress")
.success()
.then_some(())
.expect("decompress succeeded");
assert_eq!(fs::read(&recovered).expect("read recovered"), original);
}
#[test]
fn compress_decompress_round_trip_binary_data() {
let original: Vec<u8> = (0u8..=255).cycle().take(1024).collect();
let (_dir, input) = setup_input(&original);
let compressed = compress_file(&input);
let recovered = compressed.with_file_name("round_trip_bin.txt");
Command::new(lz4_bin())
.args([
"-d",
"-f",
compressed.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.status()
.expect("decompress");
assert_eq!(fs::read(&recovered).expect("read recovered"), original);
}
#[test]
fn compress_decompress_round_trip_empty_file() {
let (_dir, input) = setup_input(b"");
let compressed = compress_file(&input);
let recovered = compressed.with_file_name("round_trip_empty.txt");
Command::new(lz4_bin())
.args([
"-d",
"-f",
compressed.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.status()
.expect("decompress");
assert_eq!(fs::read(&recovered).expect("read recovered"), b"");
}
#[test]
fn test_mode_valid_archive_exits_zero() {
let (_dir, input) = setup_input(b"test mode data");
let compressed = compress_file(&input);
let status = Command::new(lz4_bin())
.args(["-t", compressed.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 -t");
assert_eq!(
status.code(),
Some(0),
"lz4 -t on valid archive must exit 0"
);
}
#[test]
fn test_mode_long_flag_valid_archive() {
let (_dir, input) = setup_input(b"test mode long flag");
let compressed = compress_file(&input);
let status = Command::new(lz4_bin())
.args(["--test", compressed.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 --test");
assert_eq!(status.code(), Some(0));
}
#[test]
fn test_mode_does_not_create_output_file() {
let (_dir, input) = setup_input(b"no output file test");
let compressed = compress_file(&input);
let parent = compressed.parent().unwrap();
let before: std::collections::HashSet<_> = fs::read_dir(parent)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
Command::new(lz4_bin())
.args(["-t", compressed.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn");
let after: std::collections::HashSet<_> = fs::read_dir(parent)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(before, after, "test mode must not create any new files");
}
#[test]
fn test_mode_corrupt_archive_exits_nonzero() {
let dir = TempDir::new().unwrap();
let corrupt = dir.path().join("corrupt.lz4");
fs::write(&corrupt, b"\x04\x22\x4d\x18\x00").unwrap();
let status = Command::new(lz4_bin())
.args(["-t", corrupt.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 -t corrupt");
assert_ne!(status.code(), Some(0), "corrupt archive must exit non-zero");
}
#[test]
fn pipe_compress_stdin_to_stdout() {
let mut child = Command::new(lz4_bin())
.args(["-c", "-"]) .stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn lz4 pipe");
child
.stdin
.as_mut()
.unwrap()
.write_all(b"pipe compress test data")
.unwrap();
let output = child.wait_with_output().expect("wait");
assert!(output.status.success(), "pipe compress must succeed");
assert!(
!output.stdout.is_empty(),
"compressed output must not be empty"
);
}
#[test]
fn pipe_compress_then_decompress_roundtrip() {
let original = b"pipe round-trip test data 12345";
let mut compress_child = Command::new(lz4_bin())
.args(["-c", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn compress");
compress_child
.stdin
.as_mut()
.unwrap()
.write_all(original)
.unwrap();
drop(compress_child.stdin.take());
let compress_out = compress_child.wait_with_output().expect("wait compress");
assert!(compress_out.status.success());
let mut decompress_child = Command::new(lz4_bin())
.args(["-d", "-c", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn decompress");
decompress_child
.stdin
.as_mut()
.unwrap()
.write_all(&compress_out.stdout)
.unwrap();
drop(decompress_child.stdin.take());
let decompress_out = decompress_child
.wait_with_output()
.expect("wait decompress");
assert!(decompress_out.status.success());
assert_eq!(decompress_out.stdout, original);
}
#[test]
fn compress_multiple_files() {
let dir = TempDir::new().unwrap();
let file1 = dir.path().join("a.txt");
let file2 = dir.path().join("b.txt");
fs::write(&file1, b"file one").unwrap();
fs::write(&file2, b"file two").unwrap();
let status = Command::new(lz4_bin())
.args(["-m", "-f", file1.to_str().unwrap(), file2.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn multiple inputs");
assert!(status.success());
assert!(
dir.path().join("a.txt.lz4").exists(),
"a.txt.lz4 must be created"
);
assert!(
dir.path().join("b.txt.lz4").exists(),
"b.txt.lz4 must be created"
);
}
#[test]
fn decompress_multiple_files() {
let dir = TempDir::new().unwrap();
let file1 = dir.path().join("c.txt");
let file2 = dir.path().join("d.txt");
fs::write(&file1, b"data one").unwrap();
fs::write(&file2, b"data two").unwrap();
Command::new(lz4_bin())
.args(["-f", file1.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("compress 1");
Command::new(lz4_bin())
.args(["-f", file2.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("compress 2");
let lz4_1 = dir.path().join("c.txt.lz4");
let lz4_2 = dir.path().join("d.txt.lz4");
fs::remove_file(&file1).unwrap();
fs::remove_file(&file2).unwrap();
let status = Command::new(lz4_bin())
.args([
"-d",
"-m",
"-f",
lz4_1.to_str().unwrap(),
lz4_2.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("decompress multiple");
assert!(status.success());
assert_eq!(fs::read(&file1).unwrap(), b"data one");
assert_eq!(fs::read(&file2).unwrap(), b"data two");
}
#[test]
fn list_mode_exits_zero_for_valid_archive() {
let (_dir, input) = setup_input(b"list mode test data");
let compressed = compress_file(&input);
let status = Command::new(lz4_bin())
.args(["--list", compressed.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 --list");
assert_eq!(status.code(), Some(0));
}
#[test]
fn legacy_format_flag_produces_valid_compressed_file() {
let original = b"legacy format test content";
let (_dir, input) = setup_input(original);
let output = input.with_extension("txt.lz4");
let status = Command::new(lz4_bin())
.args([
"-l",
"-f",
input.to_str().unwrap(),
output.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 -l (legacy format)");
assert!(status.success(), "legacy format compress must succeed");
assert!(output.exists(), "legacy .lz4 output file must exist");
}
#[test]
fn list_mode_produces_output() {
let (_dir, input) = setup_input(b"list output test");
let compressed = compress_file(&input);
let output = Command::new(lz4_bin())
.args(["--list", compressed.to_str().unwrap()])
.output()
.expect("spawn lz4 --list");
let combined = [output.stdout.as_slice(), output.stderr.as_slice()].concat();
assert!(!combined.is_empty(), "--list must produce output");
}
#[test]
fn decompress_non_lz4_file_without_output_flag_exits_nonzero() {
let (_dir, input) = setup_input(b"this is not lz4");
let status = Command::new(lz4_bin())
.args(["-d", input.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn lz4 -d non-lz4");
assert_ne!(
status.code(),
Some(0),
"decompressing a non-.lz4 file without specifying output must fail"
);
}
#[test]
fn compress_nonexistent_input_exits_nonzero() {
let status = Command::new(lz4_bin())
.args(["/tmp/lz4-test-nonexistent-input-file-xyz.txt"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn");
assert_ne!(status.code(), Some(0));
}
#[test]
fn force_overwrite_replaces_existing_output() {
let (_dir, input) = setup_input(b"overwrite test content");
let output = input.with_extension("txt.lz4");
Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap(), output.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("first compress");
assert!(output.exists());
let status = Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap(), output.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("second compress");
assert!(
status.success(),
"-f must allow overwriting existing output"
);
}
#[test]
fn high_compression_level_produces_valid_output() {
let original = b"high compression level test data repeated repeated repeated";
let (_dir, input) = setup_input(original);
let output = input.with_extension("txt.lz4");
Command::new(lz4_bin())
.args([
"-9",
"-f",
input.to_str().unwrap(),
output.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("compress -9");
let recovered = input.with_extension("txt.recovered");
Command::new(lz4_bin())
.args([
"-d",
"-f",
output.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("decompress");
assert_eq!(fs::read(&recovered).unwrap(), original);
}
#[test]
fn fast_mode_flag_produces_valid_output() {
let original = b"fast mode test";
let (_dir, input) = setup_input(original);
let output = input.with_extension("txt.lz4");
Command::new(lz4_bin())
.args([
"-1",
"-f",
input.to_str().unwrap(),
output.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("compress -1");
assert!(output.exists());
let recovered = input.with_extension("txt.fast");
Command::new(lz4_bin())
.args([
"-d",
"-f",
output.to_str().unwrap(),
recovered.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("decompress");
assert_eq!(fs::read(&recovered).unwrap(), original);
}
#[test]
fn auto_compress_output_filename_adds_lz4_extension() {
let (_dir, input) = setup_input(b"auto filename compress");
let expected_output = input.with_extension("txt.lz4");
let _ = fs::remove_file(&expected_output);
let status = Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn");
assert!(status.success());
assert!(
expected_output.exists(),
"auto compress must create {expected_output:?}"
);
}
#[test]
fn auto_decompress_output_filename_strips_lz4_extension() {
let (_dir, input) = setup_input(b"auto filename decompress");
let compressed = input.with_extension("txt.lz4");
Command::new(lz4_bin())
.args(["-f", input.to_str().unwrap(), compressed.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("compress");
fs::remove_file(&input).unwrap();
let status = Command::new(lz4_bin())
.args(["-d", "-f", compressed.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn decompress auto");
assert!(status.success());
assert!(
input.exists(),
"auto decompress must create {input:?} (stripped .lz4)"
);
}
#[test]
fn remove_source_flag_deletes_input_after_compress() {
let (_dir, input) = setup_input(b"remove source test");
let expected_output = input.with_extension("txt.lz4");
let status = Command::new(lz4_bin())
.args(["--rm", "-f", input.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn --rm");
assert!(status.success());
assert!(expected_output.exists(), "output .lz4 must exist");
assert!(
!input.exists(),
"--rm must delete the source file after compress"
);
}
#[test]
fn nb_workers_greater_than_one_does_not_crash() {
let (_dir, input) = setup_input(b"multithread test");
let output = input.with_extension("txt.lz4");
let status = Command::new(lz4_bin())
.args([
"--workers=2",
"-f",
input.to_str().unwrap(),
output.to_str().unwrap(),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn --workers=2");
assert!(
status.code().is_some(),
"process must exit normally, not via signal"
);
}