use super::*;
fn flags_offered_by(stdout: &str) -> Vec<String> {
let block = stdout
.split("OPTIONS:\n")
.nth(1)
.expect("--help has an OPTIONS: block");
block
.lines()
.take_while(|l| l.is_empty() || l.starts_with(' '))
.filter(|l| l.len() - l.trim_start().len() <= 8 && l.trim_start().starts_with('-'))
.filter_map(|l| l.split_whitespace().find(|t| t.starts_with("--")))
.map(str::to_string)
.collect()
}
#[test]
fn help_names_every_flag_and_every_emitted_file() {
for flag in ["--help", "-h"] {
let r = run(&[flag]).exit(0);
assert!(r.stderr.is_empty(), "help belongs on stdout: {}", r.stderr);
let offered = flags_offered_by(&r.stdout);
assert!(
offered.len() >= 2 && offered.contains(&"--help".to_string()),
"the option block should list the flags, got {offered:?}",
);
for token in &offered {
let attempt = run(&[token]);
assert!(
!attempt.stderr.contains("unknown option"),
"--help offers {token}, which the parser rejects:\n{}",
attempt.stderr,
);
}
for name in [
REDUCED_CNF_NAME,
PREPROCESS_RECORD_NAME,
VTREE_NAME,
COMPONENTS_JSON_NAME,
COMPONENTS_DIR,
CANDIDATES_DIR,
] {
r.assert_stdout(name);
}
}
}
#[test]
fn help_offers_every_vtree_base() {
let r = run(&["--help"]).exit(0);
for name in decomposition_spec_names()
.chain(baseline_spec_names())
.chain(standalone_spec_names())
{
r.assert_stdout(name);
}
}
#[test]
fn no_input_cnf_is_rejected() {
run(&["-o", "d"]).exit(2).assert_stderr("no input CNF");
run(&[]).exit(2).assert_stderr("no input CNF");
}
#[test]
fn a_missing_out_dir_is_rejected() {
run(&["in.cnf"])
.exit(2)
.assert_stderr("--out-dir is required");
}
#[test]
fn a_second_positional_argument_is_rejected() {
run(&["a.cnf", "b.cnf", "-o", "d"])
.exit(2)
.assert_stderr("expected exactly one input CNF");
}
#[test]
fn an_unknown_option_is_quoted_back() {
let r = run(&["in.cnf", "-o", "d", "--bogus"]).exit(2);
r.assert_stderr("unknown option");
r.assert_stderr("\"--bogus\"");
}
#[test]
fn every_value_taking_flag_reports_its_own_missing_value() {
for (flag, named) in [
("-o", "--out-dir"),
("--out-dir", "--out-dir"),
("--mode", "--mode"),
("--vtree", "--vtree"),
("--budget-ms", "--budget-ms"),
("--components", "--components"),
("--candidates", "--candidates"),
] {
run(&["in.cnf", flag])
.exit(2)
.assert_stderr(&format!("{named} needs a value"));
}
}
#[test]
fn a_bad_mode_lists_every_mode_it_accepts() {
let r = run(&["in.cnf", "-o", "d", "--mode", "bogus"]).exit(2);
r.assert_stderr("\"bogus\"");
for mode in [Mode::Mc, Mode::Wmc, Mode::Pmc, Mode::Pwmc, Mode::Compile] {
r.assert_stderr(mode.token());
}
}
#[test]
fn a_bad_budget_ms_is_rejected() {
run(&["in.cnf", "-o", "d", "--budget-ms", "notanumber"])
.exit(2)
.assert_stderr("--budget-ms expects an integer");
}
#[test]
fn a_bad_components_policy_names_both_policies() {
let r = run(&["in.cnf", "-o", "d", "--components", "bogus"]).exit(2);
r.assert_stderr("split");
r.assert_stderr("whole");
}
#[test]
fn a_non_integer_candidate_count_is_rejected() {
for bad in ["abc", "-1", "2.5"] {
run(&["in.cnf", "-o", "d", "--candidates", bad])
.exit(2)
.assert_stderr("positive integer");
}
}
#[test]
fn a_zero_candidate_count_reaches_the_shared_validator() {
run(&["in.cnf", "-o", "d", "--candidates", "0"])
.exit(2)
.assert_stderr("at least 1");
}
#[test]
fn an_unknown_vtree_spec_is_a_usage_error() {
let t = Scratch::new("badspec");
let input = t.file("in.cnf", IRREDUCIBLE_5);
let r = run(&[
s(&input),
"-o",
s(&t.out("bundle")),
"--vtree",
"nosuchspec",
])
.exit(2);
r.assert_stderr("nosuchspec");
r.assert_stderr("unknown vtree type");
r.assert_stderr(DEFAULT_VTREE_SPEC);
}
#[test]
fn a_bad_force_axis_is_a_usage_error_naming_the_token() {
let t = Scratch::new("badforce");
let input = t.file("in.cnf", IRREDUCIBLE_5);
for (spec, needle) in [
("force:dim=9", "9"),
("force:bogus=1", "bogus"),
("force:treeify=cut,feedback=2", "feedback"),
("force:sideways", "sideways"),
] {
let r = run(&[s(&input), "-o", s(&t.out("bundle")), "--vtree", spec]).exit(2);
r.assert_stderr(needle);
}
}
#[test]
fn the_force_spec_is_reachable_and_reproducible() {
let t = Scratch::new("force");
let input = t.file("in.cnf", &wide_component_dimacs(None));
for (tag, spec) in [
("bare", "force"),
("cut", "force:treeify=cut,dim=3"),
("axes", "force:feedback=2,restarts=2,clause-weight=short"),
] {
let mut written: Vec<String> = Vec::new();
for pass in 0..2 {
let out = t.out(&format!("{tag}{pass}"));
run(&[
s(&input),
"-o",
s(&out),
"--vtree",
spec,
"--no-arjun",
"--no-simplify",
])
.exit(0)
.assert_stdout(spec);
written.push(read(&out.join(VTREE_NAME)));
}
assert!(
written[0].starts_with("vtree "),
"{spec} must write an SDD-format vtree, got: {}",
&written[0][..written[0].len().min(40)],
);
assert_eq!(
written[0], written[1],
"{spec} must write the same vtree on every run",
);
}
}
#[test]
fn the_portfolio_combiner_specs_are_reachable_by_name() {
let t = Scratch::new("combiners");
let input = t.file("in.cnf", &wide_component_dimacs(None));
for (tag, spec) in [
("guided", "guided-bisect"),
("guided-steps", "guided-bisect:budget=150000steps,iters=15"),
(
"reading",
"flowcutter-incidence:binarize=edge,place=shallow,root=centroid",
),
] {
let out = t.out(tag);
run(&[
s(&input),
"-o",
s(&out),
"--vtree",
spec,
"--no-arjun",
"--no-simplify",
])
.exit(0)
.assert_stdout(spec);
let reduced = std::fs::File::open(out.join(REDUCED_CNF_NAME)).expect("open reduced.cnf");
let (formula, _) = CnfFormula::from_dimacs(std::io::BufReader::new(reduced))
.expect("the emitted CNF must parse");
assert_well_formed_vtree(&read(&out.join(VTREE_NAME)), formula.num_vars);
}
}
#[test]
fn naming_every_conversion_key_searches_exactly_one_reading() {
let t = Scratch::new("onereading");
let input = t.file("in.cnf", &wide_component_dimacs(None));
let spec = "flowcutter-primal:root=first,place=deep,binarize=balanced";
run(&[
s(&input),
"-o",
s(&t.out("one")),
"--vtree",
spec,
"--no-arjun",
"--no-simplify",
])
.exit(0)
.assert_stderr("readings=1/1");
}
#[test]
fn rooting_at_a_leaf_bag_reports_the_bag_it_settled_on() {
let t = Scratch::new("leafroot");
let input = t.file("in.cnf", &wide_component_dimacs(None));
let spec = "flowcutter-primal:root=leaf,place=deep,binarize=balanced";
let out = run(&[
s(&input),
"-o",
s(&t.out("leaf")),
"--vtree",
spec,
"--no-arjun",
"--no-simplify",
])
.exit(0);
out.assert_stderr("root=leaf#");
let reported = out
.stderr
.split("root=leaf#")
.nth(1)
.expect("the line names a bag");
let bag: String = reported.chars().take_while(char::is_ascii_digit).collect();
assert!(
!bag.is_empty(),
"the reported rooting must name a bag, got:\n{}",
out.stderr,
);
}
#[test]
fn a_step_budgeted_flowcutter_spec_converts_like_a_timed_one() {
let t = Scratch::new("stepbudget");
let input = t.file("in.cnf", &wide_component_dimacs(None));
for spec in [
"flowcutter-primal:budget=100000steps,iters=10",
"flowcutter-incidence:budget=100000steps,iters=10,binarize=edge",
] {
let out = t.out(spec.split(':').next().expect("a base name"));
run(&[
s(&input),
"-o",
s(&out),
"--vtree",
spec,
"--no-arjun",
"--no-simplify",
])
.exit(0)
.assert_stdout(spec);
let reduced = std::fs::File::open(out.join(REDUCED_CNF_NAME)).expect("open reduced.cnf");
let (formula, _) = CnfFormula::from_dimacs(std::io::BufReader::new(reduced))
.expect("the emitted CNF must parse");
assert_well_formed_vtree(&read(&out.join(VTREE_NAME)), formula.num_vars);
}
}
#[test]
fn every_failure_class_maps_to_the_exit_code_its_reader_expects() {
let t = Scratch::new("exitcodes");
let good = t.file("in.cnf", IRREDUCIBLE_5);
let malformed = t.file("bad.cnf", "p cnf 2 1\nthis is not a clause\n");
run(&[s(&good)]).exit(2);
run(&[s(&good), "-o", s(&t.out("spec")), "--vtree", "nosuchspec"]).exit(2);
run_with_env(
&[s(&good), "-o", s(&t.out("env"))],
&[("VITRI_ARJUN_SBVA", "not-a-value")],
)
.exit(2);
run(&[s(&t.out("absent.cnf")), "-o", s(&t.out("io"))]).exit(1);
run(&[s(&malformed), "-o", s(&t.out("parse"))]).exit(1);
run(&[s(&good), "-o", s(&t.out("ok"))]).exit(0);
}
#[test]
fn a_repeated_value_flag_takes_the_last_value_it_was_given() {
let t = Scratch::new("repeated");
let input = t.file("in.cnf", IRREDUCIBLE_5);
let out = t.out("spec");
run(&[
s(&input),
"-o",
s(&out),
"--vtree",
"nosuchspec",
"--vtree",
"minfill-primal",
])
.exit(0)
.assert_stdout("minfill-primal");
let first = t.out("first");
let second = t.out("second");
run(&[s(&input), "-o", s(&first), "-o", s(&second)]).exit(0);
assert!(second.join(REDUCED_CNF_NAME).exists());
assert!(
!first.exists(),
"the replaced value must not also be written to",
);
run(&[
s(&input),
"-o",
s(&t.out("mode")),
"--mode",
"compile",
"--mode",
"mc",
])
.exit(0)
.assert_stdout(&format!("mode {}", Mode::Mc.token()));
}
#[test]
fn asking_for_a_version_is_not_a_flag_this_tool_has() {
for spelling in ["--version", "-V"] {
let r = run(&["in.cnf", "-o", "d", spelling]).exit(2);
r.assert_stderr("unknown option");
r.assert_stderr(&format!("{spelling:?}"));
}
}
#[test]
fn a_lone_dash_is_refused_as_an_unknown_option() {
let r = run(&["in.cnf", "-o", "d", "-"]).exit(2);
r.assert_stderr("unknown option");
r.assert_stderr("\"-\"");
}
#[test]
fn help_lists_every_mode_and_every_component_policy_token() {
let r = run(&["--help"]).exit(0);
for token in Mode::names().chain(vitri::config::ComponentPolicy::names()) {
r.assert_stdout(token);
}
}
#[test]
fn help_states_the_exit_status_of_each_failure_class() {
let r = run(&["--help"]).exit(0);
let block = r
.stdout
.split("EXIT STATUS:\n")
.nth(1)
.expect("--help has an EXIT STATUS: block");
let codes: Vec<&str> = block
.lines()
.take_while(|l| l.is_empty() || l.starts_with(' '))
.filter_map(|l| l.split_whitespace().next())
.filter(|t| t.chars().all(|c| c.is_ascii_digit()))
.collect();
assert_eq!(
codes,
vec!["0", "1", "2"],
"each status a run can exit with needs its own entry, got:\n{block}",
);
assert!(
block.contains("VITRI_"),
"the wrong-invocation entry must say a variable can put a run there:\n{block}",
);
}
fn assert_well_formed_vtree(text: &str, num_vars: u32) {
let (header, node_lines) = tokenize_vtree_text(text);
let mut declared: BTreeSet<u32> = BTreeSet::new();
let mut children: BTreeSet<u32> = BTreeSet::new();
let mut leaf_vars: BTreeSet<u32> = BTreeSet::new();
let mut declared_nodes = 0usize;
for f in &node_lines {
match f.first().map(String::as_str) {
Some("L") => {
let id: u32 = f[1].parse().expect("a leaf line names its node id");
let var: u32 = f[2].parse().expect("a leaf line names its variable");
assert!(declared.insert(id), "node {id} is declared twice");
assert!(leaf_vars.insert(var), "variable {var} labels two leaves");
declared_nodes += 1;
}
Some("I") => {
let id: u32 = f[1].parse().expect("an internal line names its node id");
for child in [
f[2].parse::<u32>().expect("a left child id"),
f[3].parse::<u32>().expect("a right child id"),
] {
assert!(
declared.contains(&child),
"node {id} names child {child}, which is declared after it",
);
assert!(children.insert(child), "node {child} has two parents");
}
assert!(declared.insert(id), "node {id} is declared twice");
declared_nodes += 1;
}
_ => {}
}
}
assert_eq!(header, declared_nodes, "the header must count the nodes");
assert_eq!(
leaf_vars,
(1..=num_vars).collect::<BTreeSet<u32>>(),
"the leaves must cover every variable exactly once",
);
let roots: Vec<u32> = declared.difference(&children).copied().collect();
assert_eq!(
roots.len(),
1,
"a vtree has exactly one root, got {roots:?}"
);
}
#[test]
fn help_offers_every_vtree_base_and_parameter() {
let stdout = run(&["--help"]).exit(0).stdout;
for base in vtree_spec_bases() {
assert!(
stdout.contains(&base),
"--help does not offer the base {base}",
);
for p in spec_param_docs(&base) {
assert!(
stdout.contains(&format!("{}=", p.key)),
"--help does not offer {}=, which {base} takes",
p.key,
);
assert!(
stdout.contains(&p.values) && stdout.contains(p.default),
"--help does not say what {}= takes and defaults to",
p.key,
);
}
}
}