use std::path::PathBuf;
use std::process::Command;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
struct Run {
code: Option<i32>,
stdout: String,
stderr: String,
values: Vec<f64>,
}
fn run(tag: &str, fixture: &str, n_values: usize, opts: &[&str]) -> Run {
let dir = std::env::temp_dir().join(format!("pounce_issue_492_{tag}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
let mut src = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
src.push("tests/fixtures");
src.push(fixture);
std::fs::copy(&src, dir.join("m.nl")).expect("copy fixture");
let mut cmd = Command::new(pounce_exe());
cmd.current_dir(&dir).arg("m").arg("-AMPL");
for o in opts {
cmd.arg(o);
}
let out = cmd.output().expect("run pounce");
let values = match std::fs::read_to_string(dir.join("m.sol")) {
Ok(sol) => {
let numeric: Vec<f64> = sol
.lines()
.take_while(|l| !l.starts_with("objno"))
.filter_map(|l| l.trim().parse::<f64>().ok())
.collect();
assert!(
numeric.len() >= n_values,
"expected at least {n_values} entries in the .sol body, got {numeric:?}"
);
numeric[numeric.len() - n_values..].to_vec()
}
Err(_) => Vec::new(),
};
Run {
code: out.status.code(),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
values,
}
}
#[test]
fn a_computed_row_constant_no_longer_forces_an_lp_onto_the_nlp_route() {
let r = run("auto_expr", "lp_row_constant_expr.nl", 3, &[]);
assert_eq!(r.code, Some(0), "stderr:\n{}", r.stderr);
assert!(
r.stdout.contains("Problem class: LP"),
"expected the classifier to say LP:\n{}",
r.stdout
);
assert!(
r.stdout.contains("pounce-convex"),
"an LP must reach the convex route:\n{}",
r.stdout
);
}
#[test]
fn the_forced_lp_solver_accepts_a_model_with_a_computed_row_constant() {
let r = run(
"forced_expr",
"lp_row_constant_expr.nl",
3,
&["solver_selection=lp-ipm"],
);
assert_eq!(
r.code,
Some(0),
"forcing lp-ipm on an LP must solve, not error\nstdout:\n{}\nstderr:\n{}",
r.stdout,
r.stderr
);
}
#[test]
fn the_folded_row_is_the_row_the_model_wrote() {
for (tag, fixture) in [
("optimum_literal", "lp_row_constant.nl"),
("optimum_expr", "lp_row_constant_expr.nl"),
] {
let r = run(tag, fixture, 3, &[]);
assert_eq!(r.code, Some(0), "{fixture} stderr:\n{}", r.stderr);
let (x0, x1) = (r.values[1], r.values[2]);
assert!(
(x0 - 0.0).abs() < 1e-6 && (x1 - 3.0).abs() < 1e-6,
"{fixture}: expected (x0, x1) = (0, 3), got ({x0}, {x1}); \
a dropped row constant would give x0 + x1 = 6"
);
}
}
#[test]
fn phase_6_reduces_a_row_whose_constant_lived_in_the_expression_segment() {
let r = run(
"phase6",
"linear_eq_aggregation_row_constant.nl",
5,
&["presolve=yes", "presolve_linear_eq_reduction=yes"],
);
assert_eq!(r.code, Some(0), "stderr:\n{}", r.stderr);
assert!(
r.stdout.contains("eliminated 1 columns"),
"the reduction declined the row-constant equality:\n{}",
r.stdout
);
assert!(
r.stdout.contains("dropped 1 rows"),
"the consumed row was not dropped:\n{}",
r.stdout
);
assert!(r.stdout.contains("Optimal Solution Found"), "{}", r.stdout);
}
#[test]
fn the_row_constant_fixture_agrees_with_the_hand_folded_one() {
let folded = run("hand_folded", "linear_eq_aggregation.nl", 5, &[]);
let offset = run(
"offset",
"linear_eq_aggregation_row_constant.nl",
5,
&["presolve=yes", "presolve_linear_eq_reduction=yes"],
);
assert_eq!(folded.code, Some(0), "stderr:\n{}", folded.stderr);
assert_eq!(offset.code, Some(0), "stderr:\n{}", offset.stderr);
assert_eq!(
folded.values.len(),
offset.values.len(),
"the .sol body changed length: {:?} vs {:?}",
folded.values,
offset.values
);
for (k, (f, o)) in folded.values.iter().zip(offset.values.iter()).enumerate() {
assert!(
(f - o).abs() < 1e-6,
".sol entry {k} diverged: hand-folded={f} row-constant={o}"
);
}
assert!(
offset.values[0].abs() > 1e-6,
"the consumed row came back with a zero multiplier: {:?}",
offset.values
);
}