use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
fn fixture() -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests");
p.push("fixtures");
p.push("convex_qp_sens.nl");
p
}
fn staged_nl(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("pounce_issue196_{tag}"));
std::fs::create_dir_all(&dir).expect("mkdir temp");
let dst = dir.join("convex_qp_sens.nl");
std::fs::copy(fixture(), &dst).expect("copy fixture");
let _ = std::fs::remove_file(dir.join("convex_qp_sens.sol"));
dst
}
fn parse_sens_sol_state_1(sol: &str) -> Option<HashMap<usize, f64>> {
let mut lines = sol.lines();
while let Some(line) = lines.next() {
if let Some(rest) = line.strip_prefix("suffix ") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() < 5 {
continue;
}
let count: usize = parts[1].parse().ok()?;
let tabline: usize = parts[4].parse().ok()?;
let name = lines.next()?.trim().to_string();
if name != "sens_sol_state_1" {
for _ in 0..(tabline + count) {
lines.next();
}
continue;
}
for _ in 0..tabline {
lines.next();
}
let mut out = HashMap::new();
for _ in 0..count {
let l = lines.next()?;
let mut it = l.split_whitespace();
let idx: usize = it.next()?.parse().ok()?;
let val: f64 = it.next()?.parse().ok()?;
out.insert(idx, val);
}
return Some(out);
}
}
None
}
#[test]
fn auto_routes_sens_qp_to_nlp_and_writes_sens_suffix() {
let nl = staged_nl("auto");
let out = Command::new(pounce_exe())
.arg(&nl)
.output()
.expect("spawn pounce");
assert_eq!(out.status.code(), Some(0), "solve should succeed");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("routing to the general NLP"),
"auto should announce the reroute to NLP; stderr=\n{stderr}"
);
let sol_path = nl.with_extension("sol");
let sol = std::fs::read_to_string(&sol_path).expect("read .sol");
let sens = parse_sens_sol_state_1(&sol)
.expect("sens_sol_state_1 must be present after auto reroute (issue #196)");
let x = *sens.get(&0).expect("perturbed x (index 0)");
assert!(
(x - 1.5).abs() < 1e-6,
"dx*/dp = 1 so p 1.0 -> 1.5 gives x* -> 1.5; got {x}"
);
}
#[test]
fn explicit_qp_ipm_warns_and_skips_sens() {
let nl = staged_nl("qp_ipm");
let out = Command::new(pounce_exe())
.arg(&nl)
.arg("solver_selection=qp-ipm")
.output()
.expect("spawn pounce");
assert_eq!(out.status.code(), Some(0), "solve should succeed");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("warning:") && stderr.contains("will be skipped"),
"explicit convex force must warn that sensitivity is skipped; stderr=\n{stderr}"
);
let sol = std::fs::read_to_string(nl.with_extension("sol")).expect("read .sol");
assert!(
parse_sens_sol_state_1(&sol).is_none(),
"forced convex path does not compute sensitivity, so no sens_sol_state_1"
);
}
#[test]
fn nlp_path_writes_sens_suffix() {
let nl = staged_nl("nlp");
let out = Command::new(pounce_exe())
.arg(&nl)
.arg("solver_selection=nlp")
.output()
.expect("spawn pounce");
assert_eq!(out.status.code(), Some(0), "solve should succeed");
let sol = std::fs::read_to_string(nl.with_extension("sol")).expect("read .sol");
let sens = parse_sens_sol_state_1(&sol).expect("sens_sol_state_1 present on NLP path");
let x = *sens.get(&0).expect("perturbed x (index 0)");
assert!((x - 1.5).abs() < 1e-6, "expected x* -> 1.5; got {x}");
}