use std::path::PathBuf;
use std::process::Command;
use pounce_cli::verify::sha256;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
fn fixture_nl() -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests");
p.push("fixtures");
p.push("parametric.nl");
p
}
fn tmp(suffix: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("pounce_verify_{}_{suffix}", std::process::id()));
p
}
fn solve_to(sol: &PathBuf) {
let status = Command::new(pounce_exe())
.arg(fixture_nl())
.arg(sol)
.status()
.expect("spawn pounce solve");
assert!(status.success(), "solve failed: {status:?}");
assert!(sol.exists(), "no .sol written");
}
fn verify_exit(nl: &PathBuf, sol: &PathBuf) -> i32 {
Command::new(pounce_exe())
.arg("verify")
.arg(nl)
.arg(sol)
.status()
.expect("spawn pounce verify")
.code()
.expect("exit code")
}
fn verify_stdout(nl: &PathBuf, sol: &PathBuf) -> String {
let out = Command::new(pounce_exe())
.arg("verify")
.arg(nl)
.arg(sol)
.output()
.expect("spawn pounce verify");
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn genuine_solution_verifies() {
let sol = tmp("good.sol");
solve_to(&sol);
assert_eq!(
verify_exit(&fixture_nl(), &sol),
0,
"genuine .sol should verify"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn tampered_primal_is_rejected() {
let sol = tmp("tamper.sol");
solve_to(&sol);
let text = std::fs::read_to_string(&sol).unwrap();
let mut lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
let objno_idx = lines.iter().position(|l| l.starts_with("objno")).unwrap();
let last_primal = objno_idx - 1;
lines[last_primal] = "9.9e9".to_string();
std::fs::write(&sol, lines.join("\n")).unwrap();
assert_eq!(
verify_exit(&fixture_nl(), &sol),
20,
"tampered .sol must be rejected"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn fabricated_zeros_is_rejected() {
let n = 5;
let m = 4;
let mut s = String::from("POUNCE 9.9: Optimal Solution Found\n\nOptions\n0\n");
s.push_str(&format!("{m}\n{m}\n{n}\n{n}\n"));
for _ in 0..m {
s.push_str("0.0\n");
}
for _ in 0..n {
s.push_str("0.0\n");
}
s.push_str("objno 0 0\n");
let sol = tmp("fake.sol");
std::fs::write(&sol, s).unwrap();
assert_eq!(
verify_exit(&fixture_nl(), &sol),
20,
"fabricated .sol must be rejected"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn dimension_mismatch_is_usage_error() {
let mut s = String::from("msg\n\nOptions\n0\n0\n0\n3\n3\n");
for _ in 0..3 {
s.push_str("1.0\n");
}
s.push_str("objno 0 0\n");
let sol = tmp("mismatch.sol");
std::fs::write(&sol, s).unwrap();
assert_eq!(
verify_exit(&fixture_nl(), &sol),
2,
"dimension mismatch must be a usage error"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn the_two_complementarity_residuals_are_labelled_apart() {
let sol = tmp("compl.sol");
solve_to(&sol);
let out = verify_stdout(&fixture_nl(), &sol);
assert!(
out.contains("constraint complementarity (rows, |λ|·slack)"),
"the row quantity must say it is over rows:\n{out}"
);
assert!(
out.contains("bound complementarity (vars, |z|·slack)"),
"the bound quantity must say it is over variables:\n{out}"
);
assert!(
!out.contains("complementarity residual"),
"an unqualified `complementarity residual` label is the bug:\n{out}"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn absent_bound_multipliers_read_as_not_checked() {
let n = 5;
let m = 4;
let mut s = String::from("POUNCE: Optimal Solution Found\n\nOptions\n0\n");
s.push_str(&format!("{m}\n{m}\n{n}\n{n}\n"));
for _ in 0..m {
s.push_str("0.0\n");
}
for _ in 0..n {
s.push_str("0.5\n");
}
s.push_str("objno 0 0\n");
let sol = tmp("nozl.sol");
std::fs::write(&sol, s).unwrap();
let out = verify_stdout(&fixture_nl(), &sol);
let line = out
.lines()
.find(|l| l.contains("bound complementarity"))
.unwrap_or_else(|| panic!("no bound complementarity line:\n{out}"));
assert!(
line.contains("not checked"),
"bound complementarity must report `not checked`, not a number: {line}"
);
assert!(
out.contains("ipopt_zL_out/ipopt_zU_out"),
"say *why* it is not checked:\n{out}"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn bound_complementarity_reproduces_the_solvers_own_figure() {
let sol = tmp("compl_match.sol");
let out = Command::new(pounce_exe())
.arg(fixture_nl())
.arg(&sol)
.output()
.expect("spawn pounce solve");
assert!(out.status.success());
let solve_log = String::from_utf8_lossy(&out.stdout).into_owned();
let from_log = |label: &str| -> f64 {
let line = solve_log
.lines()
.find(|l| l.starts_with(label))
.unwrap_or_else(|| panic!("no `{label}` line in the solve log:\n{solve_log}"));
line.rsplit_once(':')
.and_then(|(_, rest)| rest.split_whitespace().next())
.and_then(|t| t.parse::<f64>().ok())
.unwrap_or_else(|| panic!("unparseable `{label}` line: {line}"))
};
let solver_compl = from_log("Complementarity");
let solver_dual_inf = from_log("Dual infeasibility");
let report = verify_stdout(&fixture_nl(), &sol);
let from_report = |label: &str| -> f64 {
let line = report
.lines()
.find(|l| l.contains(label))
.unwrap_or_else(|| panic!("no `{label}` line:\n{report}"));
line.rsplit_once(':')
.and_then(|(_, rest)| rest.split_whitespace().next())
.and_then(|t| t.parse::<f64>().ok())
.unwrap_or_else(|| panic!("unparseable `{label}` line: {line}"))
};
let close = |a: f64, b: f64| (a - b).abs() <= 1e-3 * a.abs().max(b.abs()).max(1e-300);
let ours = from_report("bound complementarity (vars");
assert!(
close(ours, solver_compl),
"verify's bound complementarity {ours:.3e} must match the solver's \
Complementarity {solver_compl:.3e}"
);
let ours = from_report("dual infeasibility (with z_L/z_U");
assert!(
close(ours, solver_dual_inf),
"verify's exact dual infeasibility {ours:.3e} must match the solver's \
{solver_dual_inf:.3e}"
);
let _ = std::fs::remove_file(&sol);
}
#[test]
fn receipt_separates_the_two_complementarity_fields() {
let sol = tmp("compl_receipt.sol");
solve_to(&sol);
let receipt = tmp("compl_receipt.json");
let status = Command::new(pounce_exe())
.arg("verify")
.arg(fixture_nl())
.arg(&sol)
.arg("--json-output")
.arg(&receipt)
.status()
.expect("spawn pounce verify --json-output");
assert_eq!(status.code(), Some(0));
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&receipt).unwrap()).unwrap();
let opt = &v["optimality"];
assert_eq!(opt["bound_multipliers_present"], true);
assert!(
opt["constraint_complementarity_residual"].is_number(),
"receipt must carry the row quantity under its own name: {opt}"
);
assert!(
opt["bound_complementarity_residual"].is_number(),
"a .sol with zL/zU suffixes must report bound complementarity: {opt}"
);
assert_eq!(
opt["complementarity_residual"],
opt["constraint_complementarity_residual"]
);
let _ = std::fs::remove_file(&sol);
let _ = std::fs::remove_file(&receipt);
}
#[test]
fn signed_receipt_validates_with_the_key_only() {
let sol = tmp("signed.sol");
solve_to(&sol);
let receipt = tmp("receipt.json");
let key = "test-secret-key-not-the-agent's";
let status = Command::new(pounce_exe())
.arg("verify")
.arg(fixture_nl())
.arg(&sol)
.arg("--json-output")
.arg(&receipt)
.env("POUNCE_VERIFY_KEY", key)
.status()
.expect("spawn pounce verify --json-output");
assert_eq!(status.code(), Some(0));
let text = std::fs::read_to_string(&receipt).unwrap();
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(v["signature_alg"], "HMAC-SHA256");
let sig = v["signature"].as_str().expect("signature present");
let preimage = format!(
"pounce-verify-receipt/v1\n\
verify_version=1\n\
nl_sha256={}\n\
sol_sha256={}\n\
n_vars={}\n\
n_cons={}\n\
feasible={}\n\
verified={}\n\
verdict={}\n",
v["problem"]["sha256"].as_str().unwrap(),
v["solution"]["sha256"].as_str().unwrap(),
v["problem"]["n_vars"].as_u64().unwrap(),
v["problem"]["n_cons"].as_u64().unwrap(),
v["feasibility"]["feasible"].as_bool().unwrap(),
v["verified"].as_bool().unwrap(),
v["verdict"].as_str().unwrap(),
);
let expect = sha256::hmac_hex(key.as_bytes(), preimage.as_bytes());
assert_eq!(sig, expect, "signature must validate with the real key");
let wrong = sha256::hmac_hex(b"wrong-key", preimage.as_bytes());
assert_ne!(sig, wrong, "signature must not validate under a wrong key");
let _ = std::fs::remove_file(&sol);
let _ = std::fs::remove_file(&receipt);
}