use goatd::{Graph, TreeDecomposition};
#[test]
fn a_gr_file_parses_to_a_canonical_graph_and_writes_back_the_same() {
let text = "c a comment\np tw 4 5\n1 2\n2 1\n3 3\n2 3\n2 3\n";
let graph = Graph::from_gr(text).expect("a well-formed .gr");
assert_eq!(graph.num_vertices(), 4);
assert_eq!(graph.edges(), [(0, 1), (1, 2)]);
assert_eq!(graph.to_gr(), "p tw 4 2\n1 2\n2 3\n");
assert_eq!(
Graph::from_gr(&graph.to_gr()).expect("what to_gr wrote parses"),
graph
);
}
#[test]
fn a_rejected_gr_names_what_is_wrong() {
let cases: &[(&str, &str, &[&str])] = &[
(
"1 2\n",
"an edge before the problem line",
&["problem line"],
),
("p tw 2\n", "a truncated problem line", &["problem line"]),
("p tw 2 1\n0 1\n", "vertex 0", &["vertex 0", "1-based"]),
(
"p tw 2 1\n1 3\n",
"a vertex past the count",
&["vertex 3", "declares 2"],
),
("p tw 2 1\n1\n", "an edge with one endpoint", &["edge line"]),
("p tw x 1\n1 2\n", "a non-numeric vertex count", &[]),
("p tw 2 x\n1 2\n", "a non-numeric edge count", &[]),
(
"p tw 2 1 extra\n1 2\n",
"extra fields on a problem line",
&["problem line"],
),
(
"p tw 2 1\np tw 2 1\n1 2\n",
"a second problem line",
&["more than one problem line"],
),
(
"p tw 2 0\n1 2\n",
"more edge lines than the problem line declares",
&["declares 0 edge lines", "contains 1"],
),
(
"p tw 2 2\n1 2\n",
"fewer edge lines than the problem line declares",
&["declares 2 edge lines", "contains 1"],
),
];
for &(text, what, expected) in cases {
let err = Graph::from_gr(text)
.map(|_| ())
.expect_err(what)
.to_string();
for want in expected {
assert!(
err.contains(want),
"{what}: the message must name {want:?}, got: {err}",
);
}
}
}
#[test]
fn a_td_file_parses_its_bags_and_tree_edges() {
let td_str = "s td 2 2 3\nb 1 2 1\nb 2 3 2\n2 1\n";
let td = TreeDecomposition::from_td(td_str).expect("Should parse");
assert_eq!(td.bags().len(), 2);
assert_eq!(td.bags()[0].vertices(), [0, 1]);
assert_eq!(td.bags()[1].vertices(), [1, 2]);
assert!(
td.adjacency()[0].contains(&1),
"Bag 0 should be adjacent to bag 1"
);
assert!(
td.adjacency()[1].contains(&0),
"Bag 1 should be adjacent to bag 0"
);
}
#[test]
fn td_comments_are_ignored() {
let td_str = "c comment line\ns td 1 2 2\nb 1 1 2\n";
let td = TreeDecomposition::from_td(td_str).expect("Should parse with comments");
assert_eq!(td.bags().len(), 1);
assert_eq!(td.bags()[0].vertices(), [0, 1]);
}
#[test]
fn empty_text_is_not_a_tree_decomposition() {
let result = TreeDecomposition::from_td("");
assert!(result.is_err(), "empty TD should error");
}
#[test]
fn a_solution_line_without_bags_is_not_a_tree_decomposition() {
let result = TreeDecomposition::from_td("s td 2 2 3\n");
assert!(result.is_err(), "TD with header but no bags should error");
}
#[test]
fn blank_lines_are_not_a_tree_decomposition() {
let result = TreeDecomposition::from_td("\n\n\n");
assert!(result.is_err(), "blank lines should give no bags");
}
#[test]
fn a_rejected_td_names_the_offending_id_and_the_count_it_was_checked_against() {
let cases: &[(&str, &str, &[&str])] = &[
(
"s td 2 2 3\nb 0 1 2\nb 2 2 3\n",
"bag id 0",
&["bag id 0", "1-based"],
),
(
"s td 1 2 2\nb 1 0 2\n",
"vertex 0",
&["vertex 0", "1-based"],
),
(
"s td 2 2 3\nb 1 1 2\nb 2 2 3\n0 2\n",
"bag id 0 on a tree edge",
&["bag id 0", "1-based"],
),
(
"s td 1 2 2\nb 5 1 2\n",
"a bag id past the declared bag count",
&["bag id 5", "declares 1"],
),
(
"s td 1 2 2\nb 1 1 9\n",
"a vertex past the declared vertex count",
&["vertex 9", "declares 2"],
),
(
"s td 2 2 3\nb 1 1 2\nb 2 2 3\n1 9\n",
"a tree edge past the declared bag count",
&["bag id 9", "declares 2"],
),
(
"s td 1 1 1\nb 1 1\n1 1\n",
"a self-loop in the bag tree",
&["adjacent to itself", "1 1"],
),
(
"b 1 1 2\n",
"a bag line before the solution line",
&["before the solution line"],
),
("s td x 2 3\nb 1 1 2\n", "a non-numeric bag count", &[]),
(
"s td 1\nb 1 1 2\n",
"a truncated solution line",
&["malformed solution line"],
),
("s td 1 2 2\nb 1 1 x\n", "a non-numeric vertex", &[]),
(
"s not-td 1 2 2\nb 1 1 2\n",
"the wrong solution kind",
&["malformed solution line"],
),
(
"s td 1 x 2\nb 1 1 2\n",
"a non-numeric maximum bag size",
&[],
),
(
"s td 1 1 2\nb 1 1 2\n",
"a bag larger than the declared maximum",
&["maximum bag size 1", "contains 2 vertices"],
),
(
"s td 1 2 1\nb 1 1 1\n",
"a repeated vertex within one bag",
&["bag 1", "vertex 1", "more than once"],
),
(
"s td 1 3 2\nb 1 1 2\n",
"a declared maximum larger than every bag",
&["maximum bag size 3", "contains 2 vertices"],
),
(
"s td 1 1 1\ns td 1 1 1\nb 1 1\n",
"a second solution line",
&["more than one solution line"],
),
(
"s td 1 1 1\nb 1 1\nnot a tree edge\n",
"a malformed tree edge",
&["malformed tree edge"],
),
(
"s td 1 1 1\nb 1 1\none two\n",
"a non-numeric tree edge",
&[],
),
(
"s td 2 1 2\nb 1 1\nb 2 2\n",
"a disconnected bag graph",
&["bag tree has 0 edges", "tree on 2 bags has 1"],
),
(
"s td 3 1 3\nb 1 1\nb 2 2\nb 3 3\n1 2\n1 3\n2 3\n",
"a cycle in the bag graph",
&["bag tree has 3 edges", "tree on 3 bags has 2"],
),
(
"s td 2 1 3\nb 1 1\nb 2 2\n1 2\n",
"a declared vertex absent from every bag",
&["vertex 2", "no bag"],
),
(
"s td 3 2 3\nb 1 1 2\nb 2 2 3\nb 3 1 3\n1 2\n2 3\n",
"a vertex whose bags are disconnected",
&["vertex 0", "not connected"],
),
];
for &(td_str, what, expected) in cases {
let err = TreeDecomposition::from_td(td_str)
.map(|_| ())
.expect_err(what)
.to_string();
for want in expected {
assert!(
err.contains(want),
"{what}: the message must name {want:?}, got: {err}",
);
}
}
}