Refactor generators tests and add missing tests

This commit is contained in:
2026-10-22 20:40:27 +02:00
parent be7acfce1f
commit 783a82fc18
+363 -49
View File
@@ -152,67 +152,342 @@ mod tests {
use std::fmt::Debug;
use super::*;
use crate::models::{FrozenGraph, Graph};
use crate::testing::fixtures::MakeTestGraph;
#[test]
fn petersen_append_graph() {
petersen_add::<AppendGraph>();
#[derive(Debug, Clone, Copy)]
enum Spec {
Path(usize),
Cycle(usize),
Complete(usize),
Petersen,
}
#[test]
fn petersen_graph() {
petersen_add::<Graph>();
impl Spec {
fn generate<G: GraphTopologyAddition>(
&self,
graph: &mut G,
) -> (Vec<G::Vertex>, Vec<G::Edge>) {
match *self {
Spec::Path(n) => path(graph, n),
Spec::Cycle(n) => cycle(graph, n),
Spec::Complete(n) => complete(graph, n),
Spec::Petersen => petersen(graph),
}
}
fn assert<G: GraphTopology>(&self, built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
match *self {
Spec::Path(n) => assert_path(built, n),
Spec::Cycle(n) => assert_cycle(built, n),
Spec::Complete(n) => assert_complete(built, n),
Spec::Petersen => assert_petersen(built),
}
}
}
#[test]
fn petersen_frozen_graph() {
petersen_generate_as::<FrozenGraph>();
struct Built<G: GraphTopology> {
graph: G,
vertices: Vec<G::Vertex>,
edges: Vec<G::Edge>,
initial_vertex_count: usize,
initial_edge_count: usize,
}
fn petersen_generate_as<G: GraphTopologyConversion>()
/// Generates the test for the `append_graph` harness for the given `Spec` expression.
macro_rules! min_harness_tests {
($spec:expr) => {
#[test]
fn append_graph() {
run($spec, add_to_empty::<$crate::models::AppendGraph>);
}
};
}
/// Generates one test per harness for the given `Spec` expression.
macro_rules! all_harness_tests {
($spec:expr) => {
min_harness_tests!($spec);
#[test]
fn append_graph_non_empty() {
run($spec, add_to_standard::<$crate::models::AppendGraph>);
}
#[test]
fn graph() {
run($spec, add_to_empty::<$crate::models::Graph>);
}
#[test]
fn graph_non_empty() {
run($spec, add_to_standard::<$crate::models::Graph>);
}
#[test]
fn frozen_graph() {
run($spec, conv_generate_as::<$crate::models::FrozenGraph>);
}
};
}
/// Generates the module tree `<group>::<case>::<harness>` for each generator case.
///
/// The `all_harnesses` cases run on every harness, the `min_harnesses` cases only on a reduced
/// set of harnesses.
macro_rules! generator_tests {
($($group:ident {
all { $($all_case:ident => $all_generator:expr),+ $(,)? }
min { $($min_case:ident => $min_generator:expr),* $(,)? }
})+) => {
$(
mod $group {
use super::*;
$(
mod $all_case {
use super::*;
all_harness_tests!($all_generator);
}
)+
$(
mod $min_case {
use super::*;
min_harness_tests!($min_generator);
}
)*
}
)+
};
}
generator_tests! {
path {
all {
small => Spec::Path(4),
}
min {
empty => Spec::Path(0),
single_vertex => Spec::Path(1),
single_edge => Spec::Path(2),
large => Spec::Path(21),
}
}
cycle {
all {
small_even => Spec::Cycle(4),
}
min {
empty => Spec::Cycle(0),
loop_edge => Spec::Cycle(1),
multi_edge => Spec::Cycle(2),
large_odd => Spec::Cycle(21),
}
}
complete {
all {
small_even => Spec::Complete(4),
}
min {
empty => Spec::Complete(0),
single_vertex => Spec::Complete(1),
single_edge => Spec::Complete(2),
large_odd => Spec::Complete(13),
}
}
}
mod petersen {
use super::*;
all_harness_tests!(Spec::Petersen);
}
/// Builds a graph with `harness` and asserts that it matches what `spec` should produce.
fn run<G: GraphTopology>(spec: Spec, harness: impl FnOnce(&Spec) -> Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
let (graph, vertices, edges) = generate_as::<G, _, _>(petersen);
assert_petersen(&graph, &vertices, &edges);
let built = harness(&spec);
spec.assert(&built);
}
fn petersen_add<G: GraphTopologyAddition>()
where
G::Vertex: Debug,
G::Edge: Debug,
{
/// Runs `spec` on an empty graph of type `G`.
fn add_to_empty<G: GraphTopologyAddition>(spec: &Spec) -> Built<G> {
let mut graph = G::default();
let (vertices, edges) = petersen(&mut graph);
assert_petersen(&graph, &vertices, &edges);
let (vertices, edges) = spec.generate(&mut graph);
Built {
graph,
vertices,
edges,
initial_vertex_count: 0,
initial_edge_count: 0,
}
}
fn assert_petersen<G: GraphTopology>(graph: &G, vertices: &[G::Vertex], edges: &[G::Edge])
/// Runs `spec` on a non-empty test graph of type `G`.
fn add_to_standard<G: GraphTopologyAddition>(spec: &Spec) -> Built<G> {
let (mut graph, initial_vertices, initial_edges, _) = G::standard();
let (vertices, edges) = spec.generate(&mut graph);
Built {
graph,
vertices,
edges,
initial_vertex_count: initial_vertices.len(),
initial_edge_count: initial_edges.len(),
}
}
/// Runs `spec` within `generate_as` for type `G`.
fn conv_generate_as<G: GraphTopologyConversion>(spec: &Spec) -> Built<G> {
let (graph, vertices, edges) = generate_as::<G>(|graph| spec.generate(graph));
Built {
graph,
vertices,
edges,
initial_vertex_count: 0,
initial_edge_count: 0,
}
}
fn assert_path<G: GraphTopology>(built: &Built<G>, n: usize)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(graph, 10, 15);
assert_handles(graph.vertices(), vertices, "vertex");
assert_handles(graph.edges(), edges, "edge");
assert_no_loops_no_multi_edges(graph);
assert_element_counts(built, n, n.saturating_sub(1));
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
assert_no_loops(built);
assert_no_multi_edges(built);
match n {
0 => {}
1 => assert_eq!(
built.graph.degree(built.vertices[0]),
0,
"path n == 1 should be an isolated vertex"
),
_ => {
for (i, &v) in built.vertices.iter().enumerate() {
assert_eq!(
built.graph.degree(v),
if i == 0 || i == n - 1 { 1 } else { 2 },
"unexpected degree of vertex {v:?} in path n == {n}"
);
}
}
}
assert_path_order(built);
}
fn assert_cycle<G: GraphTopology>(built: &Built<G>, n: usize)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, n, n);
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
if n != 1 {
assert_no_loops(built);
}
if n != 2 {
assert_no_multi_edges(built);
}
assert!(
graph.vertices().all(|v| graph.degree(v) == 3),
"all vertices should have degree 3"
built.vertices.iter().all(|&v| built.graph.degree(v) == 2),
"all new vertices should have degree 2"
);
assert_path_order(built);
// Asserts the closing edge e(n - 1) == (v(n - 1), v0).
if n > 0 {
// For n == 1, u == v and this validates the loop. For n == 2, the above asserted one of
// the two multi-edges, and this asserts the other.
let e = built.edges[n - 1];
let u = built.vertices[0];
let v = built.vertices[n - 1];
let incident_vertices = built.graph.incident_vertices(e);
assert!(
incident_vertices == (u, v) || incident_vertices == (v, u),
"edge {e:?} should have incident vertices {u:?} and {v:?}",
);
}
}
fn assert_complete<G: GraphTopology>(built: &Built<G>, n: usize)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, n, n * n.saturating_sub(1) / 2);
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
// The predicate in `all` is only executed if `vertices` has any elements, i.e. for n > 0.
assert!(
built
.vertices
.iter()
.all(|&v| built.graph.degree(v) == n - 1),
"all new vertices should have degree {}",
n - 1
);
// Asserts that all pairs of vertices have exactly one edge between them. This also includes
// checks for loops and multi-edges. If there are no duplicates, then this implies that all
// expected edges were also matched due to the validated counts from above.
let mut seen = vec![false; n * n];
let position = |v, e| {
built
.vertices
.iter()
.position(|&x| x == v)
.unwrap_or_else(|| panic!("edge {e:?} has an invalid incident vertex {v:?}"))
};
for &e in &built.edges {
let (u, v) = built.graph.incident_vertices(e);
assert_ne!(u, v, "unexpected loop edge {e:?} at vertex {u:?}");
let (i, j) = (position(u, e), position(v, e));
let (i, j) = (i.min(j), i.max(j));
assert!(
!seen[i * n + j],
"unexpected multi-edge {e:?} between vertices {u:?} and {v:?}"
);
seen[i * n + j] = true;
}
}
fn assert_petersen<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, 10, 15);
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
assert_no_loops(built);
assert_no_multi_edges(built);
assert!(
built.vertices.iter().all(|&v| built.graph.degree(v) == 3),
"all new vertices should have degree 3"
);
// Checks that each pair of vertices (u, v) has no common neighbor if they are adjacent, and
// exactly one common neighbor otherwise.
for (i, &v) in vertices.iter().enumerate() {
for &u in &vertices[i + 1..] {
if graph.are_adjacent(v, u) {
for (i, &v) in built.vertices.iter().enumerate() {
for &u in &built.vertices[i + 1..] {
if built.graph.are_adjacent(v, u) {
assert!(
graph.common_neighbors(u, v).next().is_none(),
built.graph.common_neighbors(u, v).next().is_none(),
"adjacent vertices {u:?} and {v:?} should not have a common neighbor"
);
} else {
assert_eq!(
graph.common_neighbors(u, v).count(),
built.graph.common_neighbors(u, v).count(),
1,
"non-adjacent vertices {u:?} and {v:?} should have exactly one common neighbor"
);
@@ -222,19 +497,29 @@ mod tests {
}
fn assert_element_counts<G: GraphTopology>(
graph: &G,
built: &Built<G>,
expected_vertex_count: usize,
expected_edge_count: usize,
) {
assert_eq!(
graph.vertex_count(),
expected_vertex_count,
"unexpected number of vertices"
built.graph.vertex_count(),
built.initial_vertex_count + expected_vertex_count,
"unexpected number of added vertices"
);
assert_eq!(
graph.edge_count(),
built.vertices.len(),
expected_vertex_count,
"unexpected number of returned vertices"
);
assert_eq!(
built.graph.edge_count(),
built.initial_edge_count + expected_edge_count,
"unexpected number of added edges"
);
assert_eq!(
built.edges.len(),
expected_edge_count,
"unexpected number of edges"
"unexpected number of returned edges"
);
}
@@ -253,24 +538,53 @@ mod tests {
}
}
fn assert_no_loops_no_multi_edges<G: GraphTopology>(graph: &G)
fn assert_no_loops<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
for e in graph.edges() {
let (u, v) = graph.incident_vertices(e);
assert_ne!(u, v, "unexpected loop edge {e:?}");
for &e in &built.edges {
let (u, v) = built.graph.incident_vertices(e);
assert_ne!(u, v, "unexpected loop edge {e:?} at vertex {u:?}");
}
for v in graph.vertices() {
}
fn assert_no_multi_edges<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
{
for &v in &built.vertices {
let mut neighbors = Vec::new();
for u in graph.adjacent_vertices(v) {
assert!(
!neighbors.contains(&u),
"unexpected multi-edge between {v:?} and {u:?}"
);
neighbors.push(u);
let mut loop_count = 0;
for u in built.graph.adjacent_vertices(v) {
if u == v {
loop_count += 1;
assert!(loop_count < 3, "unexpected multi-edge loop at vertex {v:?}");
} else {
assert!(
!neighbors.contains(&u),
"unexpected multi-edge between vertices {v:?} and {u:?}"
);
neighbors.push(u);
}
}
}
}
/// Asserts that each returned edge *ei* connects the returned vertices *vi* and *v(i + 1)*.
fn assert_path_order<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
for (w, &e) in built.vertices.windows(2).zip(&built.edges) {
let incident_vertices = built.graph.incident_vertices(e);
assert!(
incident_vertices == (w[0], w[1]) || incident_vertices == (w[1], w[0]),
"edge {e:?} should have incident vertices {:?} and {:?}",
w[0],
w[1]
);
}
}
}