use std::path::PathBuf;
use std::process::Command;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
fn fixture(name: &str) -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests");
p.push("fixtures");
p.push(name);
p
}
fn run(fixture_name: &str, tag: &str, opts: &[&str]) -> (bool, String, String) {
let dir = std::env::temp_dir().join(format!("pounce_gh483_{tag}"));
std::fs::create_dir_all(&dir).expect("scratch dir");
let nl = dir.join(fixture_name);
std::fs::copy(fixture(fixture_name), &nl).expect("copy fixture");
let out = Command::new(pounce_exe())
.arg(&nl)
.args(opts)
.output()
.expect("run pounce");
let _ = std::fs::remove_dir_all(&dir);
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn objective_columns(stdout: &str) -> (f64, f64) {
let line = stdout
.lines()
.find(|l| l.trim_start().starts_with("Objective."))
.unwrap_or_else(|| panic!("no Objective summary line in:\n{stdout}"));
let nums: Vec<f64> = line
.split_whitespace()
.filter_map(|t| t.parse::<f64>().ok())
.collect();
assert_eq!(nums.len(), 2, "expected scaled + unscaled columns: {line}");
(nums[0], nums[1])
}
#[test]
fn scaling_factor_suffix_is_applied() {
let (ok, stdout, stderr) = run(
"user_scaling_suffix.nl",
"applied",
&["nlp_scaling_method=user-scaling"],
);
assert!(ok, "solve failed\nstdout:\n{stdout}\nstderr:\n{stderr}");
let (scaled, unscaled) = objective_columns(&stdout);
assert!(
(scaled / unscaled - 100.0).abs() < 1e-6,
"scaling_factor[obj]=100 should scale the IPM's objective; \
got scaled={scaled}, unscaled={unscaled}",
);
}
#[test]
fn scaling_factor_suffix_is_inert_without_the_option() {
let (ok, stdout, stderr) = run("user_scaling_suffix.nl", "inert", &[]);
assert!(ok, "solve failed\nstdout:\n{stdout}\nstderr:\n{stderr}");
let (scaled, unscaled) = objective_columns(&stdout);
assert!(
(scaled - unscaled).abs() < 1e-9,
"default scaling should leave this objective alone; \
got scaled={scaled}, unscaled={unscaled}",
);
}
#[test]
fn variable_scaling_factor_is_applied_without_moving_the_answer() {
let (ok, stdout, stderr) = run(
"user_scaling_var_suffix.nl",
"varfactor",
&["nlp_scaling_method=user-scaling"],
);
assert!(
ok,
"solve failed
stdout:
{stdout}
stderr:
{stderr}"
);
let (scaled, unscaled) = objective_columns(&stdout);
let (ok_ref, stdout_ref, stderr_ref) = run(
"user_scaling_suffix.nl",
"varfactor_ref",
&["nlp_scaling_method=user-scaling"],
);
assert!(
ok_ref,
"reference solve failed
stdout:
{stdout_ref}
stderr:
{stderr_ref}"
);
let (_, unscaled_ref) = objective_columns(&stdout_ref);
assert!(
(unscaled - unscaled_ref).abs() <= 1e-6 * unscaled_ref.abs().max(1.0),
"scaling_factor[x1]=3 must not move the objective in user units; got {unscaled} with the factor, {unscaled_ref} without",
);
assert!(
(scaled / unscaled - 100.0).abs() < 1e-6,
"scaling_factor[obj]=100 should still reach the IPM alongside the variable factor; got scaled={scaled}, unscaled={unscaled}",
);
}
#[test]
fn variable_scaling_factor_is_inert_without_the_option() {
let (ok, stdout, stderr) = run("user_scaling_var_suffix.nl", "varinert", &[]);
assert!(ok, "solve failed\nstdout:\n{stdout}\nstderr:\n{stderr}");
}
#[test]
fn an_inapplicable_factor_is_refused_readably() {
let (ok, stdout, stderr) = run(
"user_scaling_bad_var_suffix.nl",
"badfactor",
&["nlp_scaling_method=user-scaling"],
);
assert!(
!ok,
"a negative factor must fail the solve\nstdout:\n{stdout}"
);
let line = stderr
.lines()
.find(|l| l.contains("nlp_scaling_method=user-scaling supplied"))
.unwrap_or_else(|| panic!("no refusal on stderr:\n{stderr}"));
assert!(
line.contains("supplied per-variable scaling factors that cannot be applied."),
"the opening clause is broken up: {line}"
);
assert!(
line.contains("Correct the factors, or drop nlp_scaling_method=user-scaling."),
"the closing clause is broken up: {line}"
);
assert!(
!line.contains(" "),
"the message carries a run of source indentation: {line}"
);
assert!(
stderr.contains(
"or drop nlp_scaling_method=user-scaling.
"
),
"the refusal is not newline-terminated:
{stderr}"
);
assert!(
line.contains("-3") && line.contains("finite and positive"),
"the refusal should name the offending factor and the rule: {line}"
);
}