use std::path::PathBuf;
use std::process::Command;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
fn run(fixture_name: &str, tag: &str, opts: &[&str]) -> (Option<i32>, String) {
let dir = std::env::temp_dir().join(format!("pounce_unimplopt_{tag}"));
std::fs::create_dir_all(&dir).expect("scratch dir");
let nl = dir.join(fixture_name);
let mut fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixture.push("tests/fixtures");
fixture.push(fixture_name);
std::fs::copy(&fixture, &nl).expect("copy fixture");
let out = Command::new(pounce_exe())
.arg(&nl)
.args(opts)
.arg("print_level=0")
.output()
.expect("run pounce");
let _ = std::fs::remove_dir_all(&dir);
(
out.status.code(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn requesting_an_unimplemented_feature_fails_with_an_explanation() {
for (i, (opt, needle, issue)) in [
("penalty_init_max=42", "CG-penalty", "483"),
(
"gradient_approximation=finite-difference-values",
"finite differences",
"483",
),
(
"dependency_detector=mumps",
"linear-dependency detection",
"483",
),
("check_derivatives_for_naninf=yes", "NaN/Inf", "483"),
("magic_steps=yes", "magic steps", "483"),
("theta_min=1e-5", "CG-penalty acceptor", "551"),
("alpha_for_y_tol=1e-3", "primal-and-full", "551"),
("suppress_all_output=yes", "output controls", "483"),
("hsllib=libcoinhsl.so", "HSL loader", "483"),
("n_sens_steps=3", "perturbation tier", "677"),
]
.into_iter()
.enumerate()
{
let (code, err) = run("user_scaling_suffix.nl", &format!("g{i}"), &[opt]);
assert_eq!(code, Some(2), "`{opt}` should fail; stderr:\n{err}");
assert!(
err.contains(needle),
"`{opt}` should mention `{needle}`; stderr:\n{err}",
);
assert!(err.contains(issue), "stderr:\n{err}");
}
}
#[test]
fn the_corrector_and_resto_sub_capability_refusals_explain_themselves() {
for (i, (opt, needle)) in [
("corrector_type=affine", "TryCorrector"),
("skip_corr_if_neg_curv=no", "corrector step"),
("corrector_compl_avrg_red_fact=2.0", "corrector step"),
(
"expect_infeasible_problem_ctol=1e-4",
"restoration phase itself runs",
),
(
"limited_memory_special_for_resto=yes",
"L-BFGS runs in the restoration sub-solve",
),
(
"resto_failure_feasibility_threshold=1e-6",
"restoration runs",
),
]
.into_iter()
.enumerate()
{
let (code, err) = run("user_scaling_suffix.nl", &format!("c{i}"), &[opt]);
assert_eq!(code, Some(2), "`{opt}` should fail; stderr:\n{err}");
assert!(
err.contains(needle),
"`{opt}` should mention `{needle}`; stderr:\n{err}",
);
assert!(err.contains("551"), "stderr:\n{err}");
}
}
#[test]
fn the_refusal_names_the_offending_option() {
let (_, err) = run("user_scaling_suffix.nl", "named", &["vartheta=0.9"]);
assert!(err.contains("`vartheta`"), "stderr:\n{err}");
}
#[test]
fn explicitly_setting_a_default_still_solves() {
for (i, opt) in ["dependency_detector=none", "magic_steps=no", "recalc_y=no"]
.into_iter()
.enumerate()
{
let (code, err) = run("user_scaling_suffix.nl", &format!("d{i}"), &[opt]);
assert_eq!(code, Some(0), "`{opt}` asks for nothing; stderr:\n{err}");
assert!(!err.contains("does not implement"), "stderr:\n{err}");
}
}
#[test]
fn knobs_on_implemented_features_still_solve() {
for (i, opt) in [
"max_resto_iter=17",
"accept_after_max_steps=3",
"limited_memory_max_skipping=4",
"recalc_y=yes",
"recalc_y_feas_tol=1e-4",
]
.into_iter()
.enumerate()
{
let (code, err) = run("user_scaling_suffix.nl", &format!("b{i}"), &[opt]);
assert_eq!(
code,
Some(0),
"`{opt}` configures an implemented feature; stderr:\n{err}",
);
}
}
#[test]
fn a_disproved_caching_hint_is_refused_with_a_warning() {
let (code, err) = run("user_scaling_suffix.nl", "hint", &["hessian_constant=yes"]);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert!(err.contains("warning"), "stderr:\n{err}");
assert!(err.contains("hessian_constant"), "stderr:\n{err}");
assert!(
err.contains("ignoring"),
"the warning must say the hint was refused, not merely unused; \
stderr:\n{err}"
);
}
#[test]
fn a_proved_constant_hessian_is_evaluated_once() {
let dir = std::env::temp_dir().join("pounce_unimplopt_constderiv");
std::fs::create_dir_all(&dir).expect("scratch dir");
let nl = dir.join("nonconvex_qp.nl");
let mut fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixture.push("tests/fixtures/nonconvex_qp.nl");
std::fs::copy(&fixture, &nl).expect("copy fixture");
let json = dir.join("out.json");
let out = Command::new(pounce_exe())
.arg(&nl)
.arg("--json-output")
.arg(&json)
.arg("print_level=0")
.output()
.expect("run pounce");
assert_eq!(
out.status.code(),
Some(0),
"stderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let text = std::fs::read_to_string(&json).expect("json report");
let _ = std::fs::remove_dir_all(&dir);
fn field(text: &str, key: &str) -> i64 {
let at = text
.find(&format!("\"{key}\""))
.unwrap_or_else(|| panic!("`{key}` missing from report:\n{text}"));
let rest = &text[at + key.len() + 2..];
let start = rest.find(':').expect("colon") + 1;
let end = rest[start..]
.find([',', '}'])
.map(|e| start + e)
.unwrap_or(rest.len());
rest[start..end].trim().parse().expect("integer field")
}
let iters = field(&text, "iteration_count");
let hess = field(&text, "num_hess_evals");
let jac = field(&text, "num_constr_jac_evals");
assert!(iters > 1, "expected a multi-iteration solve, got {iters}");
assert_eq!(
hess, 1,
"`∇²L` is provably constant on this model; it must be evaluated \
once and reused for all {iters} iterations"
);
assert_eq!(
jac, 1,
"every row is linear; the Jacobian must be evaluated once (got \
{jac} over {iters} iterations)"
);
}
#[test]
fn a_backend_knob_warns_but_solves() {
let (code, err) = run(
"user_scaling_suffix.nl",
"backend",
&["ma97_order=metis", "tol=1e-8"],
);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert!(err.contains("warning:"), "stderr:\n{err}");
assert!(err.contains("`ma97_order`"), "stderr:\n{err}");
assert!(err.contains("MA97"), "the backend is named; stderr:\n{err}");
assert!(
err.contains("result is unaffected"),
"the warning must say the answer is not at risk; stderr:\n{err}",
);
assert!(err.contains("551"), "stderr:\n{err}");
}
#[test]
fn backend_knobs_warn_once_per_family() {
let (code, err) = run(
"user_scaling_suffix.nl",
"backendgroup",
&[
"ma97_order=metis",
"ma97_u=1e-4",
"pardiso_msglvl=1",
"tol=1e-8",
],
);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert_eq!(
err.matches("ma97_order").count(),
1,
"one grouped line, not one per option; stderr:\n{err}",
);
assert!(err.contains("`ma97_u`"), "stderr:\n{err}");
assert!(err.contains("Pardiso"), "stderr:\n{err}");
}
#[test]
fn a_backend_knob_at_its_default_is_silent() {
let (code, err) = run("user_scaling_suffix.nl", "backenddef", &["ma97_order=auto"]);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert!(!err.contains("warning:"), "stderr:\n{err}");
}
#[test]
fn the_refusal_covers_the_convex_route() {
let (code, err) = run("boxed_qp_min.nl", "convex", &["magic_steps=yes"]);
assert_eq!(code, Some(2), "stderr:\n{err}");
assert!(err.contains("magic steps"), "stderr:\n{err}");
}
#[test]
fn a_default_run_is_silent() {
let (code, err) = run("user_scaling_suffix.nl", "plain", &[]);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert!(!err.contains("does not implement"), "stderr:\n{err}");
assert!(!err.contains("warning:"), "stderr:\n{err}");
}
fn run_bare(fixture_name: &str, tag: &str, opts: &[&str]) -> (Option<i32>, String) {
let dir = std::env::temp_dir().join(format!("pounce_unimplopt_bare_{tag}"));
std::fs::create_dir_all(&dir).expect("scratch dir");
let nl = dir.join(fixture_name);
let mut fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixture.push("tests/fixtures");
fixture.push(fixture_name);
std::fs::copy(&fixture, &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.code(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn backend_knobs_alone_are_refused() {
let (code, err) = run_bare("user_scaling_suffix.nl", "onlyknob", &["ma97_order=metis"]);
assert_eq!(code, Some(2), "stderr:\n{err}");
assert!(
err.contains("every option this run sets"),
"the message must say the whole file is inert, not just this knob; stderr:\n{err}",
);
assert!(
err.contains("linear_solver=feral"),
"the message must say what to set instead; stderr:\n{err}",
);
assert!(err.contains("551"), "stderr:\n{err}");
}
#[test]
fn several_backend_families_alone_are_refused_once() {
let (code, err) = run_bare(
"user_scaling_suffix.nl",
"onlyknobs",
&["ma97_order=metis", "pardiso_msglvl=1", "mumps_pivtol=1e-5"],
);
assert_eq!(code, Some(2), "stderr:\n{err}");
assert_eq!(
err.matches("every option this run sets").count(),
1,
"one refusal for the file, not one per family; stderr:\n{err}",
);
}
#[test]
fn one_real_option_turns_the_refusal_back_into_a_warning() {
let (code, err) = run_bare(
"user_scaling_suffix.nl",
"knobplusreal",
&["ma97_order=metis", "tol=1e-8"],
);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert!(
!err.contains("every option this run sets"),
"a file with real content must not be refused; stderr:\n{err}",
);
assert!(err.contains("warning:"), "stderr:\n{err}");
assert!(err.contains("`ma97_order`"), "stderr:\n{err}");
}
#[test]
fn backend_knobs_at_their_defaults_alone_are_silent() {
let (code, err) = run_bare(
"user_scaling_suffix.nl",
"onlydefknob",
&["ma97_order=auto"],
);
assert_eq!(code, Some(0), "stderr:\n{err}");
assert!(
!err.contains("every option this run sets"),
"stderr:\n{err}"
);
assert!(!err.contains("warning:"), "stderr:\n{err}");
}
#[test]
fn a_backend_only_options_file_is_refused_by_either_route() {
for (tag, explicit) in [("implicit", false), ("explicit", true)] {
let dir = std::env::temp_dir().join(format!("pounce_unimplopt_optfile_{tag}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
let nl = dir.join("user_scaling_suffix.nl");
let mut fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
fixture.push("tests/fixtures/user_scaling_suffix.nl");
std::fs::copy(&fixture, &nl).expect("copy fixture");
let opt = dir.join("ipopt.opt");
std::fs::write(&opt, "ma97_order metis\nma97_u 1e-4\n").expect("write ipopt.opt");
let mut cmd = Command::new(pounce_exe());
cmd.arg(&nl).current_dir(&dir);
if explicit {
cmd.arg(format!("option_file_name={}", opt.display()));
}
let out = cmd.output().expect("run pounce");
let err = String::from_utf8_lossy(&out.stderr).into_owned();
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(out.status.code(), Some(2), "{tag} route; stderr:\n{err}");
assert!(
err.contains("every option this run sets"),
"{tag} route; stderr:\n{err}",
);
}
}