#![cfg(feature = "factory")]
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
const BIN: &str = env!("CARGO_BIN_EXE_compcol");
struct Scratch {
path: PathBuf,
}
impl Scratch {
fn new(label: &str) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("compcol-test-{pid}-{n}-{label}"));
fs::create_dir_all(&path).expect("create scratch");
Self { path }
}
fn file(&self, name: &str) -> PathBuf {
self.path.join(name)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn run_with_stdin(args: &[&str], stdin: &[u8]) -> (Vec<u8>, Vec<u8>, i32) {
let mut child = Command::new(BIN)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn compcol");
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin)
.expect("write stdin");
drop(child.stdin.take());
let mut out = Vec::new();
let mut err = Vec::new();
child
.stdout
.as_mut()
.unwrap()
.read_to_end(&mut out)
.unwrap();
child
.stderr
.as_mut()
.unwrap()
.read_to_end(&mut err)
.unwrap();
let status = child.wait().expect("wait");
let code = status.code().unwrap_or(-1);
(out, err, code)
}
fn run(args: &[&str]) -> (Vec<u8>, Vec<u8>, i32) {
let child = Command::new(BIN)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn compcol");
let out = child.wait_with_output().expect("wait");
(out.stdout, out.stderr, out.status.code().unwrap_or(-1))
}
fn file_contents(p: &Path) -> Vec<u8> {
fs::read(p).expect("read")
}
#[test]
fn help_prints_usage_and_exits_zero() {
let (out, _err, code) = run(&["--help"]);
assert_eq!(code, 0);
let s = String::from_utf8_lossy(&out);
assert!(s.contains("Usage:"), "{}", s);
assert!(s.contains("--decompress"), "{}", s);
}
#[test]
fn version_prints_pkg_version() {
let (out, _err, code) = run(&["--version"]);
assert_eq!(code, 0);
assert!(String::from_utf8_lossy(&out).starts_with("compcol "));
}
#[test]
fn list_includes_compiled_algorithms() {
let (out, _err, code) = run(&["--list"]);
assert_eq!(code, 0);
let s = String::from_utf8(out).unwrap();
for name in ["rle", "deflate", "zlib", "gzip"] {
assert!(s.contains(name), "expected '{name}' in list output:\n{s}");
}
}
#[test]
fn missing_type_is_usage_error() {
let (_out, err, code) = run_with_stdin(&[], b"hello");
assert_eq!(code, 2);
assert!(String::from_utf8_lossy(&err).contains("-t ALGO is required"));
}
#[test]
fn unknown_type_is_usage_error() {
let (_out, err, code) = run_with_stdin(&["-t", "bogus", "-c"], b"hello");
assert_eq!(code, 2);
assert!(
String::from_utf8_lossy(&err).contains("unknown algorithm"),
"{}",
String::from_utf8_lossy(&err)
);
}
#[test]
fn unknown_flag_is_usage_error() {
let (_out, err, code) = run_with_stdin(&["--banana"], b"");
assert_eq!(code, 2);
assert!(String::from_utf8_lossy(&err).contains("unknown option"));
}
#[test]
fn pipe_round_trip_gzip() {
let input = b"The quick brown fox jumps over the lazy dog. ".repeat(20);
let (encoded, _err, code) = run_with_stdin(&["-t", "gzip"], &input);
assert_eq!(code, 0);
assert_eq!(&encoded[..2], &[0x1F, 0x8B]);
let (decoded, _err, code) = run_with_stdin(&["-t", "gzip", "-d"], &encoded);
assert_eq!(code, 0);
assert_eq!(decoded, input);
}
#[test]
fn pipe_round_trip_zlib() {
let input = b"compress me through zlib".to_vec();
let (encoded, _err, code) = run_with_stdin(&["-t", "zlib"], &input);
assert_eq!(code, 0);
assert_eq!(encoded[0], 0x78); let (decoded, _err, code) = run_with_stdin(&["-t", "zlib", "-d"], &encoded);
assert_eq!(code, 0);
assert_eq!(decoded, input);
}
#[test]
fn pipe_round_trip_deflate() {
let input = b"raw deflate stream payload bytes".to_vec();
let (encoded, _err, code) = run_with_stdin(&["-t", "deflate"], &input);
assert_eq!(code, 0);
let (decoded, _err, code) = run_with_stdin(&["-t", "deflate", "-d"], &encoded);
assert_eq!(code, 0);
assert_eq!(decoded, input);
}
#[test]
fn pipe_round_trip_rle() {
let input = b"aaabbbcccdddeeee".to_vec();
let (encoded, _err, code) = run_with_stdin(&["-t", "rle"], &input);
assert_eq!(code, 0);
let (decoded, _err, code) = run_with_stdin(&["-t", "rle", "-d"], &encoded);
assert_eq!(code, 0);
assert_eq!(decoded, input);
}
#[test]
fn in_place_compress_removes_input() {
let s = Scratch::new("inplace_compress");
let input = s.file("data.txt");
fs::write(&input, b"hello in place world\n").unwrap();
let (_out, err, code) = run(&["-t", "gzip", input.to_str().unwrap()]);
assert_eq!(code, 0, "stderr: {}", String::from_utf8_lossy(&err));
assert!(!input.exists(), "input should have been removed");
let gz = s.file("data.txt.gz");
assert!(gz.exists(), "output {} not created", gz.display());
let (_out, err, code) = run(&["-t", "gzip", "-d", gz.to_str().unwrap()]);
assert_eq!(code, 0, "stderr: {}", String::from_utf8_lossy(&err));
assert!(!gz.exists(), "compressed input should have been removed");
assert_eq!(file_contents(&input), b"hello in place world\n");
}
#[test]
fn keep_preserves_input() {
let s = Scratch::new("keep");
let input = s.file("kept.txt");
fs::write(&input, b"keep me").unwrap();
let (_out, err, code) = run(&["-t", "gzip", "-k", input.to_str().unwrap()]);
assert_eq!(code, 0, "stderr: {}", String::from_utf8_lossy(&err));
assert!(input.exists(), "-k should have preserved input");
assert!(s.file("kept.txt.gz").exists());
}
#[test]
fn stdout_flag_keeps_input_and_writes_to_stdout() {
let s = Scratch::new("stdout_flag");
let input = s.file("a.txt");
fs::write(&input, b"stdout please").unwrap();
let (out, err, code) = run(&["-t", "gzip", "-c", input.to_str().unwrap()]);
assert_eq!(code, 0, "stderr: {}", String::from_utf8_lossy(&err));
assert!(input.exists(), "-c must not remove input");
assert_eq!(&out[..2], &[0x1F, 0x8B]);
}
#[test]
fn output_flag_writes_to_specified_path() {
let s = Scratch::new("oflag");
let input = s.file("src.txt");
let dst = s.file("out/elsewhere.bin");
fs::create_dir_all(s.file("out")).unwrap();
fs::write(&input, b"to elsewhere").unwrap();
let (_out, err, code) = run(&[
"-t",
"gzip",
"-o",
dst.to_str().unwrap(),
input.to_str().unwrap(),
]);
assert_eq!(code, 0, "stderr: {}", String::from_utf8_lossy(&err));
assert!(input.exists(), "-o must not remove input");
assert!(dst.exists(), "-o destination missing");
assert_eq!(&file_contents(&dst)[..2], &[0x1F, 0x8B]);
}
#[test]
fn refuses_to_overwrite_existing_output() {
let s = Scratch::new("overwrite_no_f");
let input = s.file("z.txt");
fs::write(&input, b"data").unwrap();
fs::write(s.file("z.txt.gz"), b"pre-existing").unwrap();
let (_out, err, code) = run(&["-t", "gzip", input.to_str().unwrap()]);
assert_eq!(code, 2);
assert!(
String::from_utf8_lossy(&err).contains("output exists"),
"{}",
String::from_utf8_lossy(&err)
);
assert_eq!(file_contents(&s.file("z.txt.gz")), b"pre-existing");
assert!(input.exists());
}
#[test]
fn force_overwrites_existing_output() {
let s = Scratch::new("overwrite_force");
let input = s.file("z.txt");
fs::write(&input, b"new data").unwrap();
fs::write(s.file("z.txt.gz"), b"old").unwrap();
let (_out, err, code) = run(&["-t", "gzip", "-f", input.to_str().unwrap()]);
assert_eq!(code, 0, "stderr: {}", String::from_utf8_lossy(&err));
let new = file_contents(&s.file("z.txt.gz"));
assert_eq!(&new[..2], &[0x1F, 0x8B]);
assert!(!input.exists());
}
#[test]
fn decompress_requires_matching_extension_in_inplace_mode() {
let s = Scratch::new("dec_ext");
let bogus = s.file("data.txt"); fs::write(&bogus, b"\x1f\x8b\x08\x00").unwrap(); let (_out, err, code) = run(&["-t", "gzip", "-d", bogus.to_str().unwrap()]);
assert_eq!(code, 2);
assert!(
String::from_utf8_lossy(&err).contains("doesn't end with"),
"{}",
String::from_utf8_lossy(&err)
);
}
#[test]
fn long_options_with_equals() {
let input = b"hello".repeat(100);
let (encoded, _err, code) = run_with_stdin(&["--type=gzip"], &input);
assert_eq!(code, 0);
let (decoded, _err, code) = run_with_stdin(&["--type=gzip", "--decompress"], &encoded);
assert_eq!(code, 0);
assert_eq!(decoded, input);
}
#[test]
fn short_t_with_attached_value() {
let input = b"hi";
let (encoded, _err, code) = run_with_stdin(&["-tgzip"], input);
assert_eq!(code, 0);
assert_eq!(&encoded[..2], &[0x1F, 0x8B]);
}
#[test]
fn output_decompresses_with_system_gunzip() {
if Command::new("gunzip")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| !s.success())
.unwrap_or(true)
{
println!("skipping: gunzip not available");
return;
}
let input = b"Mary had a little lamb. ".repeat(200);
let (encoded, _err, code) = run_with_stdin(&["-t", "gzip"], &input);
assert_eq!(code, 0);
let mut child = Command::new("gunzip")
.arg("-c")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(&encoded).unwrap();
drop(child.stdin.take());
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
assert_eq!(out.stdout, input);
}