//! CLI integration tests for the `mhash` binary. use std::fs; use assert_cmd::Command; use predicates::prelude::*; use serde_json::Value; use tempfile::tempdir; fn mhash_command() -> assert_cmd::Command { Command::cargo_bin("mhash").unwrap_or_else(|error| { panic!("failed to locate mhash binary: {error}"); }) } #[test] fn list_algorithms_includes_openhashtab_matrix() { let mut command = mhash_command(); command.arg("list-algorithms").arg("--json"); command .assert() .success() .stdout(predicate::str::contains("parallelhash256-528")) .stdout(predicate::str::contains("kangarootwelve-264")) .stdout(predicate::str::contains("streebog-512")); } #[test] fn hashes_file_as_json() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); command .arg("--algorithm") .arg("sha256,blake3-256") .arg("--json") .arg(&file); command .assert() .success() .stdout(predicate::str::contains( "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", )) .stdout(predicate::str::contains("blake3-256")); } #[test] fn hashes_file_as_jsonl() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); command .arg("--algorithm") .arg("sha256,md5") .arg("--format") .arg("jsonl") .arg(&file); command .assert() .success() .stdout(predicate::str::contains("\"algorithm\":\"md5\"")) .stdout(predicate::str::contains("\"algorithm\":\"sha256\"")); } #[test] fn recursive_ignore_and_sum_format_hash_only_included_files() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let keep = directory.path().join("keep.txt"); let nested = directory.path().join("nested"); let ignored = nested.join("ignored.skip"); fs::create_dir(&nested).unwrap_or_else(|error| panic!("fixture dir failed: {error}")); fs::write(&keep, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); fs::write(&ignored, b"ignored").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); command .arg("--algorithm") .arg("sha256") .arg("--recursive") .arg("--ignore") .arg("*.skip") .arg("--format") .arg("sum") .arg(directory.path()); command .assert() .success() .stdout(predicate::str::contains( "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", )) .stdout(predicate::str::contains("keep.txt")) .stdout(predicate::str::contains("ignored.skip").not()); } #[test] fn uppercase_sum_output_uses_uppercase_hex() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); command .arg("--algorithm") .arg("sha256") .arg("--uppercase") .arg("--format") .arg("sum") .arg(&file); command.assert().success().stdout(predicate::str::contains( "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD", )); } #[test] fn stdin_lines_input_hashes_existing_paths() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); command .arg("--input-format") .arg("lines") .arg("--algorithm") .arg("md5") .arg("--format") .arg("jsonl") .write_stdin(format!("{}\n", file.display())); command.assert().success().stdout(predicate::str::contains( "\"digest\":\"900150983cd24fb0d6963f7d28e17f72\"", )); } #[test] fn stdin_jsonl_input_hashes_existing_paths() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let input = format!( "{{\"path\":\"{}\"}}\n", file.display().to_string().replace('\\', "\\\\") ); let mut command = mhash_command(); command .arg("--input-format") .arg("jsonl") .arg("--algorithm") .arg("sha1") .arg("--format") .arg("jsonl") .write_stdin(input); command.assert().success().stdout(predicate::str::contains( "\"digest\":\"a9993e364706816aba3e25717850c26c9cd0d89d\"", )); } #[test] fn help_and_json_hash_output_do_not_expose_backend_controls() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); mhash_command() .arg("--help") .assert() .success() .stdout(predicate::str::contains("--backend").not()); let mut command = mhash_command(); let output = command .arg("--algorithm") .arg("sha256") .arg("--io") .arg("read") .arg("--threads") .arg("2") .arg("--json") .arg(&file) .assert() .success() .get_output() .stdout .clone(); let records: Vec = serde_json::from_slice(&output) .unwrap_or_else(|error| panic!("json parse failed: {error}")); assert_eq!(records.len(), 1); assert!(records[0].get("requested_backend").is_none()); assert!(records[0].get("backend").is_none()); assert!(records[0].get("io").is_none()); } #[test] fn backend_option_is_not_public_cli() { let mut command = mhash_command(); command .arg("--backend") .arg("simd") .arg("README.md") .assert() .code(2) .stderr(predicate::str::contains("unsupported argument")); } #[test] fn bench_accepts_threads_and_io_options_as_json_smoke() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); let output = command .arg("bench") .arg("--algorithm") .arg("sha256") .arg("--threads") .arg("2") .arg("--io") .arg("read") .arg("--format") .arg("json") .arg(&file) .assert() .success() .get_output() .stdout .clone(); let records: Vec = serde_json::from_slice(&output) .unwrap_or_else(|error| panic!("json parse failed: {error}")); assert_eq!(records.len(), 1); assert_eq!(records[0]["algorithm"], "sha256"); assert_eq!(records[0]["bytes"], 3); assert!(records[0]["elapsed_ms"].as_f64().is_some()); } #[test] fn bench_end_to_end_reports_repeat_warmup_and_mode() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abcdef").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); let output = command .arg("bench") .arg("--algorithm") .arg("sha256") .arg("--repeat") .arg("2") .arg("--warmup") .arg("1") .arg("--bench-mode") .arg("e2e") .arg("--io") .arg("read") .arg("--format") .arg("json") .arg(&file) .assert() .success() .get_output() .stdout .clone(); let records: Vec = serde_json::from_slice(&output) .unwrap_or_else(|error| panic!("json parse failed: {error}")); assert_eq!(records.len(), 1); assert_eq!(records[0]["algorithm"], "sha256"); assert_eq!(records[0]["bytes"], 6); assert_eq!(records[0]["repeat"], 2); assert_eq!(records[0]["warmup"], 1); assert_eq!(records[0]["bench_mode"], "end-to-end"); assert!(records[0]["elapsed_ms"].as_f64().is_some()); assert!(records[0]["throughput_mib_s"].as_f64().is_some()); } #[test] fn verifies_generated_manifest() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); let manifest = directory.path().join("checksums.jsonl"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut hash_command = mhash_command(); hash_command .arg("--algorithm") .arg("sha256") .arg("--manifest") .arg(&manifest) .arg(&file); hash_command.assert().success(); let mut verify_command = mhash_command(); verify_command.arg("verify").arg(&manifest).arg("--json"); verify_command .assert() .success() .stdout(predicate::str::contains("\"ok\":true")); } #[test] fn verifies_multi_algorithm_manifest_for_one_path() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); let manifest = directory.path().join("checksums.jsonl"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut hash_command = mhash_command(); hash_command .arg("--algorithm") .arg("sha256,md5,xxh3-128") .arg("--manifest") .arg(&manifest) .arg(&file); hash_command.assert().success(); let mut verify_command = mhash_command(); let output = verify_command .arg("verify") .arg(&manifest) .arg("--json") .assert() .success() .get_output() .stdout .clone(); let records: Vec = serde_json::from_slice(&output) .unwrap_or_else(|error| panic!("json parse failed: {error}")); assert_eq!(records.len(), 3); assert!(records.iter().all(|record| record["ok"] == true)); assert_eq!(records[0]["algorithm"], "md5"); assert_eq!(records[1]["algorithm"], "sha256"); assert_eq!(records[2]["algorithm"], "xxh3-128"); } #[test] fn mercury_output_json_is_honored() { let directory = tempdir().unwrap_or_else(|error| panic!("tempdir failed: {error}")); let file = directory.path().join("sample.txt"); fs::write(&file, b"abc").unwrap_or_else(|error| panic!("fixture write failed: {error}")); let mut command = mhash_command(); command.env("MERCURY_OUTPUT", "json").arg(&file); command .assert() .success() .stdout(predicate::str::starts_with("[{")); } #[test] fn invalid_algorithm_exits_usage_error() { let mut command = mhash_command(); command.arg("--algorithm").arg("sha999").arg("README.md"); command .assert() .code(2) .stderr(predicate::str::contains("unsupported algorithm")); }