use std::path::PathBuf;
use std::process::{Command, Output};
use goatd::{Graph, TreeDecomposition};
fn grid_gr() -> String {
let mut edges = Vec::new();
for r in 0..3u32 {
for c in 0..3u32 {
let v = r * 3 + c;
if c + 1 < 3 {
edges.push((v, v + 1));
}
if r + 1 < 3 {
edges.push((v, v + 3));
}
}
}
Graph::new(9, edges).to_gr()
}
fn scratch(test: &str) -> PathBuf {
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("cli_{test}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create the scratch directory");
dir
}
fn goatd(args: &[&str], stdin: Option<&str>) -> Output {
use std::io::Write;
use std::process::Stdio;
let mut child = Command::new(env!("CARGO_BIN_EXE_goatd"))
.args(args)
.stdin(if stdin.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn goatd");
if let Some(text) = stdin {
let write_result = child
.stdin
.take()
.expect("stdin is piped")
.write_all(text.as_bytes());
if let Err(error) = write_result
&& error.kind() != std::io::ErrorKind::BrokenPipe
{
panic!("write the graph to stdin: {error}");
}
}
child.wait_with_output().expect("goatd runs to completion")
}
fn stderr_of(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn every_order_writes_a_valid_decomposition_of_the_grid() {
let dir = scratch("every_order");
let gr = dir.join("grid.gr");
std::fs::write(&gr, grid_gr()).expect("write the graph");
let graph = Graph::from_gr(&grid_gr()).expect("the grid parses");
let orders: &[&[&str]] = &[
&["--order", "minfill"],
&["--order", "minfill", "--ties", "sample", "--seed", "7"],
&["--order", "mindegree"],
&["--order", "mindegree", "--ties", "sample"],
&["--order", "nested-dissection"],
&["--order", "flowcutter", "--steps", "2000"],
&["--order", "portfolio", "--budget", "500"],
&["--order", "minfill", "--refine"],
];
for (i, order) in orders.iter().enumerate() {
let td_path = dir.join(format!("{i}.td"));
let mut args: Vec<&str> = vec![gr.to_str().unwrap(), "--out", td_path.to_str().unwrap()];
args.extend_from_slice(order);
let out = goatd(&args, None);
assert!(
out.status.success(),
"{order:?} failed: {}",
stderr_of(&out)
);
assert!(
out.stdout.is_empty(),
"{order:?} wrote to stdout although --out was given"
);
let text = std::fs::read_to_string(&td_path).expect("the .td was written");
let td = TreeDecomposition::from_td(&text).expect("goatd wrote a well-formed .td");
td.validate(&graph)
.unwrap_or_else(|error| panic!("{order:?}: {error}"));
assert!(
td.treewidth() <= 4,
"{order:?}: width {} on a treewidth-3 grid",
td.treewidth()
);
}
}
#[test]
fn stdin_in_and_stdout_out_by_default() {
let out = goatd(&["-"], Some(&grid_gr()));
assert!(out.status.success(), "{}", stderr_of(&out));
let text = String::from_utf8(out.stdout).expect("utf-8");
assert!(text.starts_with("s td "), "not a .td: {text}");
let td = TreeDecomposition::from_td(&text).expect("a well-formed .td on stdout");
td.validate(&Graph::from_gr(&grid_gr()).unwrap())
.expect("the decomposition written to stdout is valid");
}
#[test]
fn disconnected_and_empty_graphs_are_written_as_one_pace_bag_tree() {
for graph in [Graph::new(4, [(0, 1), (2, 3)]), Graph::new(0, [])] {
let out = goatd(&["-"], Some(&graph.to_gr()));
assert!(out.status.success(), "{}", stderr_of(&out));
let text = String::from_utf8(out.stdout).expect("utf-8");
let td = TreeDecomposition::from_td(&text).expect("one PACE bag tree");
td.validate(&graph)
.expect("the serialized decomposition remains valid");
}
}
#[test]
fn a_seeded_sampling_run_repeats_itself() {
let run = |seed: &str| {
let out = goatd(&["-", "--ties", "sample", "--seed", seed], Some(&grid_gr()));
assert!(out.status.success(), "{}", stderr_of(&out));
out.stdout
};
assert_eq!(run("11"), run("11"));
}
#[test]
fn a_weights_file_biases_the_sample_and_is_checked_against_the_graph() {
let dir = scratch("weights");
let weights = dir.join("w.txt");
std::fs::write(&weights, "1\n2\n3\n4\n5\n6\n7\n8\n9\n").expect("write weights");
let out = goatd(
&[
"-",
"--ties",
"sample",
"--weights",
weights.to_str().unwrap(),
],
Some(&grid_gr()),
);
assert!(out.status.success(), "{}", stderr_of(&out));
std::fs::write(&weights, "1\n2\n").expect("write too few weights");
let out = goatd(
&[
"-",
"--ties",
"sample",
"--weights",
weights.to_str().unwrap(),
],
Some(&grid_gr()),
);
assert_eq!(out.status.code(), Some(1));
let err = stderr_of(&out);
assert!(
err.contains("2 weights") && err.contains("9 vertices"),
"the error must give both counts: {err}"
);
}
#[test]
fn an_unsupported_flag_is_refused_naming_the_flag_and_the_order() {
let cases: &[(&[&str], &[&str])] = &[
(&["--steps", "10"], &["--steps", "minfill", "flowcutter"]),
(
&["--order", "flowcutter", "--seed", "3"],
&["--seed", "flowcutter"],
),
(
&["--order", "nested-dissection", "--ties", "sample"],
&["--ties sample", "nested-dissection", "minfill or mindegree"],
),
(
&["--order", "portfolio", "--ties", "sample"],
&["--ties sample", "portfolio"],
),
(&["--weights", "w.txt"], &["--weights", "--ties sample"]),
(
&["--order", "flowcutter", "--weights", "w.txt"],
&["--weights", "flowcutter", "minfill or mindegree"],
),
(
&["--order", "flowcutter", "--steps", "10", "--budget", "10"],
&["--steps", "--budget"],
),
(&["--ties", "salt"], &["--ties"]),
(&["--budget", "0"], &["--budget", "positive"]),
(&["--order", "treewidth"], &["--order"]),
(&[], &["no input graph"]),
];
for &(flags, expected) in cases {
let mut args: Vec<&str> = if flags.is_empty() { vec![] } else { vec!["-"] };
args.extend_from_slice(flags);
let out = goatd(&args, Some(&grid_gr()));
assert_eq!(
out.status.code(),
Some(2),
"{flags:?} must be a usage error; stderr: {}",
stderr_of(&out)
);
let err = stderr_of(&out);
for want in expected {
assert!(
err.contains(want),
"{flags:?}: must name {want:?}, got: {err}"
);
}
}
}
#[test]
fn a_bad_graph_is_an_error_naming_the_input() {
let out = goatd(&["-"], Some("p tw 2 1\n1 3\n"));
assert_eq!(out.status.code(), Some(1));
let err = stderr_of(&out);
assert!(err.contains("vertex 3"), "{err}");
let out = goatd(&["/nonexistent/graph.gr"], None);
assert_eq!(out.status.code(), Some(1));
assert!(stderr_of(&out).contains("/nonexistent/graph.gr"));
}
#[test]
fn help_prints_the_usage_and_exits_zero() {
let out = goatd(&["--help"], None);
assert!(out.status.success());
let help = String::from_utf8_lossy(&out.stdout);
assert!(help.starts_with("usage: goatd"));
assert!(help.contains("portfolio"));
}