#![cfg(feature = "cli")]
use assert_cmd::Command;
use std::io::Write;
use std::path::PathBuf;
fn fixture_patch() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/patches/H2024.05.31.0000.0000j.patch")
}
fn run_dump(args: &[&str]) -> std::process::Output {
Command::cargo_bin("zipatch")
.unwrap()
.args(args)
.output()
.unwrap()
}
fn parse_summary(stderr: &str) -> (usize, usize) {
let line = stderr
.lines()
.find(|l| l.contains(" chunks, ") && l.contains(" SQPK"))
.unwrap_or_else(|| panic!("no summary line in stderr: {stderr:?}"));
let mut parts = line.split_whitespace();
let chunks: usize = parts.next().unwrap().parse().unwrap();
let _ = parts.next();
let sqpk: usize = parts.next().unwrap().parse().unwrap();
(chunks, sqpk)
}
#[test]
fn dump_full_patch_summary_matches_stdout_line_count() {
let path = fixture_patch();
let output = run_dump(&["dump", path.to_str().unwrap()]);
assert!(
output.status.success(),
"exit code must be 0, got {:?}\nstderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let (chunks, sqpk) = parse_summary(&stderr);
assert_eq!(
stdout.lines().count(),
chunks,
"stdout line count must equal summary chunk count"
);
assert!(chunks > 0, "fixture must contain at least one chunk");
assert!(sqpk > 0 && sqpk <= chunks, "summary counts must be sane");
}
#[test]
fn dump_output_lines_match_expected_format() {
let path = fixture_patch();
let output = run_dump(&["dump", path.to_str().unwrap()]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let first_line = stdout.lines().next().expect("at least one output line");
assert!(
first_line.starts_with('#'),
"each line must start with '#', got: {first_line:?}"
);
assert!(
first_line.contains("FHDR"),
"first chunk must be FHDR, got: {first_line:?}"
);
}
#[test]
fn dump_sqpk_only_matches_sqpk_count_and_drops_non_sqpk() {
let path = fixture_patch();
let full = run_dump(&["dump", path.to_str().unwrap()]);
let (_, sqpk_full) = parse_summary(&String::from_utf8_lossy(&full.stderr));
let filtered = run_dump(&["dump", "--sqpk-only", path.to_str().unwrap()]);
assert!(filtered.status.success());
let stdout = String::from_utf8_lossy(&filtered.stdout);
let stderr = String::from_utf8_lossy(&filtered.stderr);
let (chunks_after, sqpk_after) = parse_summary(&stderr);
assert_eq!(
stdout.lines().count(),
sqpk_full,
"--sqpk-only must print one line per SQPK chunk"
);
assert_eq!(
sqpk_after, sqpk_full,
"summary SQPK count is independent of the filter"
);
assert_eq!(
chunks_after,
sqpk_full + (chunks_after - sqpk_full),
"summary chunk count remains the full stream length"
);
assert!(
!stdout.contains("FHDR") && !stdout.contains("APLY"),
"--sqpk-only must suppress non-SQPK lines"
);
}
#[test]
fn dump_missing_file_exits_nonzero_with_stderr_message() {
let output = run_dump(&["dump", "/nonexistent/path/no.patch"]);
assert!(!output.status.success(), "missing file must exit non-zero");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.starts_with("failed to open"),
"stderr must start with 'failed to open', got: {stderr:?}"
);
}
#[test]
fn dump_truncated_patch_reports_parse_error_chunk_index() {
let magic: [u8; 12] = [
0x91, 0x5A, 0x49, 0x50, 0x41, 0x54, 0x43, 0x48, 0x0D, 0x0A, 0x1A, 0x0A,
];
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(&magic).unwrap();
file.write_all(&[0x00, 0x00, 0x10, 0x00, b'A', b'D', b'I'])
.unwrap();
file.flush().unwrap();
let output = run_dump(&["dump", file.path().to_str().unwrap()]);
assert!(
!output.status.success(),
"truncated patch must exit non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("parse error at chunk #"),
"stderr must report parse error with chunk index, got: {stderr:?}"
);
}
#[test]
fn top_level_help_exits_zero_and_contains_expected_strings() {
let output = run_dump(&["--help"]);
assert!(output.status.success(), "--help must exit 0");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Usage:"));
assert!(stdout.contains("dump"));
}
#[test]
fn dump_subcommand_help_exits_zero_and_contains_expected_strings() {
let output = run_dump(&["dump", "--help"]);
assert!(output.status.success(), "dump --help must exit 0");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Usage:"));
assert!(stdout.contains("--sqpk-only"));
}