From 8c57f5d2c4048057aaefb9b5428a40c9e994e8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 20:09:35 +0200 Subject: [PATCH 1/5] Fix import order --- src/maps.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/maps.rs b/src/maps.rs index 136b815..2f28cca 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -141,9 +141,10 @@ impl IndexMut for ElementMap { #[cfg(test)] mod tests { - use super::*; use std::rc::Rc; + use super::*; + #[test] fn initial_values_are_default() { let map = ElementMap::new(42, |e| e, 6); -- 2.43.0 From 3fb3e67e8e70656e1e86d43f2f60eee58c88cb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 16 Oct 2026 20:19:08 +0200 Subject: [PATCH 2/5] Add GraphTopology::common_neighbors --- src/testing/graph_topology_testing.rs | 121 ++++++++++++++++++++++++++ src/traits.rs | 52 +++++++++++ 2 files changed, 173 insertions(+) diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 2c4c394..ef8410c 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -49,6 +49,12 @@ macro_rules! graph_topology_tests { incidence_cursor_copy, incident_vertices_incidences_consistency, incident_edges_incidences_consistency, + common_neighbors, + common_neighbors_same_vertex, + common_neighbors_single, + common_neighbors_disconnected, + common_neighbors_multiple_edges, + common_neighbors_loop_edge, ); }; @@ -771,3 +777,118 @@ where ); } } + +pub fn common_neighbors() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // Pairs of vertex indices with the indices of their expected common neighbors. + let cases: [(usize, usize, &[usize]); _] = [ + (0, 3, &[1]), + (1, 6, &[2, 3]), + (5, 8, &[]), + (2, 9, &[5, 6]), + (7, 3, &[]), + (0, 1, &[]), + (6, 7, &[9]), + // Loop only on one side, adjacent. + (2, 6, &[2]), + // Loop only on one side, not adjacent. + (2, 7, &[4]), + // Adjacent through a double edge, and both have a loop. + (2, 4, &[1, 2, 4]), + ]; + for (i, j, expected) in cases { + let expected: Vec<_> = expected.iter().map(|&k| vertices[k]).collect(); + assert_common_neighbors(&graph, vertices[i], vertices[j], &expected); + } +} + +pub fn common_neighbors_same_vertex() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // Vertex 1 has a double edge to vertex 0, but no loop. + assert_common_neighbors( + &graph, + vertices[1], + vertices[1], + &[vertices[0], vertices[2], vertices[3], vertices[4]], + ); + // Vertex 4 has a loop, and is therefore its own common neighbor with itself. + assert_common_neighbors( + &graph, + vertices[4], + vertices[4], + &[ + vertices[1], + vertices[2], + vertices[4], + vertices[7], + vertices[8], + ], + ); +} + +pub fn common_neighbors_single() +where + G::Vertex: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_common_neighbors(&graph, v, v, &[]); +} + +pub fn common_neighbors_disconnected() +where + G::Vertex: Debug, +{ + let (graph, v) = G::disconnected(); + assert_common_neighbors(&graph, v[0], v[0], &[]); + assert_common_neighbors(&graph, v[0], v[1], &[]); +} + +pub fn common_neighbors_multiple_edges() +where + G::Vertex: Debug, +{ + let (graph, [v0, v1], _) = G::multiple_edges::<3>(); + assert_common_neighbors(&graph, v0, v1, &[]); + assert_common_neighbors(&graph, v1, v1, &[v0]); +} + +pub fn common_neighbors_loop_edge() +where + G::Vertex: Debug, +{ + let (graph, v, _) = G::loop_edge(); + assert_common_neighbors(&graph, v, v, &[v]); +} + +/// Asserts that `u` and `v` have exactly the `expected` common neighbors, each yielded once, for +/// both argument orders. +fn assert_common_neighbors( + graph: &G, + u: G::Vertex, + v: G::Vertex, + expected: &[G::Vertex], +) where + G::Vertex: Debug, +{ + for (a, b) in [(u, v), (v, u)] { + let mut remaining = expected.to_vec(); + for w in graph.common_neighbors(a, b) { + let pos = remaining.iter().position(|&x| x == w).unwrap_or_else(|| { + panic!( + "unexpected or repeated common neighbor {w:?} of vertices {a:?} and {b:?} from iterator" + ) + }); + remaining.swap_remove(pos); + } + assert!( + remaining.is_empty(), + "expected common neighbors {remaining:?} of vertices {a:?} and {b:?} were not matched by iterator" + ); + } +} diff --git a/src/traits.rs b/src/traits.rs index 7f27523..005eea3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -175,6 +175,58 @@ pub trait GraphTopology { /// /// Panics if `v` is not a valid vertex of this graph. fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor; + + /// Returns an iterator over all vertices that are adjacent to both `u` and `v`. + /// + /// Every common neighbor is yielded exactly once, in unspecified order, regardless of how + /// many parallel edges connect it to `u` or `v`. This method is symmetric up to permutations, + /// i.e. swapping the arguments will yield the same vertices, but not necessarily in the same + /// order. + /// + /// If `u` has a loop and is adjacent to `v`, then `u` is a common neighbor, and will be yielded + /// exactly once, and analogously for a loop at `v`. + /// + /// If `u == v`, then the common neighbors are all neighbors of `u`, including `u` itself if and + /// only if it has a loop. + /// + /// All neighbors of `u` are collected when this method is called, while those of `v` are + /// traversed lazily. + /// + /// # Panics + /// + /// Panics if `u` or `v` is not a valid vertex of this graph. + /// + /// # Examples + /// + /// ``` + /// use grapherity::models::AppendGraph; + /// use grapherity::prelude::*; + /// + /// let mut graph = AppendGraph::new(); + /// let [u, w, v] = core::array::from_fn(|_| graph.add_vertex()); + /// // Parallel edges do not yield `w` more than once. + /// graph.add_edge(u, w); + /// graph.add_edge(u, w); + /// graph.add_edge(w, v); + /// assert!(graph.common_neighbors(u, v).eq([w])); + /// ``` + // TODO: FrozenGraph could provide a sorted-merge override, if its vertices were sorted. + fn common_neighbors( + &self, + u: Self::Vertex, + v: Self::Vertex, + ) -> impl Iterator { + // TODO: Instead of Vec, this could use a vertex ElementMap with marked neighbors if the product of the degrees of u and v is large compared to |V|. + // TODO: Instead of Vec, this could use a reusable, user-provided(?) "scratch-pad" vertex ElementMap, to not pay its allocation. + let mut u_neighbors: Vec<_> = self.adjacent_vertices(u).collect(); + let mut v_neighbors = self.adjacent_vertices(v); + std::iter::from_fn(move || { + let w = v_neighbors.find(|w| u_neighbors.contains(w))?; + // Removes all parallel entries, so that `w` is not yielded again. + u_neighbors.retain(|x| *x != w); + Some(w) + }) + } } /// A trait that adds construction operations to an undirected graph topology. -- 2.43.0 From f41b77903f1993c68a925de5665788353b641efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 16 Oct 2026 20:23:58 +0200 Subject: [PATCH 3/5] Add docs for MakeTestGraph generator methods --- src/testing/fixtures.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index b118fe7..51382ec 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -5,6 +5,7 @@ use crate::models::{AppendGraph, FrozenGraph}; use crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence}; pub trait MakeTestGraph: GraphTopology + Sized { + /// Returns a versatile, pre-defined standard test graph. // Since this is only a test fixture, the complex return type here is deemed acceptable. #[allow(clippy::type_complexity)] fn standard() -> ( @@ -14,20 +15,32 @@ pub trait MakeTestGraph: GraphTopology + Sized { [Vec>; 10], ); + /// Returns an empty graph. fn empty() -> Self; + /// Returns a graph with a single vertex and no edges. fn single_vertex() -> (Self, Self::Vertex); + /// Returns a graph with two vertices connected by a single edge. fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge); + /// Returns a graph with vertices *v0*, *v1*, and *v2*, where *v0* is isolated, and the other + /// two are connected by a single edge. fn disconnected() -> (Self, [Self::Vertex; 3]); + /// Returns a graph with a single vertex that has a single loop edge. fn loop_edge() -> (Self, Self::Vertex, Self::Edge); + /// Returns a graph with two vertices that are connected by `K` multi-edges. fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]); + /// Returns a graph that is a path with three vertices and two edges between them, returned in + /// order. fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]); + /// Returns a graph that is a path with three vertices *v0*, *v1*, and *v2*, and two edges + /// *e0 = (v0, v1)* and *e1 = (v1, v2)* between them, and two loops on the first two vertices + /// *e2 = (v0, v0)* and *e3 = (v1, v1)*. fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]); } -- 2.43.0 From 5b6ad13f0e174d80a85e60431fdb48e8aa00e9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 16 Oct 2026 20:25:40 +0200 Subject: [PATCH 4/5] Add generators::generate_as --- src/generators.rs | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/generators.rs b/src/generators.rs index 4457fea..5dc36e7 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1,6 +1,48 @@ //! Generators for common graphs. -use crate::traits::{GraphTopology, GraphTopologyAddition}; +use crate::models::AppendGraph; +use crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyConversion}; + +/// Builds a graph of type `G` by running a generator on a temporary [`AppendGraph`]. +/// +/// `generate_as` fills a temporary graph and converts it to `G`. It returns the handles of the +/// elements it created as handles of the resulting graph. This makes it possible to obtain graph +/// types directly that do not implement [`GraphTopologyAddition`], such as [`FrozenGraph`]. +/// +/// # Examples +/// +/// ``` +/// use grapherity::generators; +/// use grapherity::models::FrozenGraph; +/// use grapherity::traits::GraphTopology; +/// +/// let (graph, vertices, edges) = generators::generate_as::(generators::petersen); +/// assert_eq!(graph.vertex_count(), 10); +/// assert_eq!(graph.edge_count(), 15); +/// assert!(graph.vertices().all(|v| graph.degree(v) == 3)); +/// ``` +/// +/// [`FrozenGraph`]: crate::models::FrozenGraph +pub fn generate_as( + generator: impl FnOnce( + &mut AppendGraph, + ) -> ( + [::Vertex; V], + [::Edge; E], + ), +) -> (G, [G::Vertex; V], [G::Edge; E]) +where + G: GraphTopologyConversion, +{ + let mut builder = AppendGraph::new(); + let (vertices, edges) = generator(&mut builder); + let (graph, iso) = G::convert(&builder); + ( + graph, + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), + ) +} /// Generates a Petersen graph. /// @@ -43,7 +85,6 @@ where #[cfg(test)] mod tests { use super::*; - use crate::models::AppendGraph; #[test] fn petersen() { -- 2.43.0 From 3ae0aa9b67d441b46f2572695897e649d6de56f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 16 Oct 2026 20:28:48 +0200 Subject: [PATCH 5/5] Update generators unit tests to cover generate_as and better validate Petersen graph --- src/generators.rs | 129 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 19 deletions(-) diff --git a/src/generators.rs b/src/generators.rs index 5dc36e7..5eb62b7 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -84,37 +84,128 @@ where #[cfg(test)] mod tests { + use std::fmt::Debug; + use super::*; + use crate::models::{FrozenGraph, Graph}; #[test] - fn petersen() { - let mut graph = AppendGraph::new(); - let (vertices, edges) = super::petersen(&mut graph); - assert_eq!( - graph.vertex_count(), - 10, - "unexpected number of added vertices" - ); - assert_eq!(graph.edge_count(), 15, "unexpected number of added edges"); + fn petersen_append_graph() { + petersen_add::(); + } + + #[test] + fn petersen_graph() { + petersen_add::(); + } + + #[test] + fn petersen_frozen_graph() { + petersen_generate_as::(); + } + + fn petersen_generate_as() + where + G::Vertex: Debug, + G::Edge: Debug, + { + let (graph, vertices, edges) = generate_as::(petersen); + assert_petersen(&graph, &vertices, &edges); + } + + fn petersen_add() + where + G::Vertex: Debug, + G::Edge: Debug, + { + let mut graph = G::default(); + let (vertices, edges) = petersen(&mut graph); + assert_petersen(&graph, &vertices, &edges); + } + + fn assert_petersen(graph: &G, vertices: &[G::Vertex], edges: &[G::Edge]) + 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!( graph.vertices().all(|v| graph.degree(v) == 3), "all vertices should have degree 3" ); - assert_handles_in_graph(&graph, &vertices, &edges); + // 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) { + assert!( + 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(), + 1, + "non-adjacent vertices {u:?} and {v:?} should have exactly one common neighbor" + ); + } + } + } } - fn assert_handles_in_graph( + fn assert_element_counts( graph: &G, - vertices: &[G::Vertex], - edges: &[G::Edge], + expected_vertex_count: usize, + expected_edge_count: usize, ) { - assert!( - vertices.iter().all(|&v| graph.vertices().any(|u| u == v)), - "all returned vertices should be in the graph" + assert_eq!( + graph.vertex_count(), + expected_vertex_count, + "unexpected number of vertices" ); - assert!( - edges.iter().all(|&e| graph.edges().any(|f| f == e)), - "all returned edges should be in the graph" + assert_eq!( + graph.edge_count(), + expected_edge_count, + "unexpected number of edges" ); } + + /// Asserts that every handle in `returned` is unique and in `all`. + fn assert_handles( + all: impl Iterator, + returned: &[T], + kind: &str, + ) { + let mut remaining: Vec = all.collect(); + for e in returned { + let Some(position) = remaining.iter().position(|x| x == e) else { + panic!("returned {kind} {e:?} is not in the graph or appears more than once"); + }; + remaining.swap_remove(position); + } + } + + fn assert_no_loops_no_multi_edges(graph: &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 v in graph.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); + } + } + } } -- 2.43.0