From fb9dfe8280b23717409553ea9014d2a0766a9972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 10 Jul 2026 11:58:45 +0200 Subject: [PATCH 01/67] Renamed len() to capacity() in EntityMap, VertexMap, and EdgeMap len() is generally expected to describe the number of elements in a collection, but in this case elements beyond len() are accessible without restriction. capacity() is a better name because it describes data storage size. --- src/maps.rs | 14 +++++++------- src/testing/maps_testing.rs | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/maps.rs b/src/maps.rs index b7c24ef..72b8a29 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -13,9 +13,9 @@ impl VertexMap { } } - // Reads beyond len() are valid and return the default value. - pub fn len(&self) -> usize { - self.inner.len() + // Reads beyond capacity() are valid and return the default value. + pub fn capacity(&self) -> usize { + self.inner.capacity() } pub fn sync>(&mut self, graph: &G) { @@ -48,9 +48,9 @@ impl EdgeMap { } } - // Reads beyond len() are valid and return the default value. - pub fn len(&self) -> usize { - self.inner.len() + // Reads beyond capacity() are valid and return the default value. + pub fn capacity(&self) -> usize { + self.inner.capacity() } pub fn sync>(&mut self, graph: &G) { @@ -87,7 +87,7 @@ impl EntityMap { } } - pub fn len(&self) -> usize { + pub fn capacity(&self) -> usize { self.data.len() } diff --git a/src/testing/maps_testing.rs b/src/testing/maps_testing.rs index 202b2db..4bf9be4 100644 --- a/src/testing/maps_testing.rs +++ b/src/testing/maps_testing.rs @@ -53,16 +53,16 @@ macro_rules! vertex_map_tests { let mut graph = <$T>::new(); graph.add_vertex(); let mut map = graph.vertex_map(42); - let initial_len = map.len(); + let initial_len = map.capacity(); while graph.vertex_capacity() <= initial_len { graph.add_vertex(); } assert!( - map.len() < graph.vertex_capacity(), + map.capacity() < graph.vertex_capacity(), "precondition: map is stale before sync" ); map.sync(&graph); - assert_eq!(map.len(), graph.vertex_capacity()); + assert_eq!(map.capacity(), graph.vertex_capacity()); } #[test] @@ -109,7 +109,7 @@ macro_rules! vertex_map_deletion_tests { let capacity_before = graph.vertex_capacity(); graph.delete_vertex(v2); map.sync(&graph); - assert_eq!(map.len(), capacity_before); + assert_eq!(map.capacity(), capacity_before); assert_eq!(map[v1], 5); } @@ -197,16 +197,16 @@ macro_rules! edge_map_tests { let v2 = graph.add_vertex(); graph.add_edge(v1, v2); let mut map = graph.edge_map(42); - let initial_len = map.len(); + let initial_len = map.capacity(); while graph.edge_capacity() <= initial_len { graph.add_edge(v1, v2); } assert!( - map.len() < graph.edge_capacity(), + map.capacity() < graph.edge_capacity(), "precondition: map is stale before sync" ); map.sync(&graph); - assert_eq!(map.len(), graph.edge_capacity()); + assert_eq!(map.capacity(), graph.edge_capacity()); } #[test] @@ -259,7 +259,7 @@ macro_rules! edge_map_deletion_tests { let capacity_before = graph.edge_capacity(); graph.delete_edge(e2); map.sync(&graph); - assert_eq!(map.len(), capacity_before); + assert_eq!(map.capacity(), capacity_before); assert_eq!(map[e1], 5); } -- 2.43.0 From bf6a7142f34497c8fbb1c2d09703ec315459ce72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 17 Jul 2026 09:50:43 +0200 Subject: [PATCH 02/67] Fix readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8ce1bc..1b37b81 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This library is still experimental and its API may therefore change frequently. This project is dual-licensed under the terms of both: -- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/license/mit) You may choose either license for your use of this software. -- 2.43.0 From 24c08438d6a7435891394578660736fa62f227a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 17 Jul 2026 09:52:35 +0200 Subject: [PATCH 03/67] Add IncidenceCursor to prelude --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 46964c6..bffe3a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ pub mod models; pub mod traits; pub mod prelude { - pub use crate::traits::{GraphTopology, GraphTopologyDeletion}; + pub use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor}; } mod testing; -- 2.43.0 From 8c6c98859cb3caf3b87c0029ec6ed674ffe87666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 17 Jul 2026 09:53:05 +0200 Subject: [PATCH 04/67] Add Copy to IncidenceCursor trait --- src/traits.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traits.rs b/src/traits.rs index e3112bb..91924bf 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -5,7 +5,7 @@ use crate::maps::{EdgeMap, VertexMap}; pub trait GraphTopology { type Vertex: Copy + Eq; type Edge: Copy + Eq; - type IncidenceCursor: IncidenceCursor + Copy; + type IncidenceCursor: IncidenceCursor; fn vertex_count(&self) -> usize; fn vertex_capacity(&self) -> usize; @@ -31,6 +31,6 @@ pub trait GraphTopologyDeletion: GraphTopology { fn delete_edge(&mut self, e: Self::Edge); } -pub trait IncidenceCursor { +pub trait IncidenceCursor: Copy { fn next(&mut self, graph: &G) -> Option<(G::Vertex, G::Edge)>; } -- 2.43.0 From 913a9e0baa753de75906a1ec270685ccb938a8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 17 Jul 2026 09:54:16 +0200 Subject: [PATCH 05/67] Fix AppendGraph::capacity to actually use the underlying vec's capacity --- src/models/append_graph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 9ccfef7..2487f23 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -91,7 +91,7 @@ impl GraphTopology for AppendGraph { } fn vertex_capacity(&self) -> usize { - self.vertices.len() + self.vertices.capacity() } fn vertex_map(&self, default: T) -> VertexMap { -- 2.43.0 From ad9850800d322603672f36ef9a42d6bf914f9bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 17 Jul 2026 11:30:45 +0200 Subject: [PATCH 06/67] Update IncidenceCursor trait doc with note on Copy --- src/traits.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/traits.rs b/src/traits.rs index 096a316..9978912 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -210,6 +210,9 @@ pub trait GraphTopologyDeletion: GraphTopology { /// A cursor for traversing the incidences of a vertex one step at a time. /// +/// Because the cursor is [`Copy`], its state can be saved and restored to replay or branch a +/// traversal. +/// /// Cursors are obtained via [`GraphTopology::incidence_cursor`]. See /// [`GraphTopology::IncidenceCursor`] for guidance on when to prefer a cursor over /// [`GraphTopology::incidences`]. -- 2.43.0 From 8db7a86187c5af677c3dd73f0930605348df3361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 18 Jul 2026 23:38:08 +0200 Subject: [PATCH 07/67] Add Petersen graph generator --- src/generators.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 66 insertions(+) create mode 100644 src/generators.rs diff --git a/src/generators.rs b/src/generators.rs new file mode 100644 index 0000000..d257851 --- /dev/null +++ b/src/generators.rs @@ -0,0 +1,65 @@ +use crate::traits::GraphTopology; + +pub fn petersen( + graph: &mut G, +) -> ( + [::Vertex; 10], + [::Edge; 15], +) +where + G: GraphTopology, +{ + const N: usize = 5; + const K: usize = 2; + + let vertices = core::array::from_fn(|_| graph.add_vertex()); + let edges = core::array::from_fn(|j| { + let i = j / 3; + match j % 3 { + 0 => { + let outer = if i < N - 1 { i + 1 } else { 0 }; + graph.add_edge(vertices[i], vertices[outer]) + } + 1 => { + let inner = if i < N - K { i + N + K } else { i + K }; + graph.add_edge(vertices[i + N], vertices[inner]) + } + _ => graph.add_edge(vertices[i], vertices[i + N]), + } + }); + (vertices, edges) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::AppendGraph; + + #[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"); + assert!( + graph.vertices().all(|v| graph.degree(v) == 3), + "all vertices should have degree 3" + ); + assert_handles_in_graph(&graph, &vertices, &edges); + } + + fn assert_handles_in_graph( + graph: &G, + vertices: &[G::Vertex], + edges: &[G::Edge], + ) { + assert!( + vertices.iter().all(|&v| graph.vertices().any(|u| u == v)), + "all returned vertices should be in the graph" + ); + assert!( + edges.iter().all(|&e| graph.edges().any(|f| f == e)), + "all returned edges should be in the graph" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index ffb77ee..593f258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,6 +128,7 @@ //! [`GraphTopologyDeletion`]: crate::traits::GraphTopologyDeletion pub mod algorithms; +pub mod generators; pub mod maps; pub mod models; pub mod traits; -- 2.43.0 From d711e6857702cc780fda00b52ecd9bf53d4261da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 22 Jul 2026 13:30:09 +0200 Subject: [PATCH 08/67] Replace VertexMap and EdgeMap with now public EntityMap --- src/algorithms.rs | 28 +++--- src/lib.rs | 5 +- src/maps.rs | 105 ++++++-------------- src/models/append_graph.rs | 14 +-- src/models/graph.rs | 14 +-- src/testing/bfs_testing.rs | 4 +- src/testing/dfs_testing.rs | 6 +- src/testing/dijkstra_testing.rs | 10 +- src/testing/maps_testing.rs | 170 ++------------------------------ src/traits.rs | 11 ++- tests/maps.rs | 23 +---- 11 files changed, 90 insertions(+), 300 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index b974441..f2370c0 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use std::collections::{BinaryHeap, VecDeque}; -use crate::maps::VertexMap; +use crate::maps::EntityMap; use crate::traits::{GraphTopology, IncidenceCursor}; #[derive(PartialEq, Eq)] @@ -23,8 +23,8 @@ impl Ord for DistanceOrderedVertex { } pub struct DijkstraResult { - pub distances: VertexMap>, - pub predecessors: VertexMap>, + pub distances: EntityMap>, + pub predecessors: EntityMap>, } // TODO: Generalize the return type of the weight function. @@ -54,7 +54,7 @@ pub fn dijkstra_distances( graph: &G, source: G::Vertex, weight: W, -) -> VertexMap> +) -> EntityMap> where G: GraphTopology, W: Fn(G::Edge) -> u32, @@ -65,7 +65,7 @@ where pub fn dijkstra_distances_unweighted( graph: &G, source: G::Vertex, -) -> VertexMap> +) -> EntityMap> where G: GraphTopology, { @@ -77,7 +77,7 @@ fn dijkstra_impl( source: G::Vertex, weight: W, mut on_relax: F, -) -> VertexMap> +) -> EntityMap> where G: GraphTopology, W: Fn(G::Edge) -> u32, @@ -113,8 +113,8 @@ where } pub struct BfsResult { - pub distances: VertexMap>, - pub predecessors: VertexMap>, + pub distances: EntityMap>, + pub predecessors: EntityMap>, } pub fn bfs(graph: &G, source: G::Vertex) -> BfsResult @@ -132,7 +132,7 @@ where } } -pub fn bfs_distances(graph: &G, source: G::Vertex) -> VertexMap> +pub fn bfs_distances(graph: &G, source: G::Vertex) -> EntityMap> where G: GraphTopology, { @@ -158,7 +158,7 @@ where } struct BfsImplResult { - distances: VertexMap>, + distances: EntityMap>, found: Option<(V, u32)>, } @@ -195,8 +195,8 @@ where } pub struct DfsResult { - pub visited: VertexMap, - pub predecessors: VertexMap>, + pub visited: EntityMap, + pub predecessors: EntityMap>, } pub fn dfs(graph: &G, source: G::Vertex) -> DfsResult @@ -214,7 +214,7 @@ where } } -pub fn dfs_visited(graph: &G, source: G::Vertex) -> VertexMap +pub fn dfs_visited(graph: &G, source: G::Vertex) -> EntityMap where G: GraphTopology, { @@ -240,7 +240,7 @@ where } struct DfsImplResult { - visited: VertexMap, + visited: EntityMap, found: Option, } diff --git a/src/lib.rs b/src/lib.rs index 593f258..b8bc1ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ //! //! * Undirected graph types [`Graph`] and [`AppendGraph`] built on a flat, index-based adjacency //! list, -//! * [`VertexMap`] and [`EdgeMap`] to freely associate custom data with vertices and edges, +//! * [`EntityMap`] to freely associate custom data with vertices and edges, //! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different //! variants, //! * Exposed traits to implement custom graph data structures or algorithms. @@ -118,8 +118,7 @@ //! [BFS]: crate::algorithms::bfs //! [Dijkstra's algorithm]: crate::algorithms::dijkstra //! [DFS]: crate::algorithms::dfs -//! [`EdgeMap`]: crate::maps::EdgeMap -//! [`VertexMap`]: crate::maps::VertexMap +//! [`EntityMap`]: crate::maps::EntityMap //! [`AppendGraph`]: crate::models::AppendGraph //! [`Graph`]: crate::models::Graph //! [`GraphTopology`]: crate::traits::GraphTopology diff --git a/src/maps.rs b/src/maps.rs index 699587d..7cabb0a 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -1,84 +1,32 @@ +//! Provides maps to associate custom data to graph vertices and edges. +//! +//! [`EntityMap`] provides a copy-on-write, [`Vec`]-backed map to associate data to either all +//! vertices or all edges in a graph. + use std::ops::{Index, IndexMut}; -use crate::traits::GraphTopology; - -pub struct VertexMap { - inner: EntityMap, -} - -impl VertexMap { - pub fn new(default: T, to_index: fn(V) -> usize, capacity: usize) -> Self { - Self { - inner: EntityMap::new(default, to_index, capacity), - } - } - - // Reads beyond 'capacity' are valid and return the default value. - pub fn capacity(&self) -> usize { - self.inner.capacity() - } - - pub fn sync>(&mut self, graph: &G) { - self.inner.resize(graph.vertex_capacity()); - } -} - -impl Index for VertexMap { - type Output = T; - - fn index(&self, v: V) -> &T { - &self.inner[v] - } -} - -impl IndexMut for VertexMap { - fn index_mut(&mut self, v: V) -> &mut T { - &mut self.inner[v] - } -} - -pub struct EdgeMap { - inner: EntityMap, -} - -impl EdgeMap { - pub fn new(default: T, to_index: fn(E) -> usize, capacity: usize) -> Self { - Self { - inner: EntityMap::new(default, to_index, capacity), - } - } - - // Reads beyond 'capacity' are valid and return the default value. - pub fn capacity(&self) -> usize { - self.inner.capacity() - } - - pub fn sync>(&mut self, graph: &G) { - self.inner.resize(graph.edge_capacity()); - } -} - -impl Index for EdgeMap { - type Output = T; - - fn index(&self, e: E) -> &T { - &self.inner[e] - } -} - -impl IndexMut for EdgeMap { - fn index_mut(&mut self, e: E) -> &mut T { - &mut self.inner[e] - } -} - -struct EntityMap { +/// A map to associate custom data to graph vertices or edges. +/// +/// This map uses raw entity indices to associate homogenous custom data of type `T` to graph +/// vertices or edges. The implementation uses a [`Vec`], allocating contiguous slots for the data, +/// which means that the provided index conversion function should map entities to contiguous +/// indices, or indices with relatively few gaps. +/// +/// Data allocation happens as copy-on-write, i.e. the backing [`Vec`] is only resized if a value +/// beyond current capacity is written, but the `default` value can transparently be read. No bound +/// or validity checks are performed by the map on the provided entities. +/// +/// Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] to obtain an `EntityMap` for +/// vertices or edges, respectively. +pub struct EntityMap { data: Vec, default: T, to_index: fn(E) -> usize, } impl EntityMap { + /// Creates a new map with the given `default` value, an index conversion function `to_index`, + /// and an initial `capacity`. pub fn new(default: T, to_index: fn(E) -> usize, capacity: usize) -> Self { Self { data: vec![default.clone(); capacity], @@ -87,11 +35,18 @@ impl EntityMap { } } + /// Returns the total number of data entries the map can write without reallocating. Reads + /// beyond `capacity` are valid and return the default value. pub fn capacity(&self) -> usize { - self.data.len() + self.data.capacity() } - pub fn resize(&mut self, capacity: usize) { + /// Extends the internal data storage of the map to `capacity`, pre-filling any new slots with + /// the default value. + /// + /// Use this before writing data for new graph entities to avoid incremental growth on the first + /// write to each new entity. + pub fn extend(&mut self, capacity: usize) { if capacity > self.data.len() { self.data.resize(capacity, self.default.clone()); } diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 2487f23..9cbdbca 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -1,4 +1,4 @@ -use crate::maps::{EdgeMap, VertexMap}; +use crate::maps::EntityMap; use crate::traits::{GraphTopology, IncidenceCursor}; #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -7,8 +7,8 @@ pub struct Vertex(usize); #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Edge(usize); -pub type AppendGraphVertexMap = VertexMap; -pub type AppendGraphEdgeMap = EdgeMap; +pub type AppendGraphVertexMap = EntityMap; +pub type AppendGraphEdgeMap = EntityMap; impl Edge { fn normalize(&self) -> Self { @@ -94,8 +94,8 @@ impl GraphTopology for AppendGraph { self.vertices.capacity() } - fn vertex_map(&self, default: T) -> VertexMap { - VertexMap::new(default, |v| v.0, self.vertex_capacity()) + fn vertex_map(&self, default: T) -> EntityMap { + EntityMap::new(default, |v| v.0, self.vertex_capacity()) } fn edge_count(&self) -> usize { @@ -106,8 +106,8 @@ impl GraphTopology for AppendGraph { self.incidences.len() / 2 } - fn edge_map(&self, default: T) -> EdgeMap { - EdgeMap::new(default, |e| e.0 / 2, self.edge_capacity()) + fn edge_map(&self, default: T) -> EntityMap { + EntityMap::new(default, |e| e.0 / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { diff --git a/src/models/graph.rs b/src/models/graph.rs index ed86205..89ad70e 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -1,13 +1,13 @@ use typed_generational_arena::{Arena, Index}; -use crate::maps::{EdgeMap, VertexMap}; +use crate::maps::EntityMap; use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor}; pub type Vertex = Index; pub type Edge = Index; -pub type GraphVertexMap = VertexMap; -pub type GraphEdgeMap = EdgeMap; +pub type GraphVertexMap = EntityMap; +pub type GraphEdgeMap = EntityMap; #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct VertexSlot(usize); @@ -163,8 +163,8 @@ impl GraphTopology for Graph { self.vertices.capacity() } - fn vertex_map(&self, default: T) -> VertexMap { - VertexMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) + fn vertex_map(&self, default: T) -> EntityMap { + EntityMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) } fn edge_count(&self) -> usize { @@ -175,8 +175,8 @@ impl GraphTopology for Graph { self.incidences.capacity() / 2 } - fn edge_map(&self, default: T) -> EdgeMap { - EdgeMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) + fn edge_map(&self, default: T) -> EntityMap { + EntityMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { diff --git a/src/testing/bfs_testing.rs b/src/testing/bfs_testing.rs index 709ac9f..233a9e7 100644 --- a/src/testing/bfs_testing.rs +++ b/src/testing/bfs_testing.rs @@ -194,7 +194,7 @@ macro_rules! bfs_tests { } fn assert_bfs_distances( - distances: &$crate::maps::VertexMap< + distances: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -222,7 +222,7 @@ macro_rules! bfs_tests { } fn assert_bfs_predecessors( - predecessors: &$crate::maps::VertexMap< + predecessors: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option<<$T as $crate::traits::GraphTopology>::Vertex>, >, diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs index 78238c8..eb59676 100644 --- a/src/testing/dfs_testing.rs +++ b/src/testing/dfs_testing.rs @@ -163,7 +163,7 @@ macro_rules! dfs_tests { } fn assert_dfs_visited( - visited: &$crate::maps::VertexMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, + visited: &$crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], ) { for i in 0..10 { @@ -177,8 +177,8 @@ macro_rules! dfs_tests { fn assert_dfs_predecessors( graph: &$T, - visited: &$crate::maps::VertexMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, - predecessors: &$crate::maps::VertexMap< + visited: &$crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, + predecessors: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option<<$T as $crate::traits::GraphTopology>::Vertex>, >, diff --git a/src/testing/dijkstra_testing.rs b/src/testing/dijkstra_testing.rs index 3815c4e..f53eee1 100644 --- a/src/testing/dijkstra_testing.rs +++ b/src/testing/dijkstra_testing.rs @@ -131,7 +131,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_single_vertex( - distances: &$crate::maps::VertexMap< + distances: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -160,7 +160,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_disconnected( - distances: &$crate::maps::VertexMap< + distances: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -204,7 +204,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_test_graph( - distances: &$crate::maps::VertexMap< + distances: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -261,7 +261,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_unweighted_test_graph( - distances: &$crate::maps::VertexMap< + distances: &$crate::maps::EntityMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -300,7 +300,7 @@ macro_rules! dijkstra_tests { <$T as $crate::traits::GraphTopology>::Vertex, <$T as $crate::traits::GraphTopology>::Edge, )>; 10], - $crate::maps::EdgeMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, + $crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, ) { use $crate::traits::GraphTopology; let (graph, vertices, edges, incidences) = make_test_graph(); diff --git a/src/testing/maps_testing.rs b/src/testing/maps_testing.rs index e8b5362..c3155bc 100644 --- a/src/testing/maps_testing.rs +++ b/src/testing/maps_testing.rs @@ -1,6 +1,6 @@ #[doc(hidden)] #[macro_export] -macro_rules! vertex_map_tests { +macro_rules! entity_map_tests { ($T:ty) => { #[test] fn initial_values_are_default() { @@ -48,7 +48,7 @@ macro_rules! vertex_map_tests { } #[test] - fn sync_expands_to_new_vertices() { + fn extend_to_new_vertices() { use $crate::traits::GraphTopology; let mut graph = <$T>::new(); graph.add_vertex(); @@ -59,21 +59,21 @@ macro_rules! vertex_map_tests { } assert!( map.capacity() < graph.vertex_capacity(), - "precondition: map is stale before sync" + "precondition: map is stale before extend" ); - map.sync(&graph); + map.extend(graph.vertex_capacity()); assert_eq!(map.capacity(), graph.vertex_capacity()); } #[test] - fn sync_does_not_overwrite_existing_values() { + fn extend_does_not_overwrite_existing_values() { use $crate::traits::GraphTopology; let mut graph = <$T>::new(); let v = graph.add_vertex(); let mut map = graph.vertex_map(0); map[v] = 5; graph.add_vertex(); - map.sync(&graph); + map.extend(graph.vertex_capacity()); assert_eq!(map[v], 5); } }; @@ -81,7 +81,7 @@ macro_rules! vertex_map_tests { #[doc(hidden)] #[macro_export] -macro_rules! vertex_map_deletion_tests { +macro_rules! entity_map_deletion_tests { ($T:ty) => { #[test] fn surviving_vertex_readable_after_delete() { @@ -108,7 +108,7 @@ macro_rules! vertex_map_deletion_tests { map[v1] = 5; let capacity_before = graph.vertex_capacity(); graph.delete_vertex(v2); - map.sync(&graph); + map.extend(graph.vertex_capacity()); assert_eq!(map.capacity(), capacity_before); assert_eq!(map[v1], 5); } @@ -124,162 +124,10 @@ macro_rules! vertex_map_deletion_tests { map[v1] = 99; graph.delete_vertex(v1); let v2 = graph.add_vertex(); - // VertexMap uses raw indices, not vertex identity. A new vertex v2 reusing the slot + // EntityMap uses raw indices, not vertex identity. A new vertex v2 reusing the slot // of previously deleted v1 sees the old value. Callers must reinitialize stale slots // after deletion. assert_eq!(map[v2], 99); } }; } - -#[doc(hidden)] -#[macro_export] -macro_rules! edge_map_tests { - ($T:ty) => { - #[test] - fn initial_values_are_default() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let e2 = graph.add_edge(v1, v2); - let map = graph.edge_map(42); - assert_eq!(map[e1], 42); - assert_eq!(map[e2], 42); - } - - #[test] - fn write_and_read() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let e2 = graph.add_edge(v1, v2); - let mut map = graph.edge_map(0); - map[e1] = 7; - assert_eq!(map[e1], 7); - assert_eq!(map[e2], 0); - } - - #[test] - fn lazy_growth_on_read() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - graph.add_edge(v1, v2); - let map = graph.edge_map(99); - let e = graph.add_edge(v1, v2); - assert_eq!(map[e], 99); - } - - #[test] - fn lazy_growth_on_write() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let mut map = graph.edge_map(0); - let e2 = graph.add_edge(v1, v2); - map[e2] = 7; - assert_eq!(map[e1], 0); - assert_eq!(map[e2], 7); - } - - #[test] - fn sync_expands_to_new_edges() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - graph.add_edge(v1, v2); - let mut map = graph.edge_map(42); - let capacity_before = map.capacity(); - while graph.edge_capacity() <= capacity_before { - graph.add_edge(v1, v2); - } - assert!( - map.capacity() < graph.edge_capacity(), - "precondition: map is stale before sync" - ); - map.sync(&graph); - assert_eq!(map.capacity(), graph.edge_capacity()); - } - - #[test] - fn sync_does_not_overwrite_existing_values() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - let mut map = graph.edge_map(0); - map[e] = 5; - graph.add_edge(v1, v2); - map.sync(&graph); - assert_eq!(map[e], 5); - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! edge_map_deletion_tests { - ($T:ty) => { - #[test] - fn surviving_edge_readable_after_delete() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let e2 = graph.add_edge(v1, v2); - let mut map = graph.edge_map(0); - map[e1] = 1; - map[e2] = 2; - graph.delete_edge(e2); - assert_eq!(map[e1], 1); - } - - #[test] - fn capacity_does_not_shrink_after_delete() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let e2 = graph.add_edge(v1, v2); - let mut map = graph.edge_map(0); - map[e1] = 5; - let capacity_before = graph.edge_capacity(); - graph.delete_edge(e2); - map.sync(&graph); - assert_eq!(map.capacity(), capacity_before); - assert_eq!(map[e1], 5); - } - - #[test] - fn reused_slot_returns_old_value() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - graph.add_edge(v1, v2); - let e1 = graph.add_edge(v1, v2); - let mut map = graph.edge_map(0); - map[e1] = 99; - graph.delete_edge(e1); - let e2 = graph.add_edge(v1, v2); - // EdgeMap uses raw indices, not edge identity. Because to_index uses arr_idx/2, - // both halves of a deleted edge pair map to the same index, so a new edge reusing - // either slot sees the old value. Callers must reinitialize stale slots after deletion. - assert_eq!(map[e2], 99); - } - }; -} diff --git a/src/traits.rs b/src/traits.rs index 9978912..9493b4b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,4 +1,4 @@ -use crate::maps::{EdgeMap, VertexMap}; +use crate::maps::EntityMap; // TODO: Add functions to reserve memory for vertices and edges. // TODO: Split out GraphTopologyAddition trait. @@ -46,7 +46,8 @@ pub trait GraphTopology { /// Returns the total number of vertices the graph can hold without reallocating. fn vertex_capacity(&self) -> usize; - /// Creates and returns a [`VertexMap`] with every slot initialised to `default`. + /// Creates and returns an [`EntityMap`] for the vertices with every slot initialised to + /// `default`. /// /// # Examples /// @@ -66,7 +67,7 @@ pub trait GraphTopology { /// labels[v2] = "b"; /// assert_eq!(labels[v2], "b"); /// ``` - fn vertex_map(&self, default: T) -> VertexMap; + fn vertex_map(&self, default: T) -> EntityMap; /// Returns the number of edges in the graph. fn edge_count(&self) -> usize; @@ -74,7 +75,7 @@ pub trait GraphTopology { /// Returns the total number of edges the graph can hold without reallocating. fn edge_capacity(&self) -> usize; - /// Creates and returns an [`EdgeMap`] with every slot initialised to `default`. + /// Creates and returns an [`EntityMap`] for the edges with every slot initialised to `default`. /// /// # Examples /// @@ -96,7 +97,7 @@ pub trait GraphTopology { /// weights[e2] = 2; /// assert_eq!(weights[e2], 2); /// ``` - fn edge_map(&self, default: T) -> EdgeMap; + fn edge_map(&self, default: T) -> EntityMap; /// Returns the degree of `v`, i.e. the number of incident edges, where each loop contributes 2. /// diff --git a/tests/maps.rs b/tests/maps.rs index b936db9..73fe87d 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -1,25 +1,12 @@ -mod append_graph_vertex_map_tests { +mod append_graph_entity_map_tests { use grapherity::models::AppendGraph; - grapherity::vertex_map_tests!(AppendGraph); + grapherity::entity_map_tests!(AppendGraph); } -mod append_graph_edge_map_tests { - use grapherity::models::AppendGraph; - - grapherity::edge_map_tests!(AppendGraph); -} - -mod graph_vertex_map_tests { +mod graph_entity_map_tests { use grapherity::models::Graph; - grapherity::vertex_map_tests!(Graph); - grapherity::vertex_map_deletion_tests!(Graph); -} - -mod graph_edge_map_tests { - use grapherity::models::Graph; - - grapherity::edge_map_tests!(Graph); - grapherity::edge_map_deletion_tests!(Graph); + grapherity::entity_map_tests!(Graph); + grapherity::entity_map_deletion_tests!(Graph); } -- 2.43.0 From c253e4a84019ea747fe10f773ff62d08c056cd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 22 Jul 2026 13:31:15 +0200 Subject: [PATCH 09/67] Add tests for GraphTopology::vertex_map and edge_map --- src/testing/graph_topology_testing.rs | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 400290c..d949a5e 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -128,6 +128,19 @@ macro_rules! graph_topology_tests { assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); } + #[test] + fn vertex_map() { + use $crate::traits::GraphTopology; + let mut graph = <$T>::new(); + let v1 = graph.add_vertex(); + let mut map = graph.vertex_map(27); + assert_eq!(map[v1], 27, "unexpected value from default map read"); + map[v1] = 9; + assert_eq!(map[v1], 9, "unexpected value from map after write"); + let v2 = graph.add_vertex(); + assert_eq!(map[v2], 27, "unexpected value from default map read for new vertex"); + } + #[test] fn add_edge() { use $crate::traits::GraphTopology; @@ -152,6 +165,21 @@ macro_rules! graph_topology_tests { assert_eq!(graph.edge_count(), 18, "unexpected edge count"); } + #[test] + fn edge_map() { + use $crate::traits::GraphTopology; + let mut graph = <$T>::new(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + let e1 = graph.add_edge(v1, v2); + let mut map = graph.edge_map(27); + assert_eq!(map[e1], 27, "unexpected value from default map read"); + map[e1] = 9; + assert_eq!(map[e1], 9, "unexpected value from map after write"); + let e2 = graph.add_edge(v1, v2); + assert_eq!(map[e2], 27, "unexpected value from default map read for new vertex"); + } + #[test] fn degree_zero() { use $crate::traits::GraphTopology; -- 2.43.0 From 9608796f23cfe4172c93755ca7067f1a34f0040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 22 Jul 2026 13:44:32 +0200 Subject: [PATCH 10/67] Move dfs_find_path tests into dfs_testing --- src/testing.rs | 1 - src/testing/dfs_testing.rs | 119 +++++++++++++++++++++++++++++ src/testing/find_path_testing.rs | 124 ------------------------------- tests/find_path.rs | 13 ---- 4 files changed, 119 insertions(+), 138 deletions(-) delete mode 100644 src/testing/find_path_testing.rs delete mode 100644 tests/find_path.rs diff --git a/src/testing.rs b/src/testing.rs index a345512..c8ded87 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,6 +1,5 @@ pub(crate) mod bfs_testing; pub(crate) mod dfs_testing; pub(crate) mod dijkstra_testing; -pub(crate) mod find_path_testing; pub(crate) mod graph_topology_testing; pub(crate) mod maps_testing; diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs index eb59676..d302773 100644 --- a/src/testing/dfs_testing.rs +++ b/src/testing/dfs_testing.rs @@ -202,5 +202,124 @@ macro_rules! dfs_tests { ); } } + + #[test] + fn dfs_find_path_source_equals_target() { + use $crate::traits::GraphTopology; + let mut graph = <$T>::new(); + let v = graph.add_vertex(); + assert_eq!( + $crate::algorithms::dfs_find_path(&graph, v, v), + Some(vec![]), + "path from source to itself should be empty" + ); + } + + #[test] + fn dfs_find_path_disconnected() { + let (graph, vertices) = make_test_graph_disconnected(); + assert_eq!( + $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[1]), + None, + "no path should exist to disconnected vertex" + ); + } + + #[test] + fn dfs_find_path_adjacent() { + use $crate::traits::GraphTopology; + let mut graph = <$T>::new(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + let e = graph.add_edge(v1, v2); + let path = $crate::algorithms::dfs_find_path(&graph, v1, v2) + .expect("path should exist between adjacent vertices"); + assert_eq!( + path.len(), + 1, + "unexpected path length between adjacent vertices" + ); + assert_eq!(path[0], e, "path should use the connecting edge"); + } + + #[test] + fn dfs_find_path() { + let (graph, vertices, _, _) = make_test_graph(); + let path = $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[9]) + .expect(&format!( + "path should exist between connected vertices {:?} and {:?}", + vertices[0], vertices[9] + )); + assert_valid_path(&graph, &path, vertices[0], vertices[9]); + } + + #[test] + fn dfs_find_path_where_source_matches() { + use $crate::traits::GraphTopology; + let mut graph = <$T>::new(); + let v = graph.add_vertex(); + assert_eq!( + $crate::algorithms::dfs_find_path_where(&graph, v, |u| u == v), + Some(vec![]), + "path from source to itself should be empty" + ); + } + + #[test] + fn dfs_find_path_where_disconnected() { + let (graph, vertices) = make_test_graph_disconnected(); + assert_eq!( + $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), + None, + "no path should exist to disconnected vertex" + ); + } + + #[test] + fn dfs_find_path_where_no_match() { + let (graph, vertices, _, _) = make_test_graph(); + assert_eq!( + $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |_| false), + None, + "no path should exist when predicate never matches" + ); + } + + #[test] + fn dfs_find_path_where() { + let (graph, vertices, _, _) = make_test_graph(); + let path = + $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]) + .expect(&format!( + "path should exist between connected vertices {:?} and {:?}", + vertices[0], vertices[9] + )); + assert_valid_path(&graph, &path, vertices[0], vertices[9]); + } + + fn assert_valid_path( + graph: &$T, + path: &[<$T as $crate::traits::GraphTopology>::Edge], + source: <$T as $crate::traits::GraphTopology>::Vertex, + target: <$T as $crate::traits::GraphTopology>::Vertex, + ) { + use $crate::traits::GraphTopology; + assert!(!path.is_empty(), "path should be non-empty"); + // Walks the path: tracks current vertex, confirm each edge is incident to it. + let mut current = source; + for (i, &e) in path.iter().enumerate() { + let (v1, v2) = graph.incident_vertices(e); + assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); + assert!( + v1 == current || v2 == current, + "path edge {e:?} (index {i}, from {v1:?} to {v2:?}) is not incident to current path vertex {current:?}" + ); + current = if v1 == current { v2 } else { v1 }; + } + assert_eq!( + current, target, + "path should end at target {target:?}, but ended at {current:?}" + ); + } }; } diff --git a/src/testing/find_path_testing.rs b/src/testing/find_path_testing.rs deleted file mode 100644 index c9c4e42..0000000 --- a/src/testing/find_path_testing.rs +++ /dev/null @@ -1,124 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! find_path_tests { - ($T:ty) => { - #[test] - fn find_path_source_equals_target() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - assert_eq!( - $crate::algorithms::dfs_find_path(&graph, v, v), - Some(vec![]), - "path from source to itself should be empty" - ); - } - - #[test] - fn find_path_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); - assert_eq!( - $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[1]), - None, - "no path should exist to disconnected vertex" - ); - } - - #[test] - fn find_path_adjacent() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - let path = $crate::algorithms::dfs_find_path(&graph, v1, v2) - .expect("path should exist between adjacent vertices"); - assert_eq!( - path.len(), - 1, - "unexpected path length between adjacent vertices" - ); - assert_eq!(path[0], e, "path should use the connecting edge"); - } - - #[test] - fn find_path() { - let (graph, vertices, _, _) = make_test_graph(); - let path = $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[9]) - .expect(&format!( - "path should exist between connected vertices {:?} and {:?}", - vertices[0], vertices[9] - )); - assert_valid_path(&graph, &path, vertices[0], vertices[9]); - } - - #[test] - fn find_path_where_source_matches() { - use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - assert_eq!( - $crate::algorithms::dfs_find_path_where(&graph, v, |u| u == v), - Some(vec![]), - "path from source to itself should be empty" - ); - } - - #[test] - fn find_path_where_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); - assert_eq!( - $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), - None, - "no path should exist to disconnected vertex" - ); - } - - #[test] - fn find_path_where_no_match() { - let (graph, vertices, _, _) = make_test_graph(); - assert_eq!( - $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |_| false), - None, - "no path should exist when predicate never matches" - ); - } - - #[test] - fn find_path_where() { - let (graph, vertices, _, _) = make_test_graph(); - let path = - $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]) - .expect(&format!( - "path should exist between connected vertices {:?} and {:?}", - vertices[0], vertices[9] - )); - assert_valid_path(&graph, &path, vertices[0], vertices[9]); - } - - fn assert_valid_path( - graph: &$T, - path: &[<$T as $crate::traits::GraphTopology>::Edge], - source: <$T as $crate::traits::GraphTopology>::Vertex, - target: <$T as $crate::traits::GraphTopology>::Vertex, - ) { - use $crate::traits::GraphTopology; - assert!(!path.is_empty(), "path should be non-empty"); - // Walks the path: tracks current vertex, confirm each edge is incident to it. - let mut current = source; - for (i, &e) in path.iter().enumerate() { - let (v1, v2) = graph.incident_vertices(e); - assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); - assert!( - v1 == current || v2 == current, - "path edge {e:?} (index {i}, from {v1:?} to {v2:?}) is not incident to current path vertex {current:?}" - ); - current = if v1 == current { v2 } else { v1 }; - } - assert_eq!( - current, target, - "path should end at target {target:?}, but ended at {current:?}" - ); - } - }; -} diff --git a/tests/find_path.rs b/tests/find_path.rs deleted file mode 100644 index 7f296c8..0000000 --- a/tests/find_path.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod append_graph_tests { - use grapherity::models::AppendGraph; - - grapherity::graph_topology_test_fixtures!(AppendGraph); - grapherity::find_path_tests!(AppendGraph); -} - -mod graph_tests { - use grapherity::models::Graph; - - grapherity::graph_topology_test_fixtures!(Graph); - grapherity::find_path_tests!(Graph); -} -- 2.43.0 From 3708c1e0d122d3739e1d8c7786507daa20a86063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 22 Jul 2026 13:45:46 +0200 Subject: [PATCH 11/67] Fix formatting --- src/generators.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/generators.rs b/src/generators.rs index d257851..774e317 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -39,7 +39,11 @@ mod tests { 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.vertex_count(), + 10, + "unexpected number of added vertices" + ); assert_eq!(graph.edge_count(), 15, "unexpected number of added edges"); assert!( graph.vertices().all(|v| graph.degree(v) == 3), -- 2.43.0 From 399b36bb16597606ff976f17a72d3cdefccf9d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 24 Jul 2026 10:13:15 +0200 Subject: [PATCH 12/67] Add documentation for Graph and AppendGraph EntityMap types --- src/models/append_graph.rs | 3 +++ src/models/graph.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 9cbdbca..c767566 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -7,7 +7,10 @@ pub struct Vertex(usize); #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Edge(usize); +/// An [`EntityMap`] for [`AppendGraph`] vertices. pub type AppendGraphVertexMap = EntityMap; + +/// An [`EntityMap`] for [`AppendGraph`] edges. pub type AppendGraphEdgeMap = EntityMap; impl Edge { diff --git a/src/models/graph.rs b/src/models/graph.rs index 89ad70e..21bd4d9 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -6,7 +6,10 @@ use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor}; pub type Vertex = Index; pub type Edge = Index; +/// An [`EntityMap`] for [`Graph`] vertices. pub type GraphVertexMap = EntityMap; + +/// An [`EntityMap`] for [`Graph`] edges. pub type GraphEdgeMap = EntityMap; #[derive(Copy, Clone, PartialEq, Eq, Debug)] -- 2.43.0 From afaf1613445728ef65339f1a04ad61d035d4fd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 31 Jul 2026 10:23:47 +0200 Subject: [PATCH 13/67] Rename 'weight' parameter in Dijkstra's algorithm to 'weights' --- src/algorithms.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index e04b4ff..a1e0088 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -64,7 +64,7 @@ pub struct DijkstraResult { /// [Dijkstra's algorithm] with custom edge weights, returns minimum distances and predecessors. /// /// Calculates the shortest paths from `source` to all vertices in `graph` with edge weights given -/// by `weight` function. Returns the distances from `source` to each vertex, and the predecessors +/// by `weights` function. Returns the distances from `source` to each vertex, and the predecessors /// of each vertex on some shortest path from `source` to that vertex. Returns `None` for any vertex /// not connected to `source`. /// @@ -91,13 +91,13 @@ pub struct DijkstraResult { /// ``` /// /// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm -pub fn dijkstra(graph: &G, source: G::Vertex, weight: W) -> DijkstraResult +pub fn dijkstra(graph: &G, source: G::Vertex, weights: W) -> DijkstraResult where G: GraphTopology, W: Fn(G::Edge) -> u32, { let mut predecessors = graph.vertex_map(None); - let distances = dijkstra_impl(graph, source, weight, |adjacent, predecessor| { + let distances = dijkstra_impl(graph, source, weights, |adjacent, predecessor| { predecessors[adjacent] = Some(predecessor); }); DijkstraResult { @@ -109,7 +109,7 @@ where /// [Dijkstra's algorithm] with custom edge weights, returns minimum distances. /// /// Calculates the shortest paths from `source` to all vertices in `graph` with edge weights given -/// by `weight` function. Returns the distances from `source` to each vertex. Returns `None` for any +/// by `weights` function. Returns the distances from `source` to each vertex. Returns `None` for any /// vertex not connected to `source`. /// /// # Panics @@ -137,13 +137,13 @@ where pub fn dijkstra_distances( graph: &G, source: G::Vertex, - weight: W, + weights: W, ) -> EntityMap> where G: GraphTopology, W: Fn(G::Edge) -> u32, { - dijkstra_impl(graph, source, weight, |_, _| {}) + dijkstra_impl(graph, source, weights, |_, _| {}) } /// [Dijkstra's algorithm] with unit edge weights, returns minimum distances and predecessors. @@ -223,7 +223,7 @@ where fn dijkstra_impl( graph: &G, source: G::Vertex, - weight: W, + weights: W, mut on_relax: F, ) -> EntityMap> where @@ -242,7 +242,7 @@ where while let Some(v) = heap.pop() { for incidence in graph.incidences(v.vertex) { - let new_distance = distances[v.vertex].unwrap() + weight(incidence.1); + let new_distance = distances[v.vertex].unwrap() + weights(incidence.1); if match distances[incidence.0] { None => true, Some(old_distance) if old_distance > new_distance => true, -- 2.43.0 From 53a6bed9323b9b49195a51469406af441fef0002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 31 Jul 2026 10:24:51 +0200 Subject: [PATCH 14/67] Add todos --- src/algorithms.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/algorithms.rs b/src/algorithms.rs index a1e0088..17fb72f 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -61,6 +61,7 @@ pub struct DijkstraResult { } // TODO: Generalize the return type of the weight function. +// TODO: Add complexity information for Dijkstra's algorithm variants. /// [Dijkstra's algorithm] with custom edge weights, returns minimum distances and predecessors. /// /// Calculates the shortest paths from `source` to all vertices in `graph` with edge weights given @@ -459,6 +460,7 @@ where } } +// TODO: 'visited' is already encoded in 'predecessors', except for 'source', which is visited, but has no predecessor. /// Return data type for [`dfs`]. pub struct DfsResult { /// Vertex map indicating which vertices were visited during the search. -- 2.43.0 From 110e6310aab8fc7ed573232670ac95f450573bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 6 Aug 2026 09:44:28 +0200 Subject: [PATCH 15/67] Use PriorityQueue for Dijkstra's, requires Hash for Vertex --- Cargo.toml | 1 + src/algorithms.rs | 59 ++++++++++++++++---------------------- src/models/append_graph.rs | 4 +-- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 513fad2..4129fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ keywords = ["graph", "graph-algorithms"] categories = ["algorithms", "data-structures", "mathematics"] [dependencies] +priority-queue = "2.7.0" typed-generational-arena = "0.2.9" diff --git a/src/algorithms.rs b/src/algorithms.rs index 17fb72f..2386965 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -28,30 +28,14 @@ //! [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search //! [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm -use std::cmp::Ordering; -use std::collections::{BinaryHeap, VecDeque}; +use priority_queue::PriorityQueue; +use std::cmp::Reverse; +use std::collections::VecDeque; +use std::hash::Hash; use crate::maps::EntityMap; use crate::traits::{GraphTopology, IncidenceCursor}; -#[derive(PartialEq, Eq)] -struct DistanceOrderedVertex { - distance: u32, - vertex: V, -} - -impl PartialOrd for DistanceOrderedVertex { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for DistanceOrderedVertex { - fn cmp(&self, other: &Self) -> Ordering { - other.distance.cmp(&self.distance) - } -} - /// Return data type for [`dijkstra`] and [`dijkstra_unweighted`]. pub struct DijkstraResult { /// Vertex map of minimum distances from a given `source` vertex. @@ -61,7 +45,7 @@ pub struct DijkstraResult { } // TODO: Generalize the return type of the weight function. -// TODO: Add complexity information for Dijkstra's algorithm variants. +// TODO: A Fibonacci heap would lower complexity to O(|E| + |V| log |V|) by making decrease-key O(1) amortized instead of O(log |V|). No standard Rust implementation exists; high constant factors may negate the asymptotic gain in practice. /// [Dijkstra's algorithm] with custom edge weights, returns minimum distances and predecessors. /// /// Calculates the shortest paths from `source` to all vertices in `graph` with edge weights given @@ -69,6 +53,8 @@ pub struct DijkstraResult { /// of each vertex on some shortest path from `source` to that vertex. Returns `None` for any vertex /// not connected to `source`. /// +/// Time complexity is *O((|V| + |E|) log |V|)*, space complexity is *O(|V|)*. +/// /// # Panics /// /// Panics if `source` is not a valid vertex of `graph`. @@ -95,6 +81,7 @@ pub struct DijkstraResult { pub fn dijkstra(graph: &G, source: G::Vertex, weights: W) -> DijkstraResult where G: GraphTopology, + G::Vertex: Hash, W: Fn(G::Edge) -> u32, { let mut predecessors = graph.vertex_map(None); @@ -113,6 +100,8 @@ where /// by `weights` function. Returns the distances from `source` to each vertex. Returns `None` for any /// vertex not connected to `source`. /// +/// Time complexity is *O((|V| + |E|) log |V|)*, space complexity is *O(|V|)*. +/// /// # Panics /// /// Panics if `source` is not a valid vertex of `graph`. @@ -142,6 +131,7 @@ pub fn dijkstra_distances( ) -> EntityMap> where G: GraphTopology, + G::Vertex: Hash, W: Fn(G::Edge) -> u32, { dijkstra_impl(graph, source, weights, |_, _| {}) @@ -153,6 +143,8 @@ where /// the distances from `source` to each vertex, and the predecessors of each vertex on some shortest /// path from `source` to that vertex. Returns `None` for any vertex not connected to `source`. /// +/// Time complexity is *O((|V| + |E|) log |V|)*, space complexity is *O(|V|)*. +/// /// Prefer [`bfs`] for unweighted graphs, it has better time and space complexity. /// /// # Panics @@ -179,6 +171,7 @@ where pub fn dijkstra_unweighted(graph: &G, source: G::Vertex) -> DijkstraResult where G: GraphTopology, + G::Vertex: Hash, { dijkstra(graph, source, |_| 1) } @@ -189,6 +182,8 @@ where /// the distances from `source` to each vertex. Returns `None` for any vertex not connected to /// `source`. /// +/// Time complexity is *O((|V| + |E|) log |V|)*, space complexity is *O(|V|)*. +/// /// Prefer [`bfs_distances`] for unweighted graphs, it has better time and space complexity. /// /// # Panics @@ -217,6 +212,7 @@ pub fn dijkstra_distances_unweighted( ) -> EntityMap> where G: GraphTopology, + G::Vertex: Hash, { dijkstra_distances(graph, source, |_| 1) } @@ -229,32 +225,27 @@ fn dijkstra_impl( ) -> EntityMap> where G: GraphTopology, + G::Vertex: Hash, W: Fn(G::Edge) -> u32, F: FnMut(G::Vertex, G::Vertex), { let mut distances = graph.vertex_map(None); - let mut heap = BinaryHeap::new(); + let mut heap = PriorityQueue::new(); distances[source] = Some(0); - heap.push(DistanceOrderedVertex { - vertex: source, - distance: 0, - }); + heap.push(source, Reverse(0u32)); - while let Some(v) = heap.pop() { - for incidence in graph.incidences(v.vertex) { - let new_distance = distances[v.vertex].unwrap() + weights(incidence.1); + while let Some((v, Reverse(v_distance))) = heap.pop() { + for incidence in graph.incidences(v) { + let new_distance = v_distance + weights(incidence.1); if match distances[incidence.0] { None => true, Some(old_distance) if old_distance > new_distance => true, _ => false, } { distances[incidence.0] = Some(new_distance); - on_relax(incidence.0, v.vertex); - heap.push(DistanceOrderedVertex { - vertex: incidence.0, - distance: new_distance, - }); + on_relax(incidence.0, v); + heap.push_increase(incidence.0, Reverse(new_distance)); } } } diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 471997d..bc0d76a 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -7,14 +7,14 @@ use crate::traits::{GraphTopology, IncidenceCursor}; /// /// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// [`AppendGraph::add_vertex`] and [`AppendGraph::vertices`]. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Vertex(usize); /// An opaque handle identifying an edge in an [`AppendGraph`]. /// /// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// [`AppendGraph::add_edge`] and [`AppendGraph::edges`]. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Edge(usize); /// An [`EntityMap`] for [`AppendGraph`] vertices. -- 2.43.0 From 2851cf60fdce15899dcd18f90159b273f8ed209a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 6 Aug 2026 09:57:51 +0200 Subject: [PATCH 16/67] Add Incidence struct for GraphTopology to use --- src/algorithms.rs | 19 ++-- src/lib.rs | 2 +- src/models.rs | 6 +- src/models/append_graph.rs | 19 +++- src/models/graph.rs | 27 +++-- src/testing/dijkstra_testing.rs | 10 +- src/testing/graph_topology_testing.rs | 157 ++++++++++++++------------ src/traits.rs | 24 +++- 8 files changed, 154 insertions(+), 110 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index 2386965..2da4d09 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -34,7 +34,7 @@ use std::collections::VecDeque; use std::hash::Hash; use crate::maps::EntityMap; -use crate::traits::{GraphTopology, IncidenceCursor}; +use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; /// Return data type for [`dijkstra`] and [`dijkstra_unweighted`]. pub struct DijkstraResult { @@ -236,16 +236,16 @@ where heap.push(source, Reverse(0u32)); while let Some((v, Reverse(v_distance))) = heap.pop() { - for incidence in graph.incidences(v) { - let new_distance = v_distance + weights(incidence.1); - if match distances[incidence.0] { + for Incidence { vertex: u, edge: e } in graph.incidences(v) { + let new_distance = v_distance + weights(e); + if match distances[u] { None => true, Some(old_distance) if old_distance > new_distance => true, _ => false, } { - distances[incidence.0] = Some(new_distance); - on_relax(incidence.0, v); - heap.push_increase(incidence.0, Reverse(new_distance)); + distances[u] = Some(new_distance); + on_relax(u, v); + heap.push_increase(u, Reverse(new_distance)); } } } @@ -741,7 +741,10 @@ where None => { stack.pop(); } - Some((neighbor, edge)) => { + Some(Incidence { + vertex: neighbor, + edge, + }) => { if predicate(neighbor) { let mut path: Vec = stack.iter().filter_map(|f| f.arrival_edge).collect(); diff --git a/src/lib.rs b/src/lib.rs index 062db19..e902d00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,7 +136,7 @@ pub mod traits; /// Convenience re-exports of graph topology traits for common use. pub mod prelude { - pub use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor}; + pub use crate::traits::{GraphTopology, GraphTopologyDeletion, Incidence, IncidenceCursor}; } mod testing; diff --git a/src/models.rs b/src/models.rs index 4eb163e..1f302c8 100644 --- a/src/models.rs +++ b/src/models.rs @@ -5,5 +5,7 @@ pub mod graph; // TODO: Compressed-sparse-row graph model. -pub use append_graph::{AppendGraph, AppendGraphEdgeMap, AppendGraphVertexMap}; -pub use graph::{Graph, GraphEdgeMap, GraphVertexMap}; +pub use append_graph::{ + AppendGraph, AppendGraphEdgeMap, AppendGraphIncidence, AppendGraphVertexMap, +}; +pub use graph::{Graph, GraphEdgeMap, GraphIncidence, GraphVertexMap}; diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index bc0d76a..00c2d36 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -1,7 +1,7 @@ //! [`AppendGraph`], an undirected graph topology supporting addition only. use crate::maps::EntityMap; -use crate::traits::{GraphTopology, IncidenceCursor}; +use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in an [`AppendGraph`]. /// @@ -23,6 +23,9 @@ pub type AppendGraphVertexMap = EntityMap; /// An [`EntityMap`] for [`AppendGraph`] edges. pub type AppendGraphEdgeMap = EntityMap; +/// An [`Incidence`] for [`AppendGraph`]. +pub type AppendGraphIncidence = Incidence; + impl Edge { fn normalize(&self) -> Self { Self(self.0 & !1) @@ -50,10 +53,13 @@ pub struct AppendGraphIncidenceCursor { } impl IncidenceCursor for AppendGraphIncidenceCursor { - fn next(&mut self, graph: &AppendGraph) -> Option<(Vertex, Edge)> { + fn next(&mut self, graph: &AppendGraph) -> Option { graph .step_incidence(&mut self.incidence) - .map(|(v, e)| (v, e.normalize())) + .map(|(v, e)| Incidence { + vertex: v, + edge: e.normalize(), + }) } } @@ -191,8 +197,11 @@ impl GraphTopology for AppendGraph { self.raw_incidences(v).map(|(_, e)| e.normalize()) } - fn incidences(&self, v: Self::Vertex) -> impl Iterator { - self.raw_incidences(v).map(|(v, e)| (v, e.normalize())) + fn incidences(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|(v, e)| Incidence { + vertex: v, + edge: e.normalize(), + }) } fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor { diff --git a/src/models/graph.rs b/src/models/graph.rs index e07ddd6..39454eb 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -3,7 +3,7 @@ use typed_generational_arena::{Arena, Index}; use crate::maps::EntityMap; -use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor}; +use crate::traits::{GraphTopology, GraphTopologyDeletion, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in a [`Graph`]. /// @@ -23,6 +23,9 @@ pub type GraphVertexMap = EntityMap; /// An [`EntityMap`] for [`Graph`] edges. pub type GraphEdgeMap = EntityMap; +/// An [`Incidence`] for [`Graph`]. +pub type GraphIncidence = Incidence; + #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct VertexSlot(usize); @@ -66,13 +69,13 @@ pub struct GraphIncidenceCursor { } impl IncidenceCursor for GraphIncidenceCursor { - fn next(&mut self, graph: &Graph) -> Option<(Vertex, Edge)> { - graph.step_incidence(&mut self.incidence).map(|(vs, e)| { - ( - graph.vertices.get_idx(vs.0).unwrap(), - graph.normalize_edge(e), - ) - }) + fn next(&mut self, graph: &Graph) -> Option { + graph + .step_incidence(&mut self.incidence) + .map(|(vs, e)| Incidence { + vertex: graph.vertices.get_idx(vs.0).unwrap(), + edge: graph.normalize_edge(e), + }) } } @@ -297,9 +300,11 @@ impl GraphTopology for Graph { self.raw_incidences(v).map(|(_, e)| self.normalize_edge(e)) } - fn incidences(&self, v: Self::Vertex) -> impl Iterator { - self.raw_incidences(v) - .map(|(vs, e)| (self.vertices.get_idx(vs.0).unwrap(), self.normalize_edge(e))) + fn incidences(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|(vs, e)| Incidence { + vertex: self.vertices.get_idx(vs.0).unwrap(), + edge: self.normalize_edge(e), + }) } fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor { diff --git a/src/testing/dijkstra_testing.rs b/src/testing/dijkstra_testing.rs index f53eee1..3b06326 100644 --- a/src/testing/dijkstra_testing.rs +++ b/src/testing/dijkstra_testing.rs @@ -296,10 +296,12 @@ macro_rules! dijkstra_tests { <$T as $crate::traits::GraphTopology>::Vertex, <$T as $crate::traits::GraphTopology>::Vertex, ); 18], - [Vec<( - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Edge, - )>; 10], + [Vec< + $crate::traits::Incidence< + <$T as $crate::traits::GraphTopology>::Vertex, + <$T as $crate::traits::GraphTopology>::Edge, + >, + >; 10], $crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, ) { use $crate::traits::GraphTopology; diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 0faf451..5ee3be3 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -10,12 +10,12 @@ macro_rules! graph_topology_test_fixtures { <$T as $crate::traits::GraphTopology>::Vertex, <$T as $crate::traits::GraphTopology>::Vertex, ); 18], - [Vec<( + [Vec<$crate::traits::Incidence< <$T as $crate::traits::GraphTopology>::Vertex, <$T as $crate::traits::GraphTopology>::Edge, - )>; 10], + >>; 10], ) { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, Incidence}; let mut graph = <$T>::new(); let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 10] = core::array::from_fn(|_| graph.add_vertex()); @@ -40,50 +40,54 @@ macro_rules! graph_topology_test_fixtures { (vertices[7], vertices[9]), ] .map(|(v1, v2)| (graph.add_edge(v1, v2), v1, v2)); - let incidences: [Vec<_>; 10] = [ - vec![(vertices[1], edges[0].0), (vertices[1], edges[1].0)], + let i = |vertex, edge| Incidence { vertex, edge }; + let incidences: [Vec::Vertex, + <$T as GraphTopology>::Edge, + >>; 10] = [ + vec![i(vertices[1], edges[0].0), i(vertices[1], edges[1].0)], vec![ - (vertices[0], edges[0].0), - (vertices[0], edges[1].0), - (vertices[2], edges[2].0), - (vertices[3], edges[3].0), - (vertices[4], edges[4].0), + i(vertices[0], edges[0].0), + i(vertices[0], edges[1].0), + i(vertices[2], edges[2].0), + i(vertices[3], edges[3].0), + i(vertices[4], edges[4].0), ], vec![ - (vertices[2], edges[5].0), - (vertices[2], edges[5].0), - (vertices[1], edges[2].0), - (vertices[4], edges[6].0), - (vertices[4], edges[7].0), - (vertices[5], edges[8].0), - (vertices[6], edges[9].0), + i(vertices[2], edges[5].0), + i(vertices[2], edges[5].0), + i(vertices[1], edges[2].0), + i(vertices[4], edges[6].0), + i(vertices[4], edges[7].0), + i(vertices[5], edges[8].0), + i(vertices[6], edges[9].0), ], - vec![(vertices[1], edges[3].0), (vertices[6], edges[10].0)], + vec![i(vertices[1], edges[3].0), i(vertices[6], edges[10].0)], vec![ - (vertices[1], edges[4].0), - (vertices[2], edges[6].0), - (vertices[2], edges[7].0), - (vertices[4], edges[11].0), - (vertices[4], edges[11].0), - (vertices[7], edges[12].0), - (vertices[8], edges[13].0), + i(vertices[1], edges[4].0), + i(vertices[2], edges[6].0), + i(vertices[2], edges[7].0), + i(vertices[4], edges[11].0), + i(vertices[4], edges[11].0), + i(vertices[7], edges[12].0), + i(vertices[8], edges[13].0), ], - vec![(vertices[2], edges[8].0), (vertices[9], edges[14].0)], + vec![i(vertices[2], edges[8].0), i(vertices[9], edges[14].0)], vec![ - (vertices[2], edges[9].0), - (vertices[3], edges[10].0), - (vertices[9], edges[15].0), + i(vertices[2], edges[9].0), + i(vertices[3], edges[10].0), + i(vertices[9], edges[15].0), ], vec![ - (vertices[4], edges[12].0), - (vertices[8], edges[16].0), - (vertices[9], edges[17].0), + i(vertices[4], edges[12].0), + i(vertices[8], edges[16].0), + i(vertices[9], edges[17].0), ], - vec![(vertices[4], edges[13].0), (vertices[7], edges[16].0)], + vec![i(vertices[4], edges[13].0), i(vertices[7], edges[16].0)], vec![ - (vertices[5], edges[14].0), - (vertices[6], edges[15].0), - (vertices[7], edges[17].0), + i(vertices[5], edges[14].0), + i(vertices[6], edges[15].0), + i(vertices[7], edges[17].0), ], ]; (graph, vertices, edges, incidences) @@ -540,7 +544,7 @@ macro_rules! graph_topology_tests { "unexpected incident edge count for vertex {:?}", vertices[i] ); - let mut expected: Vec<_> = incidences[i].iter().map(|(_, e)| *e).collect(); + let mut expected: Vec<_> = incidences[i].iter().map(|x| x.edge).collect(); for e in graph.incident_edges(vertices[i]) { let pos = expected .iter() @@ -634,7 +638,7 @@ macro_rules! graph_topology_tests { for incidence in graph.incidences(vertices[i]) { let pos = expected .iter() - .position(|(v, e)| *v == incidence.0 && *e == incidence.1) + .position(|x| *x == incidence) .expect(&format!( "unexpected incidence {incidence:?} of vertex {:?} from iterator", vertices[i] @@ -657,17 +661,21 @@ macro_rules! graph_topology_tests { // For loop edges (u == v), the edge must appear exactly twice in the incidences of u. let (graph, _, _, _) = make_test_graph(); for u in graph.vertices() { - for (v, e) in graph.incidences(u) { - if u == v { + for incidence in graph.incidences(u) { + if u == incidence.vertex { assert_eq!( - graph.incidences(u).filter(|(_, f)| *f == e).count(), + graph.incidences(u).filter(|x| x.edge == incidence.edge).count(), 2, - "loop edge {e:?} should appear exactly twice in incidences of vertex {u:?}" + "loop edge {:?} should appear exactly twice in incidences of vertex {u:?}", + incidence.edge ); } else { assert!( - graph.incidences(v).any(|(w, f)| w == u && f == e), - "edge {e:?} from incidences of vertex {u:?} missing in incidences of adjacent vertex {v:?}" + graph + .incidences(incidence.vertex) + .any(|x| x.vertex == u && x.edge == incidence.edge), + "edge {:?} from incidences of vertex {u:?} missing in incidences of adjacent vertex {:?}", + incidence.edge, incidence.vertex ); } } @@ -676,19 +684,19 @@ macro_rules! graph_topology_tests { #[test] fn incidences_loop_edge() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, Incidence}; let mut graph = <$T>::new(); let v = graph.add_vertex(); let e = graph.add_edge(v, v); let mut iter = graph.incidences(v); assert_eq!( iter.next(), - Some((v, e)), + Some(Incidence { vertex: v, edge: e }), "vertex should be adjacent to itself" ); assert_eq!( iter.next(), - Some((v, e)), + Some(Incidence { vertex: v, edge: e }), "vertex should be adjacent to itself twice" ); assert_eq!( @@ -716,16 +724,16 @@ macro_rules! graph_topology_tests { vertices[i] )); assert_eq!( - current.0, + current.vertex, vertices[1 - i], "unexpected adjacent vertex of vertex {:?} in incidence {j}", vertices[i] ); assert_eq!( - edges.iter().filter(|e| **e == current.1).count(), + edges.iter().filter(|e| **e == current.edge).count(), 1, "unexpected incident edge {:?} of vertex {:?}", - current.1, + current.edge, vertices[i], ); } @@ -761,7 +769,7 @@ macro_rules! graph_topology_tests { while let Some(incidence) = cursor.next(&graph) { let pos = expected .iter() - .position(|(v, e)| *v == incidence.0 && *e == incidence.1) + .position(|x| *x == incidence) .expect(&format!( "unexpected incidence {incidence:?} of vertex {:?} from cursor", vertices[i] @@ -779,19 +787,19 @@ macro_rules! graph_topology_tests { #[test] fn incidence_cursor_loop_edge() { - use $crate::traits::{GraphTopology, IncidenceCursor}; + use $crate::traits::{GraphTopology, Incidence, IncidenceCursor}; let mut graph = <$T>::new(); let v = graph.add_vertex(); let e = graph.add_edge(v, v); let mut cursor = graph.incidence_cursor(v); assert_eq!( cursor.next(&graph), - Some((v, e)), + Some(Incidence { vertex: v, edge: e }), "vertex should be adjacent to itself" ); assert_eq!( cursor.next(&graph), - Some((v, e)), + Some(Incidence { vertex: v, edge: e }), "vertex should be adjacent to itself twice" ); assert_eq!( @@ -819,16 +827,16 @@ macro_rules! graph_topology_tests { vertices[i] )); assert_eq!( - current.0, + current.vertex, vertices[1 - i], "unexpected adjacent vertex of vertex {:?} in incidence {j}", vertices[i] ); assert_eq!( - edges.iter().filter(|e| **e == current.1).count(), + edges.iter().filter(|e| **e == current.edge).count(), 1, "unexpected incident edge {:?} of vertex {:?}", - current.1, + current.edge, vertices[i], ); } @@ -868,11 +876,12 @@ macro_rules! graph_topology_tests { use $crate::traits::GraphTopology; let (graph, _, _, _) = make_test_graph(); for u in graph.vertices() { - for (v, e) in graph.incidences(u) { - let (w1, w2) = graph.incident_vertices(e); + for incidence in graph.incidences(u) { + let (w1, w2) = graph.incident_vertices(incidence.edge); assert!( - (w1 == u && w2 == v) || (w1 == v && w2 == u), - "incident vertices {w1:?} and {w2:?} of edge {e:?} are inconsistent with incidence ({v:?}, {e:?}) of vertex {u:?}" + (w1 == u && w2 == incidence.vertex) || (w1 == incidence.vertex && w2 == u), + "edge endpoints {w1:?} and {w2:?} are inconsistent with incidence ({:?}, {:?}) of vertex {u:?}", + incidence.vertex, incidence.edge ); } } @@ -883,7 +892,7 @@ macro_rules! graph_topology_tests { use $crate::traits::GraphTopology; let (graph, _, _, _) = make_test_graph(); for u in graph.vertices() { - let mut expected: Vec<_> = graph.incidences(u).map(|(_, e)| e).collect(); + let mut expected: Vec<_> = graph.incidences(u).map(|x| x.edge).collect(); for e in graph.incident_edges(u) { let pos = expected .iter() @@ -1256,7 +1265,7 @@ macro_rules! graph_topology_deletion_tests { for i in 0..10 { let mut expected: Vec<_> = incidences[i] .iter() - .filter_map(|(_, e)| (*e != edges[2].0).then_some(*e)) + .filter_map(|x| (x.edge != edges[2].0).then_some(x.edge)) .collect(); assert_eq!( graph.incident_edges(vertices[i]).count(), @@ -1289,7 +1298,7 @@ macro_rules! graph_topology_deletion_tests { for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { let mut expected: Vec<_> = incidences[i] .iter() - .filter_map(|(v, e)| (*v != vertices[2]).then_some(*e)) + .filter_map(|x| (x.vertex != vertices[2]).then_some(x.edge)) .collect(); assert_eq!( graph.incident_edges(vertices[i]).count(), @@ -1321,7 +1330,7 @@ macro_rules! graph_topology_deletion_tests { for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { let remaining = incidences[i] .iter() - .filter(|(v, _)| *v != vertices[2]) + .filter(|x| x.vertex != vertices[2]) .cloned() .collect(); assert_vertex_incidences(&graph, vertices[i], remaining); @@ -1337,7 +1346,7 @@ macro_rules! graph_topology_deletion_tests { for i in 0..10 { let remaining = incidences[i] .iter() - .filter(|(_, e)| *e != edges[2].0) + .filter(|x| x.edge != edges[2].0) .cloned() .collect(); assert_vertex_incidences(&graph, vertices[i], remaining); @@ -1347,10 +1356,10 @@ macro_rules! graph_topology_deletion_tests { fn assert_vertex_incidences( graph: &$T, v: <$T as $crate::traits::GraphTopology>::Vertex, - mut expected: Vec<( + mut expected: Vec<$crate::traits::Incidence< <$T as $crate::traits::GraphTopology>::Vertex, <$T as $crate::traits::GraphTopology>::Edge, - )>, + >>, ) { use $crate::traits::GraphTopology; assert_eq!( @@ -1362,7 +1371,7 @@ macro_rules! graph_topology_deletion_tests { for incidence in graph.incidences(v) { let pos = expected .iter() - .position(|(u, e)| *u == incidence.0 && *e == incidence.1) + .position(|x| *x == incidence) .expect(&format!( "unexpected incidence {incidence:?} of vertex {:?} from iterator after delete", v @@ -1385,7 +1394,7 @@ macro_rules! graph_topology_deletion_tests { for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { let remaining = incidences[i] .iter() - .filter(|(v, _)| *v != vertices[2]) + .filter(|x| x.vertex != vertices[2]) .cloned() .collect(); assert_vertex_incidence_cursor(&graph, vertices[i], remaining); @@ -1401,7 +1410,7 @@ macro_rules! graph_topology_deletion_tests { for i in 0..10 { let remaining = incidences[i] .iter() - .filter(|(_, e)| *e != edges[2].0) + .filter(|x| x.edge != edges[2].0) .cloned() .collect(); assert_vertex_incidence_cursor(&graph, vertices[i], remaining); @@ -1411,17 +1420,17 @@ macro_rules! graph_topology_deletion_tests { fn assert_vertex_incidence_cursor( graph: &$T, v: <$T as $crate::traits::GraphTopology>::Vertex, - mut expected: Vec<( + mut expected: Vec<$crate::traits::Incidence< <$T as $crate::traits::GraphTopology>::Vertex, <$T as $crate::traits::GraphTopology>::Edge, - )>, + >>, ) { use $crate::traits::IncidenceCursor; let mut cursor = graph.incidence_cursor(v); while let Some(incidence) = cursor.next(graph) { let pos = expected .iter() - .position(|(u, e)| *u == incidence.0 && *e == incidence.1) + .position(|x| *x == incidence) .expect(&format!( "unexpected incidence {incidence:?} of vertex {:?} from cursor after delete", v diff --git a/src/traits.rs b/src/traits.rs index 597240e..6e92aff 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -4,7 +4,6 @@ use crate::maps::EntityMap; // TODO: Add functions to reserve memory for vertices and edges. // TODO: Split out GraphTopologyAddition trait. -// TODO: Introduce an Incidence struct. /// A trait representing an undirected graph topology. /// /// An undirected graph is a set of vertices and undirected edges, where each edge connects either @@ -165,7 +164,10 @@ pub trait GraphTopology { /// # Panics /// /// Panics if `v` is not a valid vertex of this graph. - fn incidences(&self, v: Self::Vertex) -> impl Iterator; + fn incidences( + &self, + v: Self::Vertex, + ) -> impl Iterator>; /// Returns a cursor over all incidences of `v`, analogously to /// [`incidences`](Self::incidences), initially positioned before the first one. @@ -221,8 +223,8 @@ pub trait GraphTopologyDeletion: GraphTopology { /// [`GraphTopology::IncidenceCursor`] for guidance on when to prefer a cursor over /// [`GraphTopology::incidences`]. pub trait IncidenceCursor: Copy { - /// Advances the cursor and returns the next incidence as `Some((u, e))`, or `None` if the - /// traversal is exhausted. + /// Advances the cursor and returns the next incidence, or `None` if the traversal is + /// exhausted. /// /// # Examples /// @@ -248,5 +250,17 @@ pub trait IncidenceCursor: Copy { /// assert!(c2.next(&graph).is_some()); /// assert!(c2.next(&graph).is_none()); /// ``` - fn next(&mut self, graph: &G) -> Option<(G::Vertex, G::Edge)>; + fn next(&mut self, graph: &G) -> Option>; +} + +/// An incidence of some vertex *v* in a graph, i.e. a vertex-edge pair *(u, e)* such that *u* is +/// adjacent to *v* and *e* is an edge between them. +/// +/// Return type of [`GraphTopology::incidences`] and [`IncidenceCursor::next`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Incidence { + /// The vertex adjacent to *v*. + pub vertex: V, + /// The edge between *v* and [`vertex`](Self::vertex). + pub edge: E, } -- 2.43.0 From 10d0c382cc86e5f43b094770be7fb4470f7fcb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 6 Aug 2026 14:17:21 +0200 Subject: [PATCH 17/67] Add GraphTopologyAddition trait from GraphTopology Move capacity and add methods from GraphTopology trait to new GraphTopologyAddition trait --- src/generators.rs | 4 ++-- src/lib.rs | 4 +++- src/models/append_graph.rs | 20 ++++++++++--------- src/models/graph.rs | 22 ++++++++++++--------- src/testing/bfs_testing.rs | 8 ++++---- src/testing/dfs_testing.rs | 14 +++++++------- src/testing/dijkstra_testing.rs | 8 ++++---- src/testing/graph_topology_testing.rs | 12 ++++++------ src/testing/maps_testing.rs | 18 ++++++++--------- src/traits.rs | 28 ++++++++++++++++----------- 10 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/generators.rs b/src/generators.rs index 774e317..55f47a1 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1,4 +1,4 @@ -use crate::traits::GraphTopology; +use crate::traits::{GraphTopology, GraphTopologyAddition}; pub fn petersen( graph: &mut G, @@ -7,7 +7,7 @@ pub fn petersen( [::Edge; 15], ) where - G: GraphTopology, + G: GraphTopologyAddition, { const N: usize = 5; const K: usize = 2; diff --git a/src/lib.rs b/src/lib.rs index e902d00..024fec4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,7 +136,9 @@ pub mod traits; /// Convenience re-exports of graph topology traits for common use. pub mod prelude { - pub use crate::traits::{GraphTopology, GraphTopologyDeletion, Incidence, IncidenceCursor}; + pub use crate::traits::{ + GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, IncidenceCursor, + }; } mod testing; diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 00c2d36..d3f37fe 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -1,7 +1,7 @@ //! [`AppendGraph`], an undirected graph topology supporting addition only. use crate::maps::EntityMap; -use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; +use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in an [`AppendGraph`]. /// @@ -147,10 +147,6 @@ impl GraphTopology for AppendGraph { self.vertices.len() } - fn vertex_capacity(&self) -> usize { - self.vertices.capacity() - } - fn vertex_map(&self, default: T) -> EntityMap { EntityMap::new(default, |v| v.0, self.vertex_capacity()) } @@ -159,10 +155,6 @@ impl GraphTopology for AppendGraph { self.incidences.len() / 2 } - fn edge_capacity(&self) -> usize { - self.incidences.capacity() / 2 - } - fn edge_map(&self, default: T) -> EntityMap { EntityMap::new(default, |e| e.0 / 2, self.edge_capacity()) } @@ -209,6 +201,16 @@ impl GraphTopology for AppendGraph { incidence: self.vertices[v.0].first_incidence, } } +} + +impl GraphTopologyAddition for AppendGraph { + fn vertex_capacity(&self) -> usize { + self.vertices.capacity() + } + + fn edge_capacity(&self) -> usize { + self.incidences.capacity() / 2 + } fn add_vertex(&mut self) -> Self::Vertex { self.vertices.push(VertexIncidenceHeader { diff --git a/src/models/graph.rs b/src/models/graph.rs index 39454eb..d0b37c4 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -3,7 +3,9 @@ use typed_generational_arena::{Arena, Index}; use crate::maps::EntityMap; -use crate::traits::{GraphTopology, GraphTopologyDeletion, Incidence, IncidenceCursor}; +use crate::traits::{ + GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, IncidenceCursor, +}; /// An opaque handle identifying a vertex in a [`Graph`]. /// @@ -239,10 +241,6 @@ impl GraphTopology for Graph { self.vertices.len() } - fn vertex_capacity(&self) -> usize { - self.vertices.capacity() - } - fn vertex_map(&self, default: T) -> EntityMap { EntityMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) } @@ -251,10 +249,6 @@ impl GraphTopology for Graph { self.incidences.len() / 2 } - fn edge_capacity(&self) -> usize { - self.incidences.capacity() / 2 - } - fn edge_map(&self, default: T) -> EntityMap { EntityMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) } @@ -312,6 +306,16 @@ impl GraphTopology for Graph { incidence: self.vertices[v].first_incidence, } } +} + +impl GraphTopologyAddition for Graph { + fn vertex_capacity(&self) -> usize { + self.vertices.capacity() + } + + fn edge_capacity(&self) -> usize { + self.incidences.capacity() / 2 + } fn add_vertex(&mut self) -> Self::Vertex { self.vertices.insert(VertexIncidenceHeader { diff --git a/src/testing/bfs_testing.rs b/src/testing/bfs_testing.rs index 233a9e7..b5e4365 100644 --- a/src/testing/bfs_testing.rs +++ b/src/testing/bfs_testing.rs @@ -4,7 +4,7 @@ macro_rules! bfs_tests { ($T:ty) => { #[test] fn bfs_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let result = $crate::algorithms::bfs(&graph, v); @@ -54,7 +54,7 @@ macro_rules! bfs_tests { #[test] fn bfs_distances_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let distances = $crate::algorithms::bfs_distances(&graph, v); @@ -91,7 +91,7 @@ macro_rules! bfs_tests { #[test] fn bfs_find_source() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert_eq!( @@ -139,7 +139,7 @@ macro_rules! bfs_tests { #[test] fn bfs_find_where_source_matches() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert_eq!( diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs index d302773..b89ff30 100644 --- a/src/testing/dfs_testing.rs +++ b/src/testing/dfs_testing.rs @@ -4,7 +4,7 @@ macro_rules! dfs_tests { ($T:ty) => { #[test] fn dfs_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let result = $crate::algorithms::dfs(&graph, v); @@ -49,7 +49,7 @@ macro_rules! dfs_tests { #[test] fn dfs_visited_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let visited = $crate::algorithms::dfs_visited(&graph, v); @@ -78,7 +78,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_source() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert!( @@ -111,7 +111,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_where_source_matches() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert_eq!( @@ -205,7 +205,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_source_equals_target() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert_eq!( @@ -227,7 +227,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_adjacent() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -255,7 +255,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_where_source_matches() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert_eq!( diff --git a/src/testing/dijkstra_testing.rs b/src/testing/dijkstra_testing.rs index 3b06326..34cbe79 100644 --- a/src/testing/dijkstra_testing.rs +++ b/src/testing/dijkstra_testing.rs @@ -4,7 +4,7 @@ macro_rules! dijkstra_tests { ($T:ty) => { #[test] fn dijkstra_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let result = $crate::algorithms::dijkstra(&graph, v, |_| { @@ -25,7 +25,7 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_zero_weight_loop() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 3] = core::array::from_fn(|_| graph.add_vertex()); @@ -71,7 +71,7 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_unweighted_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let result = $crate::algorithms::dijkstra_unweighted(&graph, v); @@ -95,7 +95,7 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_distances_unweighted_single_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, v); diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 5ee3be3..f7ea567 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -15,7 +15,7 @@ macro_rules! graph_topology_test_fixtures { <$T as $crate::traits::GraphTopology>::Edge, >>; 10], ) { - use $crate::traits::{GraphTopology, Incidence}; + use $crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; let mut graph = <$T>::new(); let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 10] = core::array::from_fn(|_| graph.add_vertex()); @@ -96,7 +96,7 @@ macro_rules! graph_topology_test_fixtures { #[allow(dead_code)] fn make_test_graph_disconnected() -> ($T, [<$T as $crate::traits::GraphTopology>::Vertex; 3]) { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 3] = core::array::from_fn(|_| graph.add_vertex()); @@ -112,7 +112,7 @@ macro_rules! graph_topology_tests { ($T:ty) => { #[test] fn add_vertex() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v = graph.add_vertex(); assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); @@ -147,7 +147,7 @@ macro_rules! graph_topology_tests { #[test] fn add_edge() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; let mut graph = <$T>::new(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -974,7 +974,7 @@ macro_rules! graph_topology_deletion_tests { #[test] fn delete_vertex_invalid_index() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; use $crate::traits::GraphTopologyDeletion; let mut graph = Graph::new(); let v = graph.add_vertex(); @@ -1169,7 +1169,7 @@ macro_rules! graph_topology_deletion_tests { #[test] fn delete_edge_invalid_index() { - use $crate::traits::GraphTopology; + use $crate::traits::GraphTopologyAddition; use $crate::traits::GraphTopologyDeletion; let mut graph = Graph::new(); let v1 = graph.add_vertex(); diff --git a/src/testing/maps_testing.rs b/src/testing/maps_testing.rs index b9c7e1f..daa3b67 100644 --- a/src/testing/maps_testing.rs +++ b/src/testing/maps_testing.rs @@ -4,7 +4,7 @@ macro_rules! entity_map_tests { ($T:ty) => { #[test] fn initial_values_are_default() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -15,7 +15,7 @@ macro_rules! entity_map_tests { #[test] fn write_and_read() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -27,7 +27,7 @@ macro_rules! entity_map_tests { #[test] fn lazy_growth_on_read() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); graph.add_vertex(); let map = graph.vertex_map(99); @@ -37,7 +37,7 @@ macro_rules! entity_map_tests { #[test] fn lazy_growth_on_write() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); let v1 = graph.add_vertex(); let mut map = graph.vertex_map(0); @@ -49,7 +49,7 @@ macro_rules! entity_map_tests { #[test] fn expand_to_new_vertices() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); graph.add_vertex(); let mut map = graph.vertex_map(42); @@ -67,7 +67,7 @@ macro_rules! entity_map_tests { #[test] fn expand_does_not_overwrite_existing_values() { - use $crate::traits::GraphTopology; + use $crate::traits::{GraphTopology, GraphTopologyAddition}; let mut graph = <$T>::new(); let v = graph.add_vertex(); let mut map = graph.vertex_map(0); @@ -85,8 +85,7 @@ macro_rules! entity_map_deletion_tests { ($T:ty) => { #[test] fn surviving_vertex_readable_after_delete() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; + use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; let mut graph = <$T>::new(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -99,8 +98,7 @@ macro_rules! entity_map_deletion_tests { #[test] fn reused_slot_returns_old_value() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; + use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; let mut graph = <$T>::new(); graph.add_vertex(); let v1 = graph.add_vertex(); diff --git a/src/traits.rs b/src/traits.rs index 6e92aff..a625d06 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,13 +2,11 @@ use crate::maps::EntityMap; -// TODO: Add functions to reserve memory for vertices and edges. -// TODO: Split out GraphTopologyAddition trait. /// A trait representing an undirected graph topology. /// /// An undirected graph is a set of vertices and undirected edges, where each edge connects either /// exactly two vertices or one vertex with itself (loop edge). This trait provides methods for -/// querying a graph topology, iterating over vertices and edges, and adding new vertices and edges. +/// querying a graph topology, and iterating over vertices and edges. /// /// # Vertices and edges /// @@ -20,9 +18,10 @@ use crate::maps::EntityMap; /// Methods accepting vertices or edges as parameters panic if the handle was invalidated by a /// deletion, and return incorrect results if the handle was not produced by this graph instance. /// -/// # Deletion +/// # Addition and deletion /// -/// This trait covers graph construction and querying only. To delete vertices and edges, see +/// This trait covers graph querying only. To construct a graph incrementally by adding vertices and +/// edges, see [`GraphTopologyAddition`]. To delete vertices and edges, see /// [`GraphTopologyDeletion`]. /// /// [`Edge`]: GraphTopology::Edge @@ -45,9 +44,6 @@ pub trait GraphTopology { /// Returns the number of vertices in the graph. fn vertex_count(&self) -> usize; - /// Returns the total number of vertices the graph can hold without reallocating. - fn vertex_capacity(&self) -> usize; - /// Creates and returns an [`EntityMap`] for the vertices with every slot initialised to /// `default`. /// @@ -74,9 +70,6 @@ pub trait GraphTopology { /// Returns the number of edges in the graph. fn edge_count(&self) -> usize; - /// Returns the total number of edges the graph can hold without reallocating. - fn edge_capacity(&self) -> usize; - /// Creates and returns an [`EntityMap`] for the edges with every slot initialised to `default`. /// /// # Examples @@ -179,6 +172,19 @@ pub trait GraphTopology { /// /// Panics if `v` is not a valid vertex of this graph. fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor; +} + +// TODO: Add functions to reserve memory for vertices and edges. +/// A trait that adds construction operations to an undirected graph topology. +/// +/// This trait provides methods for addition of vertices and edges, and capacity management in an +/// undirected graph. +pub trait GraphTopologyAddition: GraphTopology { + /// Returns the total number of vertices the graph can hold without reallocating. + fn vertex_capacity(&self) -> usize; + + /// Returns the total number of edges the graph can hold without reallocating. + fn edge_capacity(&self) -> usize; /// Adds a new isolated vertex and returns its handle. fn add_vertex(&mut self) -> Self::Vertex; -- 2.43.0 From 7d8c9aaa1024fb616707410f416ba7c8b1430ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 6 Aug 2026 20:54:42 +0200 Subject: [PATCH 18/67] Add link in lib module doc --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 024fec4..496bb4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ //! * [`EntityMap`] to freely associate custom data with vertices and edges, //! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different //! variants, -//! * Exposed traits to implement custom graph data structures or algorithms. +//! * Exposed [`traits`] to implement custom graph data structures or algorithms. //! //! # Usage example //! @@ -125,6 +125,7 @@ //! [`GraphTopology::Edge`]: crate::traits::GraphTopology::Edge //! [`GraphTopology::Vertex`]: crate::traits::GraphTopology::Vertex //! [`GraphTopologyDeletion`]: crate::traits::GraphTopologyDeletion +//! [`traits`]: crate::traits #![warn(missing_docs)] -- 2.43.0 From a7346bd7828c1b81d66013e08b4d8adf90a4542e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 6 Aug 2026 23:01:10 +0200 Subject: [PATCH 19/67] Rename EntityMap to ElementMap (tests not yet updated) --- src/algorithms.rs | 28 ++++++++++++++-------------- src/lib.rs | 4 ++-- src/maps.rs | 18 +++++++++--------- src/models/append_graph.rs | 18 +++++++++--------- src/models/graph.rs | 18 +++++++++--------- src/testing/bfs_testing.rs | 4 ++-- src/testing/dfs_testing.rs | 6 +++--- src/testing/dijkstra_testing.rs | 10 +++++----- src/traits.rs | 10 +++++----- 9 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index 2da4d09..7719e0f 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -33,15 +33,15 @@ use std::cmp::Reverse; use std::collections::VecDeque; use std::hash::Hash; -use crate::maps::EntityMap; +use crate::maps::ElementMap; use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; /// Return data type for [`dijkstra`] and [`dijkstra_unweighted`]. pub struct DijkstraResult { /// Vertex map of minimum distances from a given `source` vertex. - pub distances: EntityMap>, + pub distances: ElementMap>, /// Vertex map of predecessors on some shortest path from a given `source` vertex. - pub predecessors: EntityMap>, + pub predecessors: ElementMap>, } // TODO: Generalize the return type of the weight function. @@ -128,7 +128,7 @@ pub fn dijkstra_distances( graph: &G, source: G::Vertex, weights: W, -) -> EntityMap> +) -> ElementMap> where G: GraphTopology, G::Vertex: Hash, @@ -209,7 +209,7 @@ where pub fn dijkstra_distances_unweighted( graph: &G, source: G::Vertex, -) -> EntityMap> +) -> ElementMap> where G: GraphTopology, G::Vertex: Hash, @@ -222,7 +222,7 @@ fn dijkstra_impl( source: G::Vertex, weights: W, mut on_relax: F, -) -> EntityMap> +) -> ElementMap> where G: GraphTopology, G::Vertex: Hash, @@ -255,9 +255,9 @@ where /// Return data type for [`bfs`]. pub struct BfsResult { /// Vertex map of minimum distances from a given `source` vertex. - pub distances: EntityMap>, + pub distances: ElementMap>, /// Vertex map of predecessors on some shortest path from a given `source` vertex. - pub predecessors: EntityMap>, + pub predecessors: ElementMap>, } /// [Breadth-first search] traversal from `source`, returns distances and predecessors. @@ -333,7 +333,7 @@ where /// ``` /// /// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search -pub fn bfs_distances(graph: &G, source: G::Vertex) -> EntityMap> +pub fn bfs_distances(graph: &G, source: G::Vertex) -> ElementMap> where G: GraphTopology, { @@ -415,7 +415,7 @@ where } struct BfsImplResult { - distances: EntityMap>, + distances: ElementMap>, found: Option<(V, u32)>, } @@ -455,9 +455,9 @@ where /// Return data type for [`dfs`]. pub struct DfsResult { /// Vertex map indicating which vertices were visited during the search. - pub visited: EntityMap, + pub visited: ElementMap, /// Vertex map of predecessors on the DFS tree path from a given `source` vertex. - pub predecessors: EntityMap>, + pub predecessors: ElementMap>, } /// [Depth-first search] traversal from `source`, returns visited vertices and predecessors. @@ -533,7 +533,7 @@ where /// ``` /// /// [Depth-first search]: https://en.wikipedia.org/wiki/Depth-first_search -pub fn dfs_visited(graph: &G, source: G::Vertex) -> EntityMap +pub fn dfs_visited(graph: &G, source: G::Vertex) -> ElementMap where G: GraphTopology, { @@ -613,7 +613,7 @@ where } struct DfsImplResult { - visited: EntityMap, + visited: ElementMap, found: Option, } diff --git a/src/lib.rs b/src/lib.rs index 496bb4a..946e3f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ //! //! * Undirected graph types [`Graph`] and [`AppendGraph`] built on a flat, index-based adjacency //! list, -//! * [`EntityMap`] to freely associate custom data with vertices and edges, +//! * [`ElementMap`] to freely associate custom data with vertices and edges, //! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different //! variants, //! * Exposed [`traits`] to implement custom graph data structures or algorithms. @@ -118,7 +118,7 @@ //! [BFS]: crate::algorithms::bfs //! [Dijkstra's algorithm]: crate::algorithms::dijkstra //! [DFS]: crate::algorithms::dfs -//! [`EntityMap`]: crate::maps::EntityMap +//! [`ElementMap`]: crate::maps::ElementMap //! [`AppendGraph`]: crate::models::AppendGraph //! [`Graph`]: crate::models::Graph //! [`GraphTopology`]: crate::traits::GraphTopology diff --git a/src/maps.rs b/src/maps.rs index 1aaa5a5..f6ddf0f 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -1,30 +1,30 @@ //! Provides maps to associate custom data to graph vertices and edges. //! -//! [`EntityMap`] provides a copy-on-write, [`Vec`]-backed map to associate data to either all +//! [`ElementMap`] provides a copy-on-write, [`Vec`]-backed map to associate data to either all //! vertices or all edges in a graph. use std::ops::{Index, IndexMut}; /// A map to associate custom data to graph vertices or edges. /// -/// This map uses raw entity indices to associate homogenous custom data of type `T` to graph +/// This map uses raw element indices to associate homogenous custom data of type `T` to graph /// vertices or edges. The implementation uses a [`Vec`], allocating contiguous slots for the data, -/// which means that the provided index conversion function should map entities to contiguous +/// which means that the provided index conversion function should map elements to contiguous /// indices, or indices with relatively few gaps. /// /// Data allocation happens as copy-on-write, i.e. the backing [`Vec`] is only resized if a value is /// written beyond current capacity, but the `default` value can transparently be read. No bound or -/// validity checks are performed by the map on the provided entity handles. +/// validity checks are performed by the map on the provided element handles. /// -/// Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] to obtain an `EntityMap` for +/// Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] to obtain an `ElementMap` for /// vertices or edges, respectively. -pub struct EntityMap { +pub struct ElementMap { data: Vec, default: T, to_index: fn(E) -> usize, } -impl EntityMap { +impl ElementMap { /// Creates a new map with the given `default` value, an index conversion function `to_index`, /// and an initial `capacity`. pub fn new(default: T, to_index: fn(E) -> usize, capacity: usize) -> Self { @@ -53,7 +53,7 @@ impl EntityMap { } } -impl Index for EntityMap { +impl Index for ElementMap { type Output = T; fn index(&self, e: E) -> &T { @@ -66,7 +66,7 @@ impl Index for EntityMap { } } -impl IndexMut for EntityMap { +impl IndexMut for ElementMap { fn index_mut(&mut self, e: E) -> &mut T { let i = (self.to_index)(e); if i >= self.data.len() { diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index d3f37fe..ac637ea 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -1,6 +1,6 @@ //! [`AppendGraph`], an undirected graph topology supporting addition only. -use crate::maps::EntityMap; +use crate::maps::ElementMap; use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in an [`AppendGraph`]. @@ -17,11 +17,11 @@ pub struct Vertex(usize); #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Edge(usize); -/// An [`EntityMap`] for [`AppendGraph`] vertices. -pub type AppendGraphVertexMap = EntityMap; +/// An [`ElementMap`] for [`AppendGraph`] vertices. +pub type AppendGraphVertexMap = ElementMap; -/// An [`EntityMap`] for [`AppendGraph`] edges. -pub type AppendGraphEdgeMap = EntityMap; +/// An [`ElementMap`] for [`AppendGraph`] edges. +pub type AppendGraphEdgeMap = ElementMap; /// An [`Incidence`] for [`AppendGraph`]. pub type AppendGraphIncidence = Incidence; @@ -147,16 +147,16 @@ impl GraphTopology for AppendGraph { self.vertices.len() } - fn vertex_map(&self, default: T) -> EntityMap { - EntityMap::new(default, |v| v.0, self.vertex_capacity()) + fn vertex_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |v| v.0, self.vertex_capacity()) } fn edge_count(&self) -> usize { self.incidences.len() / 2 } - fn edge_map(&self, default: T) -> EntityMap { - EntityMap::new(default, |e| e.0 / 2, self.edge_capacity()) + fn edge_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |e| e.0 / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { diff --git a/src/models/graph.rs b/src/models/graph.rs index d0b37c4..c47ec36 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -2,7 +2,7 @@ use typed_generational_arena::{Arena, Index}; -use crate::maps::EntityMap; +use crate::maps::ElementMap; use crate::traits::{ GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, IncidenceCursor, }; @@ -19,11 +19,11 @@ pub type Vertex = Index; /// deleted. Obtain via graph methods like [`Graph::add_edge`] and [`Graph::edges`]. pub type Edge = Index; -/// An [`EntityMap`] for [`Graph`] vertices. -pub type GraphVertexMap = EntityMap; +/// An [`ElementMap`] for [`Graph`] vertices. +pub type GraphVertexMap = ElementMap; -/// An [`EntityMap`] for [`Graph`] edges. -pub type GraphEdgeMap = EntityMap; +/// An [`ElementMap`] for [`Graph`] edges. +pub type GraphEdgeMap = ElementMap; /// An [`Incidence`] for [`Graph`]. pub type GraphIncidence = Incidence; @@ -241,16 +241,16 @@ impl GraphTopology for Graph { self.vertices.len() } - fn vertex_map(&self, default: T) -> EntityMap { - EntityMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) + fn vertex_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) } fn edge_count(&self) -> usize { self.incidences.len() / 2 } - fn edge_map(&self, default: T) -> EntityMap { - EntityMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) + fn edge_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { diff --git a/src/testing/bfs_testing.rs b/src/testing/bfs_testing.rs index b5e4365..63f8fae 100644 --- a/src/testing/bfs_testing.rs +++ b/src/testing/bfs_testing.rs @@ -194,7 +194,7 @@ macro_rules! bfs_tests { } fn assert_bfs_distances( - distances: &$crate::maps::EntityMap< + distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -222,7 +222,7 @@ macro_rules! bfs_tests { } fn assert_bfs_predecessors( - predecessors: &$crate::maps::EntityMap< + predecessors: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option<<$T as $crate::traits::GraphTopology>::Vertex>, >, diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs index b89ff30..61f4944 100644 --- a/src/testing/dfs_testing.rs +++ b/src/testing/dfs_testing.rs @@ -163,7 +163,7 @@ macro_rules! dfs_tests { } fn assert_dfs_visited( - visited: &$crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, + visited: &$crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], ) { for i in 0..10 { @@ -177,8 +177,8 @@ macro_rules! dfs_tests { fn assert_dfs_predecessors( graph: &$T, - visited: &$crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, - predecessors: &$crate::maps::EntityMap< + visited: &$crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, + predecessors: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option<<$T as $crate::traits::GraphTopology>::Vertex>, >, diff --git a/src/testing/dijkstra_testing.rs b/src/testing/dijkstra_testing.rs index 34cbe79..d015ca6 100644 --- a/src/testing/dijkstra_testing.rs +++ b/src/testing/dijkstra_testing.rs @@ -131,7 +131,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_single_vertex( - distances: &$crate::maps::EntityMap< + distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -160,7 +160,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_disconnected( - distances: &$crate::maps::EntityMap< + distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -204,7 +204,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_test_graph( - distances: &$crate::maps::EntityMap< + distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -261,7 +261,7 @@ macro_rules! dijkstra_tests { } fn assert_distances_unweighted_test_graph( - distances: &$crate::maps::EntityMap< + distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, Option, >, @@ -302,7 +302,7 @@ macro_rules! dijkstra_tests { <$T as $crate::traits::GraphTopology>::Edge, >, >; 10], - $crate::maps::EntityMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, + $crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, ) { use $crate::traits::GraphTopology; let (graph, vertices, edges, incidences) = make_test_graph(); diff --git a/src/traits.rs b/src/traits.rs index a625d06..f459cfa 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,6 +1,6 @@ //! Core traits for undirected graph topologies. -use crate::maps::EntityMap; +use crate::maps::ElementMap; /// A trait representing an undirected graph topology. /// @@ -44,7 +44,7 @@ pub trait GraphTopology { /// Returns the number of vertices in the graph. fn vertex_count(&self) -> usize; - /// Creates and returns an [`EntityMap`] for the vertices with every slot initialised to + /// Creates and returns an [`ElementMap`] for the vertices with every slot initialised to /// `default`. /// /// # Examples @@ -65,12 +65,12 @@ pub trait GraphTopology { /// labels[v2] = "b"; /// assert_eq!(labels[v2], "b"); /// ``` - fn vertex_map(&self, default: T) -> EntityMap; + fn vertex_map(&self, default: T) -> ElementMap; /// Returns the number of edges in the graph. fn edge_count(&self) -> usize; - /// Creates and returns an [`EntityMap`] for the edges with every slot initialised to `default`. + /// Creates and returns an [`ElementMap`] for the edges with every slot initialised to `default`. /// /// # Examples /// @@ -92,7 +92,7 @@ pub trait GraphTopology { /// weights[e2] = 2; /// assert_eq!(weights[e2], 2); /// ``` - fn edge_map(&self, default: T) -> EntityMap; + fn edge_map(&self, default: T) -> ElementMap; /// Returns the degree of `v`, i.e. the number of incident edges, where each loop contributes 2. /// -- 2.43.0 From 59d9c2b8a785c488072f217e264508dd30c83e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 6 Aug 2026 23:06:52 +0200 Subject: [PATCH 20/67] Add ElementMap::reserve to replace ElementMap::expand, update tests ElementMap tests are now self contained unit tests, without any graph context. The only exception is the repurposed test "reused_slot_returns_old_value", which is not suited for GraphTopologyDeletion, but actually specific for Graph. --- src/maps.rs | 70 +++++++++++++++++++--- src/models/graph.rs | 23 ++++++++ src/testing.rs | 1 - src/testing/maps_testing.rs | 115 ------------------------------------ tests/maps.rs | 12 ---- 5 files changed, 85 insertions(+), 136 deletions(-) delete mode 100644 src/testing/maps_testing.rs delete mode 100644 tests/maps.rs diff --git a/src/maps.rs b/src/maps.rs index f6ddf0f..ac7dbdc 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -41,15 +41,13 @@ impl ElementMap { self.data.capacity() } - /// Expands the internal data storage capacity of the map to `capacity`, Does nothing if - /// capacity is already sufficient. + /// Reserves capacity for at least `additional` more data entries. Does nothing if capacity is + /// already sufficient. /// - /// Use this before writing data for new graph entities to avoid incremental growth on the first - /// write to each new entity. - pub fn expand(&mut self, capacity: usize) { - if capacity > self.data.len() { - self.data.resize(capacity, self.default.clone()); - } + /// Use this before writing data for new graph elements to avoid incremental growth on the first + /// write to each new element. + pub fn reserve(&mut self, additional: usize) { + self.data.reserve(additional); } } @@ -75,3 +73,59 @@ impl IndexMut for ElementMap { &mut self.data[i] } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initial_values_are_default() { + let map = ElementMap::new(42, |e| e, 0); + assert_eq!(map[0], 42, "initial read at 0 should be default"); + assert_eq!(map[5], 42, "initial read at 5 should be default"); + } + + #[test] + fn reserve_increases_capacity_without_initializing() { + let mut map = ElementMap::new(0, |e| e, 0); + let capacity_before = map.capacity(); + map.reserve(capacity_before + 10); + assert!( + map.capacity() > capacity_before, + "expected sufficient capacity increase after reserve" + ); + assert_eq!(map[5], 0, "expected read beyond length to return default"); + } + + #[test] + fn reserve_prevents_reallocation_on_write() { + let mut map = ElementMap::new(0, |e| e, 0); + map.reserve(10); + let capacity_before = map.capacity(); + for i in 0..10 { + map[i] = i as i32; + } + assert_eq!( + map.capacity(), + capacity_before, + "writes within reserved capacity should not reallocate" + ); + } + + #[test] + fn reserve_does_not_overwrite_existing_values() { + let mut map = ElementMap::new(0, |e| e, 0); + map[0] = 5; + let capacity_before = map.capacity(); + map.reserve(capacity_before + 10); + assert_eq!(map[0], 5, "reserve should not change existing data"); + } + + #[test] + fn write_and_read() { + let mut map = ElementMap::new(0, |e| e, 0); + map[1] = 7; + assert_eq!(map[1], 7, "unexpected read value after write"); + assert_eq!(map[0], 0, "initial read should be default"); + } +} diff --git a/src/models/graph.rs b/src/models/graph.rs index c47ec36..e584cf8 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -420,4 +420,27 @@ mod tests { graph.delete_edge(f); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); } + + #[test] + fn reused_slot_returns_old_value() { + let mut graph = Graph::new(); + graph.add_vertex(); + let v1 = graph.add_vertex(); + let mut map = graph.vertex_map(0); + map[v1] = 99; + graph.delete_vertex(v1); + let v2 = graph.add_vertex(); + assert_eq!( + v1.arr_idx(), + v2.arr_idx(), + "precondition: new vertex {v1:?} should reuse slot of deleted vertex {v2:?}" + ); + // ElementMap uses raw indices, not vertex identity. A new vertex v2 reusing the slot + // of previously deleted v1 sees the old value. Callers must reinitialize stale slots + // after deletion. + assert_eq!( + map[v2], 99, + "new vertex reusing slot of deleted vertex should return old value" + ); + } } diff --git a/src/testing.rs b/src/testing.rs index 124eff5..d815794 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -4,4 +4,3 @@ pub(crate) mod bfs_testing; pub(crate) mod dfs_testing; pub(crate) mod dijkstra_testing; pub(crate) mod graph_topology_testing; -pub(crate) mod maps_testing; diff --git a/src/testing/maps_testing.rs b/src/testing/maps_testing.rs deleted file mode 100644 index daa3b67..0000000 --- a/src/testing/maps_testing.rs +++ /dev/null @@ -1,115 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! entity_map_tests { - ($T:ty) => { - #[test] - fn initial_values_are_default() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let map = graph.vertex_map(42); - assert_eq!(map[v1], 42); - assert_eq!(map[v2], 42); - } - - #[test] - fn write_and_read() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let mut map = graph.vertex_map(0); - map[v1] = 7; - assert_eq!(map[v1], 7); - assert_eq!(map[v2], 0); - } - - #[test] - fn lazy_growth_on_read() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - graph.add_vertex(); - let map = graph.vertex_map(99); - let v = graph.add_vertex(); - assert_eq!(map[v], 99); - } - - #[test] - fn lazy_growth_on_write() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let mut map = graph.vertex_map(0); - let v2 = graph.add_vertex(); - map[v2] = 7; - assert_eq!(map[v1], 0); - assert_eq!(map[v2], 7); - } - - #[test] - fn expand_to_new_vertices() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - graph.add_vertex(); - let mut map = graph.vertex_map(42); - let capacity_before = map.capacity(); - while graph.vertex_capacity() <= capacity_before { - graph.add_vertex(); - } - assert!( - map.capacity() < graph.vertex_capacity(), - "precondition: map is stale before expand" - ); - map.expand(graph.vertex_capacity()); - assert_eq!(map.capacity(), graph.vertex_capacity()); - } - - #[test] - fn expand_does_not_overwrite_existing_values() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - let mut map = graph.vertex_map(0); - map[v] = 5; - graph.add_vertex(); - map.expand(graph.vertex_capacity()); - assert_eq!(map[v], 5); - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! entity_map_deletion_tests { - ($T:ty) => { - #[test] - fn surviving_vertex_readable_after_delete() { - use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let mut map = graph.vertex_map(0); - map[v1] = 1; - map[v2] = 2; - graph.delete_vertex(v2); - assert_eq!(map[v1], 1); - } - - #[test] - fn reused_slot_returns_old_value() { - use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; - let mut graph = <$T>::new(); - graph.add_vertex(); - let v1 = graph.add_vertex(); - let mut map = graph.vertex_map(0); - map[v1] = 99; - graph.delete_vertex(v1); - let v2 = graph.add_vertex(); - // EntityMap uses raw indices, not vertex identity. A new vertex v2 reusing the slot - // of previously deleted v1 sees the old value. Callers must reinitialize stale slots - // after deletion. - assert_eq!(map[v2], 99); - } - }; -} diff --git a/tests/maps.rs b/tests/maps.rs deleted file mode 100644 index 73fe87d..0000000 --- a/tests/maps.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod append_graph_entity_map_tests { - use grapherity::models::AppendGraph; - - grapherity::entity_map_tests!(AppendGraph); -} - -mod graph_entity_map_tests { - use grapherity::models::Graph; - - grapherity::entity_map_tests!(Graph); - grapherity::entity_map_deletion_tests!(Graph); -} -- 2.43.0 From b4374174a9845ad4abd73d03a27630df56c6388c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 7 Aug 2026 19:01:03 +0200 Subject: [PATCH 21/67] Add reserve_vertices and reserve_edges to GraphTopologyAddition --- src/models/append_graph.rs | 8 +++ src/models/graph.rs | 8 +++ src/testing/graph_topology_testing.rs | 85 ++++++++++++++++++++++++++- src/traits.rs | 15 ++++- 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index ac637ea..9526f95 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -212,6 +212,14 @@ impl GraphTopologyAddition for AppendGraph { self.incidences.capacity() / 2 } + fn reserve_vertices(&mut self, additional: usize) { + self.vertices.reserve(additional); + } + + fn reserve_edges(&mut self, additional: usize) { + self.incidences.reserve(additional * 2); + } + fn add_vertex(&mut self) -> Self::Vertex { self.vertices.push(VertexIncidenceHeader { incidence_count: 0, diff --git a/src/models/graph.rs b/src/models/graph.rs index e584cf8..702d244 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -317,6 +317,14 @@ impl GraphTopologyAddition for Graph { self.incidences.capacity() / 2 } + fn reserve_vertices(&mut self, additional: usize) { + self.vertices.reserve(additional); + } + + fn reserve_edges(&mut self, additional: usize) { + self.incidences.reserve(additional * 2); + } + fn add_vertex(&mut self) -> Self::Vertex { self.vertices.insert(VertexIncidenceHeader { incidence_count: 0, diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index f7ea567..47d6569 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -852,7 +852,6 @@ macro_rules! graph_topology_tests { #[test] fn incidence_cursor_copy() { use $crate::traits::{GraphTopology, IncidenceCursor}; - // Constructs a graph with two vertices connected to `v`. let mut graph = <$T>::new(); let v = graph.add_vertex(); for _ in 0..2 { @@ -909,6 +908,90 @@ macro_rules! graph_topology_tests { ); } } + + #[test] + fn reserve_vertices_increases_capacity() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let capacity_before = graph.vertex_capacity(); + graph.reserve_vertices(capacity_before + 10); + assert!( + graph.vertex_capacity() > capacity_before, + "expected sufficient capacity increase after reserve" + ); + } + + #[test] + fn reserve_vertices_prevents_reallocation_on_add() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + graph.reserve_vertices(10); + let capacity_before = graph.vertex_capacity(); + for _ in 0..10 { + graph.add_vertex(); + } + assert_eq!( + graph.vertex_capacity(), + capacity_before, + "writes within reserved capacity should not reallocate" + ); + } + + #[test] + fn reserve_vertices_does_not_affect_edge_capacity() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let capacity_before = graph.edge_capacity(); + graph.reserve_vertices(10); + assert_eq!( + graph.edge_capacity(), + capacity_before, + "reserve_vertices must not change edge capacity" + ); + } + + #[test] + fn reserve_edges_increases_capacity() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let capacity_before = graph.edge_capacity(); + graph.reserve_edges(capacity_before + 10); + assert!( + graph.edge_capacity() > capacity_before, + "expected sufficient capacity increase after reserve" + ); + } + + #[test] + fn reserve_edges_prevents_reallocation_on_add() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + graph.reserve_edges(10); + let capacity_before = graph.edge_capacity(); + for _ in 0..10 { + graph.add_edge(v1, v2); + } + assert_eq!( + graph.edge_capacity(), + capacity_before, + "writes within reserved capacity should not reallocate" + ); + } + + #[test] + fn reserve_edges_does_not_affect_vertex_capacity() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let capacity_before = graph.vertex_capacity(); + graph.reserve_edges(10); + assert_eq!( + graph.vertex_capacity(), + capacity_before, + "reserve_edges must not change vertex capacity" + ); + } }; } diff --git a/src/traits.rs b/src/traits.rs index f459cfa..ca525f8 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -174,7 +174,6 @@ pub trait GraphTopology { fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor; } -// TODO: Add functions to reserve memory for vertices and edges. /// A trait that adds construction operations to an undirected graph topology. /// /// This trait provides methods for addition of vertices and edges, and capacity management in an @@ -186,6 +185,20 @@ pub trait GraphTopologyAddition: GraphTopology { /// Returns the total number of edges the graph can hold without reallocating. fn edge_capacity(&self) -> usize; + /// Reserves capacity for at least `additional` more vertices. Does nothing if capacity is + /// already sufficient. + /// + /// Use this before adding multiple vertices to avoid incremental growth on each call to + /// [`add_vertex`]. + fn reserve_vertices(&mut self, additional: usize); + + /// Reserves capacity for at least `additional` more edges. Does nothing if capacity is already + /// sufficient. + /// + /// Use this before adding multiple edges to avoid incremental growth on each call to + /// [`add_edge`]. + fn reserve_edges(&mut self, additional: usize); + /// Adds a new isolated vertex and returns its handle. fn add_vertex(&mut self) -> Self::Vertex; -- 2.43.0 From 3d99501d4bbaf9a679637b8e2419579d7ec23a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 17 Aug 2026 10:16:54 +0200 Subject: [PATCH 22/67] Fix inline documentation after recent changes --- src/lib.rs | 6 +++--- src/maps.rs | 3 +++ src/traits.rs | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 946e3f3..3b617ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,8 +91,7 @@ //! graph.degree(v); //! ``` //! -//! [`VertexMap`] and [`EdgeMap`] are not required to panic if provided with an invalid -//! vertex or edge. +//! [`ElementMap`] is not required to panic if provided with an invalid vertex or edge. //! //! Vertices and edges are provided by the graph and not intended to be generated by users. //! @@ -121,11 +120,12 @@ //! [`ElementMap`]: crate::maps::ElementMap //! [`AppendGraph`]: crate::models::AppendGraph //! [`Graph`]: crate::models::Graph +//! [`traits`]: crate::traits //! [`GraphTopology`]: crate::traits::GraphTopology //! [`GraphTopology::Edge`]: crate::traits::GraphTopology::Edge //! [`GraphTopology::Vertex`]: crate::traits::GraphTopology::Vertex +//! [`GraphTopologyAddition`]: crate::traits::GraphTopologyAddition //! [`GraphTopologyDeletion`]: crate::traits::GraphTopologyDeletion -//! [`traits`]: crate::traits #![warn(missing_docs)] diff --git a/src/maps.rs b/src/maps.rs index ac7dbdc..c96701d 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -18,6 +18,9 @@ use std::ops::{Index, IndexMut}; /// /// Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] to obtain an `ElementMap` for /// vertices or edges, respectively. +/// +/// [`GraphTopology::edge_map`]: crate::traits::GraphTopology::edge_map +/// [`GraphTopology::vertex_map`]: crate::traits::GraphTopology::vertex_map pub struct ElementMap { data: Vec, default: T, diff --git a/src/traits.rs b/src/traits.rs index ca525f8..afb11ab 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -190,6 +190,8 @@ pub trait GraphTopologyAddition: GraphTopology { /// /// Use this before adding multiple vertices to avoid incremental growth on each call to /// [`add_vertex`]. + /// + /// [`add_vertex`]: GraphTopologyAddition::add_vertex fn reserve_vertices(&mut self, additional: usize); /// Reserves capacity for at least `additional` more edges. Does nothing if capacity is already @@ -197,6 +199,8 @@ pub trait GraphTopologyAddition: GraphTopology { /// /// Use this before adding multiple edges to avoid incremental growth on each call to /// [`add_edge`]. + /// + /// [`add_edge`]: GraphTopologyAddition::add_edge fn reserve_edges(&mut self, additional: usize); /// Adds a new isolated vertex and returns its handle. -- 2.43.0 From cd1d2cddee18dac9b7db2c9dc805260bda296b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 17 Aug 2026 10:19:22 +0200 Subject: [PATCH 23/67] Move related tests into new testing macro for GraphTopologyAddition --- src/models/append_graph.rs | 1 + src/models/graph.rs | 1 + src/testing/graph_topology_testing.rs | 42 +++++++++++++++------------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 9526f95..4468980 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -241,6 +241,7 @@ mod tests { crate::graph_topology_test_fixtures!(AppendGraph); crate::graph_topology_tests!(AppendGraph); + crate::graph_topology_addition_tests!(AppendGraph); #[test] fn incident_vertices_paired_index() { diff --git a/src/models/graph.rs b/src/models/graph.rs index 702d244..4ccef26 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -383,6 +383,7 @@ mod tests { crate::graph_topology_test_fixtures!(Graph); crate::graph_topology_tests!(Graph); + crate::graph_topology_addition_tests!(Graph); crate::graph_topology_deletion_tests!(Graph); #[test] diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 47d6569..07e9793 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -110,14 +110,6 @@ macro_rules! graph_topology_test_fixtures { #[macro_export] macro_rules! graph_topology_tests { ($T:ty) => { - #[test] - fn add_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); - } - #[test] fn vertex_count_empty() { use $crate::traits::GraphTopology; @@ -145,16 +137,6 @@ macro_rules! graph_topology_tests { assert_eq!(map[v2], 27, "unexpected value from default map read for new vertex"); } - #[test] - fn add_edge() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); - } - #[test] fn edge_count_empty() { use $crate::traits::GraphTopology; @@ -908,7 +890,13 @@ macro_rules! graph_topology_tests { ); } } + }; +} +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_tests { + ($T:ty) => { #[test] fn reserve_vertices_increases_capacity() { use $crate::traits::GraphTopologyAddition; @@ -992,6 +980,24 @@ macro_rules! graph_topology_tests { "reserve_edges must not change vertex capacity" ); } + + #[test] + fn add_vertex() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let v = graph.add_vertex(); + assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); + } + + #[test] + fn add_edge() { + use $crate::traits::GraphTopologyAddition; + let mut graph = <$T>::new(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + let e = graph.add_edge(v1, v2); + assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); + } }; } -- 2.43.0 From 5085b7795b720f3ba782a14e2caae2f733b67a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 17 Aug 2026 10:20:53 +0200 Subject: [PATCH 24/67] Remove local vars in AppendGraph::incident_vertices and construct result directly --- src/models/append_graph.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 4468980..b06971d 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -176,9 +176,10 @@ impl GraphTopology for AppendGraph { } fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { - let v2 = self.incidences[e.0].adjacent; - let v1 = self.incidences[e.0 ^ 1].adjacent; - (v1, v2) + ( + self.incidences[e.0 ^ 1].adjacent, + self.incidences[e.0].adjacent, + ) } fn edges(&self) -> impl Iterator { -- 2.43.0 From a89086253a0f858c2cbbe50e0d87bd5e9763cba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 17 Aug 2026 10:25:52 +0200 Subject: [PATCH 25/67] Add FrozenGraph (without tests) --- README.md | 2 +- src/lib.rs | 17 ++- src/models.rs | 6 +- src/models/append_graph.rs | 4 +- src/models/frozen_graph.rs | 250 +++++++++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 src/models/frozen_graph.rs diff --git a/README.md b/README.md index 417af7c..b471e97 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This crate provides data structures to model graphs and to perform algorithms on Currently supported are: -* Undirected graph types `Graph` and `AppendGraph`, built on a flat, index-based adjacency list, +* Undirected graph types `Graph`, `AppendGraph`, and `FrozenGraph`, built on a flat, index-based adjacency list, * Maps to freely associate custom data with vertices and edges, * Connectivity and pathing algorithms: Dijkstra's algorithm, DFS, and BFS in different variants, * Exposed traits to implement custom graph data structures or algorithms. diff --git a/src/lib.rs b/src/lib.rs index 3b617ef..014e586 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,8 @@ //! //! Currently supported are: //! -//! * Undirected graph types [`Graph`] and [`AppendGraph`] built on a flat, index-based adjacency -//! list, +//! * Undirected graph types [`Graph`], [`AppendGraph`], and [`FrozenGraph`] built on a flat, +//! index-based adjacency list, //! * [`ElementMap`] to freely associate custom data with vertices and edges, //! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different //! variants, @@ -62,9 +62,11 @@ //! //! # Graph types //! -//! [`Graph`] and [`AppendGraph`] both implement [`GraphTopology`] using a flat, index-based adjacency -//! list to store edges, which means that vertex and edge insertions happen in `O(1)`. Degree -//! lookups are also `O(1)`. +//! [`Graph`], [`AppendGraph`], and [`FrozenGraph`] all implement [`GraphTopology`] using a flat, +//! index-based adjacency list to store edges. Degree lookups run in `O(1)`. +//! +//! [`Graph`] and [`AppendGraph`] implement [`GraphTopologyAddition`], which provides functionality +//! for iterative graph construction. Vertex and edge insertions happen in `O(1)`. //! //! [`Graph`] additionally implements [`GraphTopologyDeletion`], thereby supporting deletion of //! vertices and edges. @@ -73,6 +75,10 @@ //! be smaller and more performant compared to [`Graph`] by not requiring the per-element generation //! tracking needed for stable handles after deletion. //! +//! [`FrozenGraph`] uses a memory layout that is more cache-friendly for traversals than +//! [`AppendGraph`], but the trade-off is that its topology must be constructed during its +//! initialization. It cannot be modified afterward. +//! //! # Vertices and edges //! //! Graphs in this library use stable indices [`GraphTopology::Vertex`] and [`GraphTopology::Edge`] @@ -119,6 +125,7 @@ //! [DFS]: crate::algorithms::dfs //! [`ElementMap`]: crate::maps::ElementMap //! [`AppendGraph`]: crate::models::AppendGraph +//! [`FrozenGraph`]: crate::models::FrozenGraph //! [`Graph`]: crate::models::Graph //! [`traits`]: crate::traits //! [`GraphTopology`]: crate::traits::GraphTopology diff --git a/src/models.rs b/src/models.rs index 1f302c8..0834a4a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,11 +1,13 @@ //! Concrete graph topology models. pub mod append_graph; +pub mod frozen_graph; pub mod graph; -// TODO: Compressed-sparse-row graph model. - pub use append_graph::{ AppendGraph, AppendGraphEdgeMap, AppendGraphIncidence, AppendGraphVertexMap, }; +pub use frozen_graph::{ + FrozenGraph, FrozenGraphEdgeMap, FrozenGraphIncidence, FrozenGraphVertexMap, +}; pub use graph::{Graph, GraphEdgeMap, GraphIncidence, GraphVertexMap}; diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index b06971d..ad7de88 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -67,7 +67,8 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// /// `AppendGraph` is optimised for workloads that incrementally build a graph and query it /// repeatedly. [`Vertex`] and [`Edge`] handles are never invalidated. Use [`Graph`] instead if you -/// need to remove vertices or edges. +/// need to remove vertices or edges. Use [`FrozenGraph`] if you do not need incremental +/// construction. /// /// Incidences are stored as interleaved adjacency lists in a single flat [`Vec`]. In general, /// vertex neighborhood traversals result in scattered index jumps. @@ -93,6 +94,7 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// [`add_vertex`]: Self::add_vertex /// [`add_edge`]: Self::add_edge /// [`degree`]: Self::degree +/// [`FrozenGraph`]: crate::models::FrozenGraph /// [`Graph`]: crate::models::Graph pub struct AppendGraph { vertices: Vec, diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs new file mode 100644 index 0000000..b2980dc --- /dev/null +++ b/src/models/frozen_graph.rs @@ -0,0 +1,250 @@ +//! [`FrozenGraph`], an undirected graph topology supporting only one-time construction. + +use std::ops::IndexMut; + +use crate::maps::ElementMap; +use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; + +/// An opaque handle identifying a vertex in a [`FrozenGraph`]. +/// +/// Handles are stable for the lifetime of the graph. Obtain via graph methods like +/// [`FrozenGraph::vertices`]. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct Vertex(usize); + +/// An opaque handle identifying an edge in a [`FrozenGraph`]. +/// +/// Handles are stable for the lifetime of the graph. Obtain via graph methods like +/// [`FrozenGraph::edges`]. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct Edge(usize); + +/// An [`ElementMap`] for [`FrozenGraph`] vertices. +pub type FrozenGraphVertexMap = ElementMap; + +/// An [`ElementMap`] for [`FrozenGraph`] edges. +pub type FrozenGraphEdgeMap = ElementMap; + +/// An [`Incidence`] for [`FrozenGraph`]. +pub type FrozenGraphIncidence = Incidence; + +#[derive(Copy, Clone)] +struct IncidenceEntry { + adjacent: Vertex, + opposite: usize, +} + +/// A resumable cursor over the incidences of a single vertex in a [`FrozenGraph`]. +/// +/// Obtain via [`FrozenGraph::incidence_cursor`]. See [`IncidenceCursor`] on usage guidance. +#[derive(Copy, Clone)] +pub struct FrozenGraphIncidenceCursor { + current: usize, + end: usize, +} + +impl IncidenceCursor for FrozenGraphIncidenceCursor { + fn next(&mut self, graph: &FrozenGraph) -> Option { + if self.current < self.end { + let incidence = graph.normalize_incidence(self.current); + self.current += 1; + Some(incidence) + } else { + None + } + } +} + +/// An undirected graph that supports only one-time construction, and no further modification. +/// +/// `FrozenGraph` is optimised for workloads that repeatedly query a graph, but do not require +/// changing it once it is constructed. [`Vertex`] and [`Edge`] handles are never invalidated. Use +/// [`AppendGraph`] instead if you need to incrementally add vertices or edges, or [`Graph`] if you +/// need to remove vertices or edges after initial construction. +/// +/// `FrozenGraph` uses a compressed-sparse data representation. Incidences are stored as contiguous +/// adjacency lists per vertex in a single flat [`Vec`]. Vertex neighborhood traversals are +/// therefore cache-friendly without index jumps. +/// +/// # Examples +/// +/// ``` +/// use grapherity::prelude::*; +/// use grapherity::models::{FrozenGraph, AppendGraph}; +/// +/// // Constructs a FrozenGraph instance via AppendGraph. +/// let mut graph = AppendGraph::new(); +/// let v1 = graph.add_vertex(); +/// let v2 = graph.add_vertex(); +/// let e = graph.add_edge(v1, v2); +/// let (graph, vertices, edges) = FrozenGraph::from_graph(&graph); +/// +/// // Queries the FrozenGraph instance. +/// assert!(graph.are_adjacent(vertices[v1], vertices[v2])); +/// ``` +/// +/// # Time and space complexity +/// +/// [`degree`] runs in *O(1)* time. Space complexity is *O(|V| + |E|)*. +/// +/// [`degree`]: Self::degree +/// [`AppendGraph`]: crate::models::AppendGraph +/// [`Graph`]: crate::models::Graph +pub struct FrozenGraph { + vertices: Vec, + incidences: Vec, +} + +impl FrozenGraph { + /// Creates a graph instance as a copy of another graph. + /// + /// Returns the copied graph instance and two mappings of the `source` graph vertices and edges + /// to the new ones. Note that the returned mappings cannot support additional growth, i.e. any + /// vertex or edge added to the `source` graph after calling `from_graph` will map to an invalid + /// vertex or edge in the new `FrozenGraph`. + pub fn from_graph( + source: &G, + ) -> ( + Self, + ElementMap, + ElementMap, + ) { + // Builds vertex list with sentinel and vertex map. + let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); + let mut vertices = Vec::with_capacity(source.vertex_count() + 1); + let mut offset = 0; + for (i, v) in source.vertices().enumerate() { + vertex_map[v] = Vertex(i); + vertices.push(offset); + offset += source.degree(v); + } + vertices.push(offset); + + // Builds incidence list and edge map. Uses "usize::MAX" as "unset" here: an edge can never + // be normalized to "usize::MAX", since it must be the smaller index of two incidences. + const UNSET: usize = usize::MAX; + let mut edge_map = source.edge_map(Edge(UNSET)); + let mut incidences = Vec::with_capacity(source.edge_count() * 2); + for v in source.vertices() { + for Incidence { vertex: u, edge: e } in source.incidences(v) { + let em = edge_map.index_mut(e); + if em.0 == UNSET { + // Adds the first incidence for the edge, but we don't know the opposite, yet. + *em = Edge(incidences.len()); + incidences.push(IncidenceEntry { + adjacent: vertex_map[u], + opposite: 0, + }); + } else { + // Adds the second incidence for the edge and fills the opposite for the first. + incidences[em.0].opposite = incidences.len(); + incidences.push(IncidenceEntry { + adjacent: vertex_map[u], + opposite: em.0, + }); + } + } + } + + ( + Self { + vertices, + incidences, + }, + vertex_map, + edge_map, + ) + } + + fn raw_incidences(&self, v: Vertex) -> impl Iterator { + self.vertices[v.0]..self.vertices[v.0 + 1] + } + + fn normalize_edge(&self, e: usize) -> Edge { + let f = self.incidences[e].opposite; + Edge(if e < f { e } else { f }) + } + + fn normalize_incidence(&self, i: usize) -> FrozenGraphIncidence { + let entry = &self.incidences[i]; + Incidence { + vertex: entry.adjacent, + edge: Edge(if i < entry.opposite { + i + } else { + entry.opposite + }), + } + } +} + +impl From<&G> for FrozenGraph { + fn from(graph: &G) -> Self { + Self::from_graph(graph).0 + } +} + +impl GraphTopology for FrozenGraph { + type Vertex = Vertex; + type Edge = Edge; + type IncidenceCursor = FrozenGraphIncidenceCursor; + + fn vertex_count(&self) -> usize { + self.vertices.len() - 1 + } + + fn vertex_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |v| v.0, self.vertex_count()) + } + + fn edge_count(&self) -> usize { + self.incidences.len() / 2 + } + + fn edge_map(&self, default: T) -> ElementMap { + ElementMap::new(default, |e| e.0, self.edge_count()) + } + + fn degree(&self, v: Self::Vertex) -> usize { + self.vertices[v.0 + 1] - self.vertices[v.0] + } + + fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool { + self.adjacent_vertices(v1).any(|x| x == v2) + } + + fn vertices(&self) -> impl Iterator { + (0..self.vertices.len() - 1).map(Vertex) + } + + fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|i| self.incidences[i].adjacent) + } + + fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { + let entry = &self.incidences[e.0]; + (self.incidences[entry.opposite].adjacent, entry.adjacent) + } + + fn edges(&self) -> impl Iterator { + self.incidences + .iter() + .enumerate() + .filter_map(|(i, x)| (i < x.opposite).then_some(Edge(i))) + } + + fn incident_edges(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|i| self.normalize_edge(i)) + } + + fn incidences(&self, v: Self::Vertex) -> impl Iterator { + self.raw_incidences(v).map(|i| self.normalize_incidence(i)) + } + + fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor { + FrozenGraphIncidenceCursor { + current: self.vertices[v.0], + end: self.vertices[v.0 + 1], + } + } +} -- 2.43.0 From cbabb8f960e6a986739e7592cfdfafdf366ed856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 27 Aug 2026 20:11:03 +0200 Subject: [PATCH 26/67] Remove unnecessary doc link path parts --- src/lib.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 014e586..c45198a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,19 +120,19 @@ //! assert_eq!(graph.degree(v1), 6); //! ``` //! -//! [BFS]: crate::algorithms::bfs -//! [Dijkstra's algorithm]: crate::algorithms::dijkstra -//! [DFS]: crate::algorithms::dfs -//! [`ElementMap`]: crate::maps::ElementMap -//! [`AppendGraph`]: crate::models::AppendGraph -//! [`FrozenGraph`]: crate::models::FrozenGraph -//! [`Graph`]: crate::models::Graph -//! [`traits`]: crate::traits -//! [`GraphTopology`]: crate::traits::GraphTopology -//! [`GraphTopology::Edge`]: crate::traits::GraphTopology::Edge -//! [`GraphTopology::Vertex`]: crate::traits::GraphTopology::Vertex -//! [`GraphTopologyAddition`]: crate::traits::GraphTopologyAddition -//! [`GraphTopologyDeletion`]: crate::traits::GraphTopologyDeletion +//! [BFS]: algorithms::bfs +//! [Dijkstra's algorithm]: algorithms::dijkstra +//! [DFS]: algorithms::dfs +//! [`ElementMap`]: maps::ElementMap +//! [`AppendGraph`]: models::AppendGraph +//! [`FrozenGraph`]: models::FrozenGraph +//! [`Graph`]: models::Graph +//! [`traits`]: traits +//! [`GraphTopology`]: traits::GraphTopology +//! [`GraphTopology::Edge`]: traits::GraphTopology::Edge +//! [`GraphTopology::Vertex`]: traits::GraphTopology::Vertex +//! [`GraphTopologyAddition`]: traits::GraphTopologyAddition +//! [`GraphTopologyDeletion`]: traits::GraphTopologyDeletion #![warn(missing_docs)] -- 2.43.0 From 1185e3b66737f301130e356da0646e34d985411c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 27 Aug 2026 20:11:54 +0200 Subject: [PATCH 27/67] Add Default trait to GraphTopologyAddition --- src/traits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traits.rs b/src/traits.rs index afb11ab..fda781a 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -178,7 +178,7 @@ pub trait GraphTopology { /// /// This trait provides methods for addition of vertices and edges, and capacity management in an /// undirected graph. -pub trait GraphTopologyAddition: GraphTopology { +pub trait GraphTopologyAddition: GraphTopology + Default { /// Returns the total number of vertices the graph can hold without reallocating. fn vertex_capacity(&self) -> usize; -- 2.43.0 From 7fbd5eed9dcb4192a63c63613414b2fd71b5551f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 29 Aug 2026 22:38:27 +0200 Subject: [PATCH 28/67] Refactor graph creation in all tests into new internal MakeTestGraph trait This large refactor allows future re-use of the existing tests for `FrozenGraph`, which does not implement `GraphTopologyAddition`, but instead has to convert from a different graph which does. - Fix tests for `GraphTopologyDeletion` hard-coding Graph instead of using the trait. - Fix some tests silently depending on macro call site uses. - Change `GraphTopologyAddition` tests to use `Default::default` instead of `new`. - Add new tests for `GraphTopology::vertex_map` and `edge_map` that also depend on `GraphTopologyAddition`, removing these dependencies from the existing tests. - Add new test macro `graph_topology_addition_deletion_tests` for tests that need deletion and addition. --- src/lib.rs | 2 +- src/models/append_graph.rs | 10 +- src/models/graph.rs | 11 +- src/testing.rs | 3 + src/testing/bfs_testing.rs | 45 +- src/testing/dfs_testing.rs | 71 +-- src/testing/dijkstra_testing.rs | 69 ++- src/testing/fixtures.rs | 164 ++++++ src/testing/graph_topology_testing.rs | 718 +++++++++----------------- tests/bfs.rs | 2 - tests/dfs.rs | 2 - tests/dijkstra.rs | 2 - 12 files changed, 509 insertions(+), 590 deletions(-) create mode 100644 src/testing/fixtures.rs diff --git a/src/lib.rs b/src/lib.rs index c45198a..c2515f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,4 +149,4 @@ pub mod prelude { }; } -mod testing; +pub mod testing; diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index ad7de88..84730c3 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -239,12 +239,16 @@ impl GraphTopologyAddition for AppendGraph { } #[cfg(test)] -mod tests { - use super::*; +mod trait_tests { + use super::AppendGraph; - crate::graph_topology_test_fixtures!(AppendGraph); crate::graph_topology_tests!(AppendGraph); crate::graph_topology_addition_tests!(AppendGraph); +} + +#[cfg(test)] +mod tests { + use super::*; #[test] fn incident_vertices_paired_index() { diff --git a/src/models/graph.rs b/src/models/graph.rs index 4ccef26..5eda23a 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -378,13 +378,18 @@ impl GraphTopologyDeletion for Graph { } #[cfg(test)] -mod tests { - use super::*; +mod trait_tests { + use super::Graph; - crate::graph_topology_test_fixtures!(Graph); crate::graph_topology_tests!(Graph); crate::graph_topology_addition_tests!(Graph); crate::graph_topology_deletion_tests!(Graph); + crate::graph_topology_addition_deletion_tests!(Graph); +} + +#[cfg(test)] +mod tests { + use super::*; #[test] fn incident_vertices_paired_index() { diff --git a/src/testing.rs b/src/testing.rs index d815794..b92fa0d 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -4,3 +4,6 @@ pub(crate) mod bfs_testing; pub(crate) mod dfs_testing; pub(crate) mod dijkstra_testing; pub(crate) mod graph_topology_testing; + +#[doc(hidden)] +pub mod fixtures; diff --git a/src/testing/bfs_testing.rs b/src/testing/bfs_testing.rs index 63f8fae..2d5bf23 100644 --- a/src/testing/bfs_testing.rs +++ b/src/testing/bfs_testing.rs @@ -4,9 +4,7 @@ macro_rules! bfs_tests { ($T:ty) => { #[test] fn bfs_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let result = $crate::algorithms::bfs(&graph, v); assert_eq!( result.distances[v], @@ -21,7 +19,8 @@ macro_rules! bfs_tests { #[test] fn bfs_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); let result = $crate::algorithms::bfs(&graph, vertices[0]); assert_eq!( result.distances[vertices[0]], @@ -46,7 +45,8 @@ macro_rules! bfs_tests { #[test] fn bfs() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); let result = $crate::algorithms::bfs(&graph, vertices[0]); assert_bfs_distances(&result.distances, &vertices); assert_bfs_predecessors(&result.predecessors, &vertices); @@ -54,9 +54,7 @@ macro_rules! bfs_tests { #[test] fn bfs_distances_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let distances = $crate::algorithms::bfs_distances(&graph, v); assert_eq!( distances[v], @@ -67,7 +65,8 @@ macro_rules! bfs_tests { #[test] fn bfs_distances_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); let distances = $crate::algorithms::bfs_distances(&graph, vertices[0]); assert_eq!( distances[vertices[0]], @@ -84,16 +83,15 @@ macro_rules! bfs_tests { #[test] fn bfs_distances() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); let distances = $crate::algorithms::bfs_distances(&graph, vertices[0]); assert_bfs_distances(&distances, &vertices); } #[test] fn bfs_find_source() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( $crate::algorithms::bfs_find(&graph, v, v), Some(0), @@ -103,7 +101,8 @@ macro_rules! bfs_tests { #[test] fn bfs_find_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); assert_eq!( $crate::algorithms::bfs_find(&graph, vertices[0], vertices[1]), None, @@ -113,7 +112,8 @@ macro_rules! bfs_tests { #[test] fn bfs_find() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); let expected_distances = [ Some(0), Some(1), @@ -139,9 +139,7 @@ macro_rules! bfs_tests { #[test] fn bfs_find_where_source_matches() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( $crate::algorithms::bfs_find_where(&graph, v, |u| u == v), Some((v, 0)), @@ -151,7 +149,8 @@ macro_rules! bfs_tests { #[test] fn bfs_find_where_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); assert_eq!( $crate::algorithms::bfs_find_where(&graph, vertices[0], |v| v == vertices[1]), None, @@ -162,7 +161,8 @@ macro_rules! bfs_tests { #[test] fn bfs_find_where_no_match() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!( $crate::algorithms::bfs_find_where(&graph, vertices[0], |_| false), None, @@ -172,7 +172,8 @@ macro_rules! bfs_tests { #[test] fn bfs_find_where_nearest() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); // vertices[5], vertices[6], vertices[7], vertices[8] are all at distance 3 from vertices[0]. // vertices[9] is at distance 4. Predicate matches vertices[7], vertices[8], vertices[9]. // BFS must return one of the distance-3 ones, not vertices[9]. @@ -193,6 +194,7 @@ macro_rules! bfs_tests { ); } + // TODO: Move out of macro. fn assert_bfs_distances( distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, @@ -221,6 +223,7 @@ macro_rules! bfs_tests { } } + // TODO: Move out of macro. fn assert_bfs_predecessors( predecessors: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs index 61f4944..e57ad56 100644 --- a/src/testing/dfs_testing.rs +++ b/src/testing/dfs_testing.rs @@ -4,9 +4,7 @@ macro_rules! dfs_tests { ($T:ty) => { #[test] fn dfs_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let result = $crate::algorithms::dfs(&graph, v); assert!(result.visited[v], "source vertex should be visited"); assert_eq!( @@ -17,7 +15,7 @@ macro_rules! dfs_tests { #[test] fn dfs_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); let result = $crate::algorithms::dfs(&graph, vertices[0]); assert!( result.visited[vertices[0]], @@ -41,7 +39,7 @@ macro_rules! dfs_tests { #[test] fn dfs() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); let result = $crate::algorithms::dfs(&graph, vertices[0]); assert_dfs_visited(&result.visited, &vertices); assert_dfs_predecessors(&graph, &result.visited, &result.predecessors, &vertices); @@ -49,16 +47,14 @@ macro_rules! dfs_tests { #[test] fn dfs_visited_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let visited = $crate::algorithms::dfs_visited(&graph, v); assert!(visited[v], "source vertex should be visited"); } #[test] fn dfs_visited_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); let visited = $crate::algorithms::dfs_visited(&graph, vertices[0]); assert!(visited[vertices[0]], "source vertex should be visited"); for &v in &vertices[1..3] { @@ -71,16 +67,14 @@ macro_rules! dfs_tests { #[test] fn dfs_visited() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); let visited = $crate::algorithms::dfs_visited(&graph, vertices[0]); assert_dfs_visited(&visited, &vertices); } #[test] fn dfs_find_source() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert!( $crate::algorithms::dfs_find(&graph, v, v), "source should find itself" @@ -89,7 +83,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); assert!( !$crate::algorithms::dfs_find(&graph, vertices[0], vertices[1]), "disconnected target should not be found" @@ -98,7 +92,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for i in 0..10 { assert!( $crate::algorithms::dfs_find(&graph, vertices[0], vertices[i]), @@ -111,9 +105,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_where_source_matches() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( $crate::algorithms::dfs_find_where(&graph, v, |u| u == v), Some(v), @@ -123,7 +115,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_where_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); assert_eq!( $crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), None, @@ -134,7 +126,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_where_no_match() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!( $crate::algorithms::dfs_find_where(&graph, vertices[0], |_| false), None, @@ -144,7 +136,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_where_adjacent() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!( $crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), Some(vertices[1]), @@ -154,7 +146,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_where() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!( $crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[9]), Some(vertices[9]), @@ -162,6 +154,7 @@ macro_rules! dfs_tests { ); } + // TODO: Move out of macro. fn assert_dfs_visited( visited: &$crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], @@ -175,6 +168,7 @@ macro_rules! dfs_tests { } } + // TODO: Move out of macro. fn assert_dfs_predecessors( graph: &$T, visited: &$crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, @@ -205,9 +199,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_source_equals_target() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( $crate::algorithms::dfs_find_path(&graph, v, v), Some(vec![]), @@ -217,7 +209,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); assert_eq!( $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[1]), None, @@ -227,24 +219,16 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_adjacent() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - let path = $crate::algorithms::dfs_find_path(&graph, v1, v2) + let (graph, vertices, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); + let path = $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[1]) .expect("path should exist between adjacent vertices"); - assert_eq!( - path.len(), - 1, - "unexpected path length between adjacent vertices" - ); + assert_eq!(path.len(), 1, "unexpected path length between adjacent vertices"); assert_eq!(path[0], e, "path should use the connecting edge"); } #[test] fn dfs_find_path() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); let path = $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[9]) .expect(&format!( "path should exist between connected vertices {:?} and {:?}", @@ -255,9 +239,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_where_source_matches() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( $crate::algorithms::dfs_find_path_where(&graph, v, |u| u == v), Some(vec![]), @@ -267,7 +249,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_where_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); assert_eq!( $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), None, @@ -277,7 +259,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_where_no_match() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!( $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |_| false), None, @@ -287,7 +269,7 @@ macro_rules! dfs_tests { #[test] fn dfs_find_path_where() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); let path = $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]) .expect(&format!( @@ -297,6 +279,7 @@ macro_rules! dfs_tests { assert_valid_path(&graph, &path, vertices[0], vertices[9]); } + // TODO: Move out of macro. fn assert_valid_path( graph: &$T, path: &[<$T as $crate::traits::GraphTopology>::Edge], diff --git a/src/testing/dijkstra_testing.rs b/src/testing/dijkstra_testing.rs index d015ca6..fc3d333 100644 --- a/src/testing/dijkstra_testing.rs +++ b/src/testing/dijkstra_testing.rs @@ -4,9 +4,7 @@ macro_rules! dijkstra_tests { ($T:ty) => { #[test] fn dijkstra_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let result = $crate::algorithms::dijkstra(&graph, v, |_| { panic!("unexpected call of weight functor") }); @@ -15,7 +13,8 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); let result = $crate::algorithms::dijkstra(&graph, vertices[0], |_| { panic!("unexpected call of weight functor") }); @@ -25,17 +24,7 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_zero_weight_loop() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::new(); - let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 3] = - core::array::from_fn(|_| graph.add_vertex()); - let e1 = graph.add_edge(vertices[0], vertices[0]); - let e2 = graph.add_edge(vertices[1], vertices[1]); - graph.add_edge(vertices[0], vertices[1]); - graph.add_edge(vertices[1], vertices[2]); - let mut weights = graph.edge_map(1); - weights[e1] = 0; - weights[e2] = 0; + let (graph, vertices, _, weights) = make_two_edge_path_with_zero_weight_loops(); let result = $crate::algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); assert_single_vertex(&result, vertices[0]); for i in 1..3 { @@ -71,16 +60,15 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_unweighted_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let result = $crate::algorithms::dijkstra_unweighted(&graph, v); assert_single_vertex(&result, v) } #[test] fn dijkstra_unweighted_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); let result = $crate::algorithms::dijkstra_unweighted(&graph, vertices[0]); assert_single_vertex(&result, vertices[0]); assert_disconnected(&result, &vertices[1..3]); @@ -88,23 +76,23 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_unweighted() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); let result = $crate::algorithms::dijkstra_unweighted(&graph, vertices[0]); assert_unweighted_test_graph(&result, &vertices); } #[test] fn dijkstra_distances_unweighted_single_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, v); assert_distances_single_vertex(&distances, v); } #[test] fn dijkstra_distances_unweighted_disconnected() { - let (graph, vertices) = make_test_graph_disconnected(); + let (graph, vertices): ($T, _) = + $crate::testing::fixtures::MakeTestGraph::disconnected(); let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); assert_distances_single_vertex(&distances, vertices[0]); assert_distances_disconnected(&distances, &vertices[1..3]); @@ -112,11 +100,13 @@ macro_rules! dijkstra_tests { #[test] fn dijkstra_distances_unweighted() { - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); assert_distances_unweighted_test_graph(&distances, &vertices); } + // TODO: Move out of macro. fn assert_single_vertex( result: &$crate::algorithms::DijkstraResult< <$T as $crate::traits::GraphTopology>::Vertex, @@ -130,6 +120,7 @@ macro_rules! dijkstra_tests { ); } + // TODO: Move out of macro. fn assert_distances_single_vertex( distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, @@ -144,6 +135,7 @@ macro_rules! dijkstra_tests { ); } + // TODO: Move out of macro. fn assert_disconnected( result: &$crate::algorithms::DijkstraResult< <$T as $crate::traits::GraphTopology>::Vertex, @@ -159,6 +151,7 @@ macro_rules! dijkstra_tests { } } + // TODO: Move out of macro. fn assert_distances_disconnected( distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, @@ -174,6 +167,7 @@ macro_rules! dijkstra_tests { } } + // TODO: Move out of macro. fn assert_test_graph( result: &$crate::algorithms::DijkstraResult< <$T as $crate::traits::GraphTopology>::Vertex, @@ -203,6 +197,7 @@ macro_rules! dijkstra_tests { } } + // TODO: Move out of macro. fn assert_distances_test_graph( distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, @@ -231,6 +226,7 @@ macro_rules! dijkstra_tests { } } + // TODO: Move out of macro. fn assert_unweighted_test_graph( result: &$crate::algorithms::DijkstraResult< <$T as $crate::traits::GraphTopology>::Vertex, @@ -260,6 +256,7 @@ macro_rules! dijkstra_tests { } } + // TODO: Move out of macro. fn assert_distances_unweighted_test_graph( distances: &$crate::maps::ElementMap< <$T as $crate::traits::GraphTopology>::Vertex, @@ -288,6 +285,7 @@ macro_rules! dijkstra_tests { } } + // TODO: Move out of macro. fn make_test_graph_weighted() -> ( $T, [<$T as $crate::traits::GraphTopology>::Vertex; 10], @@ -305,12 +303,29 @@ macro_rules! dijkstra_tests { $crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, ) { use $crate::traits::GraphTopology; - let (graph, vertices, edges, incidences) = make_test_graph(); - let mut weights = graph.edge_map(99_u32); + let (graph, vertices, edges, incidences): ($T, _, _, _) = + $crate::testing::fixtures::MakeTestGraph::standard(); + let mut weights = graph.edge_map(99); for i in [1, 3, 7, 10, 12, 14, 15, 17] { weights[edges[i].0] = i.try_into().unwrap(); } (graph, vertices, edges, incidences, weights) } + + // TODO: Move out of macro. + fn make_two_edge_path_with_zero_weight_loops() -> ( + $T, + [<$T as $crate::traits::GraphTopology>::Vertex; 3], + [<$T as $crate::traits::GraphTopology>::Edge; 4], + $crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, + ) { + use $crate::traits::GraphTopology; + let (graph, vertices, edges): ($T, _, _) = + $crate::testing::fixtures::MakeTestGraph::two_edge_path_with_loops(); + let mut weights = graph.edge_map(1); + weights[edges[2]] = 0; + weights[edges[3]] = 0; + (graph, vertices, edges, weights) + } }; } diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs new file mode 100644 index 0000000..d4c2109 --- /dev/null +++ b/src/testing/fixtures.rs @@ -0,0 +1,164 @@ +use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; + +pub trait MakeTestGraph: GraphTopology + Sized { + fn standard() -> ( + Self, + [Self::Vertex; 10], + [(Self::Edge, Self::Vertex, Self::Vertex); 18], + [Vec>; 10], + ); + + fn empty() -> Self; + + fn single_vertex() -> (Self, Self::Vertex); + + fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge); + + fn disconnected() -> (Self, [Self::Vertex; 3]); + + fn loop_edge() -> (Self, Self::Vertex, Self::Edge); + + fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]); + + fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]); + + fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]); +} + +impl MakeTestGraph for G { + fn standard() -> ( + Self, + [Self::Vertex; 10], + [(Self::Edge, Self::Vertex, Self::Vertex); 18], + [Vec>; 10], + ) { + let mut graph = Self::default(); + let vertices = core::array::from_fn(|_| graph.add_vertex()); + let edges = [ + (vertices[0], vertices[1]), + (vertices[0], vertices[1]), + (vertices[1], vertices[2]), + (vertices[1], vertices[3]), + (vertices[1], vertices[4]), + (vertices[2], vertices[2]), + (vertices[2], vertices[4]), + (vertices[2], vertices[4]), + (vertices[2], vertices[5]), + (vertices[2], vertices[6]), + (vertices[3], vertices[6]), + (vertices[4], vertices[4]), + (vertices[4], vertices[7]), + (vertices[4], vertices[8]), + (vertices[5], vertices[9]), + (vertices[6], vertices[9]), + (vertices[7], vertices[8]), + (vertices[7], vertices[9]), + ] + .map(|(v1, v2)| (graph.add_edge(v1, v2), v1, v2)); + let i = |vertex, edge| Incidence { vertex, edge }; + let incidences = [ + vec![i(vertices[1], edges[0].0), i(vertices[1], edges[1].0)], + vec![ + i(vertices[0], edges[0].0), + i(vertices[0], edges[1].0), + i(vertices[2], edges[2].0), + i(vertices[3], edges[3].0), + i(vertices[4], edges[4].0), + ], + vec![ + i(vertices[2], edges[5].0), + i(vertices[2], edges[5].0), + i(vertices[1], edges[2].0), + i(vertices[4], edges[6].0), + i(vertices[4], edges[7].0), + i(vertices[5], edges[8].0), + i(vertices[6], edges[9].0), + ], + vec![i(vertices[1], edges[3].0), i(vertices[6], edges[10].0)], + vec![ + i(vertices[1], edges[4].0), + i(vertices[2], edges[6].0), + i(vertices[2], edges[7].0), + i(vertices[4], edges[11].0), + i(vertices[4], edges[11].0), + i(vertices[7], edges[12].0), + i(vertices[8], edges[13].0), + ], + vec![i(vertices[2], edges[8].0), i(vertices[9], edges[14].0)], + vec![ + i(vertices[2], edges[9].0), + i(vertices[3], edges[10].0), + i(vertices[9], edges[15].0), + ], + vec![ + i(vertices[4], edges[12].0), + i(vertices[8], edges[16].0), + i(vertices[9], edges[17].0), + ], + vec![i(vertices[4], edges[13].0), i(vertices[7], edges[16].0)], + vec![ + i(vertices[5], edges[14].0), + i(vertices[6], edges[15].0), + i(vertices[7], edges[17].0), + ], + ]; + (graph, vertices, edges, incidences) + } + + fn empty() -> Self { + Self::default() + } + + fn single_vertex() -> (Self, Self::Vertex) { + let mut graph = Self::default(); + let v = graph.add_vertex(); + (graph, v) + } + + fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge) { + let mut graph = Self::default(); + let vertices = core::array::from_fn(|_| graph.add_vertex()); + let e = graph.add_edge(vertices[0], vertices[1]); + (graph, vertices, e) + } + + fn disconnected() -> (Self, [Self::Vertex; 3]) { + let mut graph = Self::default(); + let vertices = core::array::from_fn(|_| graph.add_vertex()); + graph.add_edge(vertices[1], vertices[2]); + (graph, vertices) + } + + fn loop_edge() -> (Self, Self::Vertex, Self::Edge) { + let mut graph = Self::default(); + let v = graph.add_vertex(); + let e = graph.add_edge(v, v); + (graph, v, e) + } + + fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]) { + let mut graph = Self::default(); + let vertices = core::array::from_fn(|_| graph.add_vertex()); + let edges = core::array::from_fn(|_| graph.add_edge(vertices[0], vertices[1])); + (graph, vertices, edges) + } + + fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]) { + let mut graph = Self::default(); + let vertices = core::array::from_fn(|_| graph.add_vertex()); + let edges = core::array::from_fn(|i| graph.add_edge(vertices[i], vertices[i + 1])); + (graph, vertices, edges) + } + + fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]) { + let mut graph = Self::default(); + let vertices = core::array::from_fn(|_| graph.add_vertex()); + // Path edges. + let e0 = graph.add_edge(vertices[0], vertices[1]); + let e1 = graph.add_edge(vertices[1], vertices[2]); + // Loops. + let e2 = graph.add_edge(vertices[0], vertices[0]); + let e3 = graph.add_edge(vertices[1], vertices[1]); + (graph, vertices, [e0, e1, e2, e3]) + } +} diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 07e9793..dd414b7 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -1,111 +1,3 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! graph_topology_test_fixtures { - ($T:ty) => { - fn make_test_graph() -> ( - $T, - [<$T as $crate::traits::GraphTopology>::Vertex; 10], - [( - <$T as $crate::traits::GraphTopology>::Edge, - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Vertex, - ); 18], - [Vec<$crate::traits::Incidence< - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Edge, - >>; 10], - ) { - use $crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; - let mut graph = <$T>::new(); - let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 10] = - core::array::from_fn(|_| graph.add_vertex()); - let edges = [ - (vertices[0], vertices[1]), - (vertices[0], vertices[1]), - (vertices[1], vertices[2]), - (vertices[1], vertices[3]), - (vertices[1], vertices[4]), - (vertices[2], vertices[2]), - (vertices[2], vertices[4]), - (vertices[2], vertices[4]), - (vertices[2], vertices[5]), - (vertices[2], vertices[6]), - (vertices[3], vertices[6]), - (vertices[4], vertices[4]), - (vertices[4], vertices[7]), - (vertices[4], vertices[8]), - (vertices[5], vertices[9]), - (vertices[6], vertices[9]), - (vertices[7], vertices[8]), - (vertices[7], vertices[9]), - ] - .map(|(v1, v2)| (graph.add_edge(v1, v2), v1, v2)); - let i = |vertex, edge| Incidence { vertex, edge }; - let incidences: [Vec::Vertex, - <$T as GraphTopology>::Edge, - >>; 10] = [ - vec![i(vertices[1], edges[0].0), i(vertices[1], edges[1].0)], - vec![ - i(vertices[0], edges[0].0), - i(vertices[0], edges[1].0), - i(vertices[2], edges[2].0), - i(vertices[3], edges[3].0), - i(vertices[4], edges[4].0), - ], - vec![ - i(vertices[2], edges[5].0), - i(vertices[2], edges[5].0), - i(vertices[1], edges[2].0), - i(vertices[4], edges[6].0), - i(vertices[4], edges[7].0), - i(vertices[5], edges[8].0), - i(vertices[6], edges[9].0), - ], - vec![i(vertices[1], edges[3].0), i(vertices[6], edges[10].0)], - vec![ - i(vertices[1], edges[4].0), - i(vertices[2], edges[6].0), - i(vertices[2], edges[7].0), - i(vertices[4], edges[11].0), - i(vertices[4], edges[11].0), - i(vertices[7], edges[12].0), - i(vertices[8], edges[13].0), - ], - vec![i(vertices[2], edges[8].0), i(vertices[9], edges[14].0)], - vec![ - i(vertices[2], edges[9].0), - i(vertices[3], edges[10].0), - i(vertices[9], edges[15].0), - ], - vec![ - i(vertices[4], edges[12].0), - i(vertices[8], edges[16].0), - i(vertices[9], edges[17].0), - ], - vec![i(vertices[4], edges[13].0), i(vertices[7], edges[16].0)], - vec![ - i(vertices[5], edges[14].0), - i(vertices[6], edges[15].0), - i(vertices[7], edges[17].0), - ], - ]; - (graph, vertices, edges, incidences) - } - - #[allow(dead_code)] - fn make_test_graph_disconnected() - -> ($T, [<$T as $crate::traits::GraphTopology>::Vertex; 3]) { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); - let vertices: [<$T as $crate::traits::GraphTopology>::Vertex; 3] = - core::array::from_fn(|_| graph.add_vertex()); - graph.add_edge(vertices[1], vertices[2]); - (graph, vertices) - } - }; -} - #[doc(hidden)] #[macro_export] macro_rules! graph_topology_tests { @@ -113,71 +5,62 @@ macro_rules! graph_topology_tests { #[test] fn vertex_count_empty() { use $crate::traits::GraphTopology; - let graph = <$T>::new(); + let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); assert_eq!(graph.vertex_count(), 0, "unexpected vertex count"); } #[test] fn vertex_count() { use $crate::traits::GraphTopology; - let (graph, _, _, _) = make_test_graph(); + let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); } #[test] fn vertex_map() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let mut map = graph.vertex_map(27); - assert_eq!(map[v1], 27, "unexpected value from default map read"); - map[v1] = 9; - assert_eq!(map[v1], 9, "unexpected value from map after write"); - let v2 = graph.add_vertex(); - assert_eq!(map[v2], 27, "unexpected value from default map read for new vertex"); + assert_eq!(map[v], 27, "unexpected value from default map read"); + map[v] = 9; + assert_eq!(map[v], 9, "unexpected value from map after write"); } #[test] fn edge_count_empty() { use $crate::traits::GraphTopology; - let graph = <$T>::new(); + let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); assert_eq!(graph.edge_count(), 0, "unexpected edge count"); } #[test] fn edge_count() { use $crate::traits::GraphTopology; - let (graph, _, _, _) = make_test_graph(); + let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!(graph.edge_count(), 18, "unexpected edge count"); } #[test] fn edge_map() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let mut map = graph.edge_map(27); - assert_eq!(map[e1], 27, "unexpected value from default map read"); - map[e1] = 9; - assert_eq!(map[e1], 9, "unexpected value from map after write"); - let e2 = graph.add_edge(v1, v2); - assert_eq!(map[e2], 27, "unexpected value from default map read for new vertex"); + let (graph, _, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); + let mut map = graph.edge_map(28); + assert_eq!(map[e], 28, "unexpected value from default map read"); + map[e] = 8; + assert_eq!(map[e], 8, "unexpected value from map after write"); } #[test] fn degree_zero() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!(graph.degree(v), 0, "unexpected non-zero degree"); } #[test] fn degree() { use $crate::traits::GraphTopology; - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); let expected_degrees = [2, 5, 7, 2, 7, 2, 3, 3, 2, 3]; for i in 0..graph.vertex_count() { assert_eq!( @@ -192,9 +75,7 @@ macro_rules! graph_topology_tests { #[test] fn loop_edge() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - graph.add_edge(v, v); + let (graph, v, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); assert_eq!(graph.vertex_count(), 1, "unexpected vertex count"); assert_eq!(graph.edge_count(), 1, "unexpected edge count"); assert_eq!(graph.degree(v), 2, "unexpected degree"); @@ -207,48 +88,34 @@ macro_rules! graph_topology_tests { #[test] fn multiple_edges() { use $crate::traits::GraphTopology; - let k = 3; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - for _ in 0..k { - graph.add_edge(v1, v2); - } + const K: usize = 3; + let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); assert_eq!(graph.vertex_count(), 2, "unexpected vertex count"); - assert_eq!(graph.edge_count(), k, "unexpected edge count"); - assert_eq!(graph.degree(v1), k, "unexpected degree of vertex {v1:?}"); - assert_eq!(graph.degree(v2), k, "unexpected degree of vertex {v2:?}"); - assert!(graph.are_adjacent(v1, v2), "should be adjacent"); + assert_eq!(graph.edge_count(), K, "unexpected edge count"); + assert_eq!(graph.degree(vertices[0]), K, "unexpected degree of vertex {:?}", vertices[0]); + assert_eq!(graph.degree(vertices[1]), K, "unexpected degree of vertex {:?}", vertices[1]); + assert!(graph.are_adjacent(vertices[0], vertices[1]), "should be adjacent"); } #[test] fn are_adjacent_vertex_self() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - assert!( - !graph.are_adjacent(v, v), - "should not be adjacent to itself" - ); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); + assert!(!graph.are_adjacent(v, v), "should not be adjacent to itself"); } #[test] fn are_adjacent_single_edge() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - assert!(!graph.are_adjacent(v1, v2), "should not be adjacent"); - assert!(!graph.are_adjacent(v2, v1), "should not be adjacent"); - graph.add_edge(v1, v2); - assert!(graph.are_adjacent(v1, v2), "should be adjacent"); - assert!(graph.are_adjacent(v2, v1), "should be adjacent"); + let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); + assert!(graph.are_adjacent(vertices[0], vertices[1]), "should be adjacent"); + assert!(graph.are_adjacent(vertices[1], vertices[0]), "should be adjacent"); } #[test] fn are_adjacent() { use $crate::traits::GraphTopology; - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert!( graph.are_adjacent(vertices[0], vertices[1]), "expected {:?} and {:?} to be adjacent", @@ -293,7 +160,7 @@ macro_rules! graph_topology_tests { #[test] fn vertices_empty() { use $crate::traits::GraphTopology; - let graph = <$T>::new(); + let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); assert_eq!( graph.vertices().count(), 0, @@ -304,7 +171,7 @@ macro_rules! graph_topology_tests { #[test] fn vertices() { use $crate::traits::GraphTopology; - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!(graph.vertices().count(), 10, "unexpected vertex count"); // Expects each vertex to appear exactly once. @@ -320,8 +187,7 @@ macro_rules! graph_topology_tests { #[test] fn adjacent_vertices_empty() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( graph.adjacent_vertices(v).count(), 0, @@ -332,7 +198,7 @@ macro_rules! graph_topology_tests { #[test] fn adjacent_vertices() { use $crate::traits::GraphTopology; - let (graph, vertices, _, _) = make_test_graph(); + let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); // Checks adjacency of vertex 4. assert_eq!( graph.adjacent_vertices(vertices[4]).count(), @@ -370,35 +236,21 @@ macro_rules! graph_topology_tests { #[test] fn adjacent_vertices_loop_edge() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - graph.add_edge(v, v); + let (graph, v, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); let mut iter = graph.adjacent_vertices(v); assert_eq!(iter.next(), Some(v), "vertex should be adjacent to itself"); - assert_eq!( - iter.next(), - Some(v), - "vertex should be adjacent to itself twice" - ); - assert_eq!( - iter.next(), - None, - "too many adjacent vertices from iterator" - ); + assert_eq!(iter.next(), Some(v), "vertex should be adjacent to itself twice"); + assert_eq!(iter.next(), None, "too many adjacent vertices from iterator"); } #[test] fn adjacent_vertices_multiple_edges() { use $crate::traits::GraphTopology; - let k = 3; - let mut graph = <$T>::new(); - let vertices = [graph.add_vertex(), graph.add_vertex()]; - for _ in 0..k { - graph.add_edge(vertices[0], vertices[1]); - } + const K: usize = 3; + let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); for i in 0..2 { let mut iter = graph.adjacent_vertices(vertices[i]); - for j in 0..k { + for j in 0..K { assert_eq!( iter.next(), Some(vertices[1 - i]), @@ -418,23 +270,18 @@ macro_rules! graph_topology_tests { #[test] fn incident_vertices_single_edge() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - let (u1, u2) = graph.incident_vertices(e); + let (graph, vertices, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); + let (u, v) = graph.incident_vertices(e); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {e:?}" + (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), + "unexpected incident vertices {u:?} and {v:?} for edge {e:?}" ); } #[test] fn incident_vertices_loop_edge() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - let e = graph.add_edge(v, v); + let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); assert_eq!( graph.incident_vertices(e), (v, v), @@ -445,28 +292,26 @@ macro_rules! graph_topology_tests { #[test] fn incident_vertices_multiple_edges() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e1 = graph.add_edge(v1, v2); - let e2 = graph.add_edge(v1, v2); - assert_ne!(e1, e2, "edges should be distinct"); - let (u1, u2) = graph.incident_vertices(e1); + let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::<2>(); + assert_ne!(edges[0], edges[1], "edges should be distinct"); + let (u, v) = graph.incident_vertices(edges[0]); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for first multi-edge {e1:?}" + (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), + "unexpected incident vertices {u:?} and {v:?} for first multi-edge {:?}", + edges[0] ); - let (u1, u2) = graph.incident_vertices(e2); + let (u, v) = graph.incident_vertices(edges[1]); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for second multi-edge {e2:?}" + (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), + "unexpected incident vertices {u:?} and {v:?} for second multi-edge {:?}", + edges[1] ); } #[test] fn incident_vertices() { use $crate::traits::GraphTopology; - let (graph, _, edges, _) = make_test_graph(); + let (graph, _, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for &(e, v1, v2) in edges.iter() { let (u1, u2) = graph.incident_vertices(e); assert!( @@ -479,7 +324,7 @@ macro_rules! graph_topology_tests { #[test] fn edges_empty() { use $crate::traits::GraphTopology; - let graph = <$T>::new(); + let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); assert_eq!( graph.edges().count(), 0, @@ -490,7 +335,7 @@ macro_rules! graph_topology_tests { #[test] fn edges() { use $crate::traits::GraphTopology; - let (graph, _, edges, _) = make_test_graph(); + let (graph, _, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); assert_eq!(graph.edges().count(), 18, "unexpected edge count"); // Expects each edge to appear exactly once. @@ -506,8 +351,7 @@ macro_rules! graph_topology_tests { #[test] fn incident_edges_empty() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( graph.incident_edges(v).count(), 0, @@ -518,7 +362,7 @@ macro_rules! graph_topology_tests { #[test] fn incident_edges() { use $crate::traits::GraphTopology; - let (graph, vertices, _, incidences) = make_test_graph(); + let (graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for i in 0..10 { assert_eq!( graph.incident_edges(vertices[i]).count(), @@ -549,31 +393,20 @@ macro_rules! graph_topology_tests { #[test] fn incident_edges_loop_edge() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - let e = graph.add_edge(v, v); + let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); let mut iter = graph.incident_edges(v); assert_eq!(iter.next(), Some(e), "loop edge should appear in incident edges"); - assert_eq!( - iter.next(), - Some(e), - "loop edge should appear twice in incident edges" - ); + assert_eq!(iter.next(), Some(e), "loop edge should appear twice in incident edges"); assert_eq!(iter.next(), None, "too many incident edges from iterator"); } #[test] fn incident_edges_multiple_edges() { use $crate::traits::GraphTopology; - let k = 3; - let mut graph = <$T>::new(); - let vertices = [graph.add_vertex(), graph.add_vertex()]; - let mut edges = Vec::new(); - for _ in 0..k { - edges.push(graph.add_edge(vertices[0], vertices[1])); - } + const K: usize = 3; + let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); for i in 0..2 { - let mut expected = edges.clone(); + let mut expected = edges.to_vec(); for e in graph.incident_edges(vertices[i]) { let pos = expected .iter() @@ -596,8 +429,7 @@ macro_rules! graph_topology_tests { #[test] fn incidences_empty() { use $crate::traits::GraphTopology; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); assert_eq!( graph.incidences(v).count(), 0, @@ -608,7 +440,7 @@ macro_rules! graph_topology_tests { #[test] fn incidences() { use $crate::traits::GraphTopology; - let (graph, vertices, _, incidences) = make_test_graph(); + let (graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for i in 0..10 { assert_eq!( graph.incidences(vertices[i]).count(), @@ -641,7 +473,7 @@ macro_rules! graph_topology_tests { use $crate::traits::GraphTopology; // For each incidence (v, e) of u, the same edge e must appear in the incidences of v. // For loop edges (u == v), the edge must appear exactly twice in the incidences of u. - let (graph, _, _, _) = make_test_graph(); + let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for u in graph.vertices() { for incidence in graph.incidences(u) { if u == incidence.vertex { @@ -667,9 +499,7 @@ macro_rules! graph_topology_tests { #[test] fn incidences_loop_edge() { use $crate::traits::{GraphTopology, Incidence}; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - let e = graph.add_edge(v, v); + let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); let mut iter = graph.incidences(v); assert_eq!( iter.next(), @@ -681,28 +511,19 @@ macro_rules! graph_topology_tests { Some(Incidence { vertex: v, edge: e }), "vertex should be adjacent to itself twice" ); - assert_eq!( - iter.next(), - None, - "too many adjacent vertices from iterator" - ); + assert_eq!(iter.next(), None, "too many adjacent vertices from iterator"); } #[test] fn incidences_multiple_edges() { use $crate::traits::GraphTopology; - let k = 3; - let mut graph = <$T>::new(); - let vertices = [graph.add_vertex(), graph.add_vertex()]; - let mut edges = Vec::new(); - for _ in 0..k { - edges.push(graph.add_edge(vertices[0], vertices[1])); - } + const K: usize = 3; + let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); for i in 0..2 { let mut iter = graph.incidences(vertices[i]); - for j in 0..k { + for j in 0..K { let current = iter.next().expect(&format!( - "incidence {j} missing, expected {k} incidences for vertex {:?}", + "incidence {j} missing, expected {K} incidences for vertex {:?}", vertices[i] )); assert_eq!( @@ -731,8 +552,7 @@ macro_rules! graph_topology_tests { #[test] fn incidence_cursor_empty() { use $crate::traits::{GraphTopology, IncidenceCursor}; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); + let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); let mut cursor = graph.incidence_cursor(v); assert_eq!( cursor.next(&graph), @@ -744,7 +564,7 @@ macro_rules! graph_topology_tests { #[test] fn incidence_cursor() { use $crate::traits::{GraphTopology, IncidenceCursor}; - let (graph, vertices, _, incidences) = make_test_graph(); + let (graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for i in 0..10 { let mut expected = incidences[i].clone(); let mut cursor = graph.incidence_cursor(vertices[i]); @@ -770,9 +590,7 @@ macro_rules! graph_topology_tests { #[test] fn incidence_cursor_loop_edge() { use $crate::traits::{GraphTopology, Incidence, IncidenceCursor}; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - let e = graph.add_edge(v, v); + let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); let mut cursor = graph.incidence_cursor(v); assert_eq!( cursor.next(&graph), @@ -784,28 +602,19 @@ macro_rules! graph_topology_tests { Some(Incidence { vertex: v, edge: e }), "vertex should be adjacent to itself twice" ); - assert_eq!( - cursor.next(&graph), - None, - "too many incidences from cursor" - ); + assert_eq!(cursor.next(&graph), None, "too many incidences from cursor"); } #[test] fn incidence_cursor_multiple_edges() { use $crate::traits::{GraphTopology, IncidenceCursor}; - let k = 3; - let mut graph = <$T>::new(); - let vertices = [graph.add_vertex(), graph.add_vertex()]; - let mut edges = Vec::new(); - for _ in 0..k { - edges.push(graph.add_edge(vertices[0], vertices[1])); - } + const K: usize = 3; + let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); for i in 0..2 { let mut cursor = graph.incidence_cursor(vertices[i]); - for j in 0..k { + for j in 0..K { let current = cursor.next(&graph).expect(&format!( - "incidence {j} missing, expected {k} incidences for vertex {:?}", + "incidence {j} missing, expected {K} incidences for vertex {:?}", vertices[i] )); assert_eq!( @@ -834,13 +643,9 @@ macro_rules! graph_topology_tests { #[test] fn incidence_cursor_copy() { use $crate::traits::{GraphTopology, IncidenceCursor}; - let mut graph = <$T>::new(); - let v = graph.add_vertex(); - for _ in 0..2 { - let u = graph.add_vertex(); - graph.add_edge(u, v); - } - let mut c1 = graph.incidence_cursor(v); + let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::two_edge_path(); + // Traverses incidences of the middle vertex. + let mut c1 = graph.incidence_cursor(vertices[1]); assert!(c1.next(&graph).is_some(), "expected first incidence"); // Copies cursor mid-traversal. let mut c2 = c1; @@ -855,7 +660,7 @@ macro_rules! graph_topology_tests { #[test] fn incident_vertices_incidences_consistency() { use $crate::traits::GraphTopology; - let (graph, _, _, _) = make_test_graph(); + let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for u in graph.vertices() { for incidence in graph.incidences(u) { let (w1, w2) = graph.incident_vertices(incidence.edge); @@ -871,7 +676,7 @@ macro_rules! graph_topology_tests { #[test] fn incident_edges_incidences_consistency() { use $crate::traits::GraphTopology; - let (graph, _, _, _) = make_test_graph(); + let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); for u in graph.vertices() { let mut expected: Vec<_> = graph.incidences(u).map(|x| x.edge).collect(); for e in graph.incident_edges(u) { @@ -900,7 +705,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn reserve_vertices_increases_capacity() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let capacity_before = graph.vertex_capacity(); graph.reserve_vertices(capacity_before + 10); assert!( @@ -912,7 +717,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn reserve_vertices_prevents_reallocation_on_add() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); graph.reserve_vertices(10); let capacity_before = graph.vertex_capacity(); for _ in 0..10 { @@ -928,7 +733,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn reserve_vertices_does_not_affect_edge_capacity() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let capacity_before = graph.edge_capacity(); graph.reserve_vertices(10); assert_eq!( @@ -941,7 +746,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn reserve_edges_increases_capacity() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let capacity_before = graph.edge_capacity(); graph.reserve_edges(capacity_before + 10); assert!( @@ -953,7 +758,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn reserve_edges_prevents_reallocation_on_add() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); graph.reserve_edges(10); @@ -971,7 +776,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn reserve_edges_does_not_affect_vertex_capacity() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let capacity_before = graph.vertex_capacity(); graph.reserve_edges(10); assert_eq!( @@ -984,7 +789,7 @@ macro_rules! graph_topology_addition_tests { #[test] fn add_vertex() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let v = graph.add_vertex(); assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); } @@ -992,12 +797,45 @@ macro_rules! graph_topology_addition_tests { #[test] fn add_edge() { use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::new(); + let mut graph = <$T>::default(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); let e = graph.add_edge(v1, v2); assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); } + + #[test] + fn vertex_map_new_vertex() { + use $crate::traits::{GraphTopology, GraphTopologyAddition}; + let mut graph = <$T>::default(); + let mut map = graph.vertex_map(27); + let v = graph.add_vertex(); + assert_eq!( + map[v], 27, + "unexpected value from default map read for new vertex" + ); + map[v] = 9; + assert_eq!( + map[v], 9, + "unexpected value from map read after write for new vertex" + ); + } + + #[test] + fn edge_map_new_edge() { + use $crate::traits::{GraphTopology, GraphTopologyAddition}; + let mut graph = <$T>::default(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + let mut map = graph.edge_map(28); + let e = graph.add_edge(v1, v2); + assert_eq!(map[e], 28,); + map[e] = 8; + assert_eq!( + map[e], 8, + "unexpected value from map read after write for new edge" + ); + } }; } @@ -1007,40 +845,18 @@ macro_rules! graph_topology_deletion_tests { ($T:ty) => { #[test] fn delete_vertex() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v = graph.add_vertex(); - assert_eq!( - graph.vertex_count(), - 1, - "unexpected vertex count before delete" - ); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); + assert_eq!(graph.vertex_count(), 1, "unexpected vertex count before delete"); graph.delete_vertex(v); - assert_eq!( - graph.vertex_count(), - 0, - "unexpected vertex count after delete" - ); - assert_ne!( - graph.add_vertex(), - v, - "unexpected duplicate vertex after delete" - ); + assert_eq!(graph.vertex_count(), 0, "unexpected vertex count after delete"); } #[test] fn delete_vertex_loop() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v = graph.add_vertex(); - graph.add_edge(v, v); - assert_eq!( - graph.vertex_count(), - 1, - "unexpected vertex count before delete" - ); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, v, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); + assert_eq!(graph.vertex_count(), 1, "unexpected vertex count before delete"); assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); assert!( graph.are_adjacent(v, v), @@ -1048,25 +864,14 @@ macro_rules! graph_topology_deletion_tests { ); assert_eq!(graph.degree(v), 2, "unexpected vertex degree before delete"); graph.delete_vertex(v); - assert_eq!( - graph.vertex_count(), - 0, - "unexpected vertex count after delete" - ); + assert_eq!(graph.vertex_count(), 0, "unexpected vertex count after delete"); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); - assert_ne!( - graph.add_vertex(), - v, - "unexpected duplicate vertex after delete" - ); } #[test] fn delete_vertex_invalid_index() { - use $crate::traits::GraphTopologyAddition; use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v = graph.add_vertex(); + let (mut graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); graph.delete_vertex(v); let result = std::panic::catch_unwind(move || graph.delete_vertex(v)); assert!(result.is_err(), "second deletion should panic"); @@ -1074,25 +879,12 @@ macro_rules! graph_topology_deletion_tests { #[test] fn delete_vertex_connected() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, _) = make_test_graph(); - assert_eq!( - graph.vertex_count(), - 10, - "unexpected vertex count before delete" - ); - assert_eq!( - graph.edge_count(), - 18, - "unexpected edge count before delete" - ); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); + assert_eq!(graph.vertex_count(), 10, "unexpected vertex count before delete"); + assert_eq!(graph.edge_count(), 18, "unexpected edge count before delete"); graph.delete_vertex(vertices[2]); - assert_eq!( - graph.vertex_count(), - 9, - "unexpected vertex count after delete" - ); + assert_eq!(graph.vertex_count(), 9, "unexpected vertex count after delete"); assert_eq!(graph.edge_count(), 12, "unexpected edge count after delete"); let expected_edges = [ (vertices[0], vertices[1]), @@ -1124,146 +916,70 @@ macro_rules! graph_topology_deletion_tests { #[test] fn delete_edge() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - assert_eq!( - graph.vertex_count(), - 2, - "unexpected vertex count before delete" - ); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, vertices, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); + assert_eq!(graph.vertex_count(), 2, "unexpected vertex count before delete"); assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); assert!( - graph.are_adjacent(v1, v2), + graph.are_adjacent(vertices[0], vertices[1]), "expected vertices to be adjacent before delete" ); - assert_eq!( - graph.degree(v1), - 1, - "unexpected vertex degree before delete" - ); - assert_eq!( - graph.degree(v2), - 1, - "unexpected vertex degree before delete" - ); + assert_eq!(graph.degree(vertices[0]), 1, "unexpected vertex degree before delete"); + assert_eq!(graph.degree(vertices[1]), 1, "unexpected vertex degree before delete"); graph.delete_edge(e); - assert_eq!( - graph.vertex_count(), - 2, - "unexpected vertex count after delete" - ); + assert_eq!(graph.vertex_count(), 2, "unexpected vertex count after delete"); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); assert!( - !graph.are_adjacent(v1, v2), + !graph.are_adjacent(vertices[0], vertices[1]), "unexpected adjacency after delete" ); - assert_eq!(graph.degree(v1), 0, "unexpected vertex degree after delete"); - assert_eq!(graph.degree(v2), 0, "unexpected vertex degree after delete"); - assert_ne!( - graph.add_edge(v1, v2), - e, - "unexpected duplicate edge after delete" - ); + assert_eq!(graph.degree(vertices[0]), 0, "unexpected vertex degree after delete"); + assert_eq!(graph.degree(vertices[1]), 0, "unexpected vertex degree after delete"); } #[test] fn delete_edge_loop() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v = graph.add_vertex(); - let e = graph.add_edge(v, v); - assert_eq!( - graph.vertex_count(), - 1, - "unexpected vertex count before delete" - ); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); + assert_eq!(graph.vertex_count(), 1, "unexpected vertex count before delete"); assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); - assert!( - graph.are_adjacent(v, v), - "expected vertex to be self-adjacent before delete" - ); + assert!(graph.are_adjacent(v, v), "expected vertex to be self-adjacent before delete"); assert_eq!(graph.degree(v), 2, "unexpected vertex degree before delete"); graph.delete_edge(e); - assert_eq!( - graph.vertex_count(), - 1, - "unexpected vertex count after delete" - ); + assert_eq!(graph.vertex_count(), 1, "unexpected vertex count after delete"); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); - assert!( - !graph.are_adjacent(v, v), - "unexpected adjacency after delete" - ); + assert!(!graph.are_adjacent(v, v), "unexpected adjacency after delete"); assert_eq!(graph.degree(v), 0, "unexpected vertex degree after delete"); - assert_ne!( - graph.add_edge(v, v), - e, - "unexpected duplicate edge after delete" - ); } #[test] fn delete_edge_multiple() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - graph.add_edge(v1, v2); - assert_eq!( - graph.vertex_count(), - 2, - "unexpected vertex count before delete" - ); - assert_eq!(graph.edge_count(), 2, "unexpected edge count before delete"); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + const K: usize = 2; + let (mut graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); + assert_eq!(graph.vertex_count(), 2, "unexpected vertex count before delete"); + assert_eq!(graph.edge_count(), K, "unexpected edge count before delete"); assert!( - graph.are_adjacent(v1, v2), + graph.are_adjacent(vertices[0], vertices[1]), "expected vertices to be adjacent before delete" ); - assert_eq!( - graph.degree(v1), - 2, - "unexpected vertex degree before delete" - ); - assert_eq!( - graph.degree(v2), - 2, - "unexpected vertex degree before delete" - ); - graph.delete_edge(e); - assert_eq!( - graph.vertex_count(), - 2, - "unexpected vertex count after delete" - ); - assert_eq!(graph.edge_count(), 1, "unexpected edge count after delete"); + assert_eq!(graph.degree(vertices[0]), K, "unexpected vertex degree before delete"); + assert_eq!(graph.degree(vertices[1]), K, "unexpected vertex degree before delete"); + graph.delete_edge(edges[0]); + assert_eq!(graph.vertex_count(), 2, "unexpected vertex count after delete"); + assert_eq!(graph.edge_count(), K - 1, "unexpected edge count after delete"); assert!( - graph.are_adjacent(v1, v2), + graph.are_adjacent(vertices[0], vertices[1]), "expected vertices to be adjacent after delete" ); - assert_eq!(graph.degree(v1), 1, "unexpected vertex degree after delete"); - assert_eq!(graph.degree(v2), 1, "unexpected vertex degree after delete"); - assert_ne!( - graph.add_edge(v1, v2), - e, - "unexpected duplicate edge after delete" - ); + assert_eq!(graph.degree(vertices[0]), K - 1, "unexpected vertex degree after delete"); + assert_eq!(graph.degree(vertices[1]), K - 1, "unexpected vertex degree after delete"); } #[test] fn delete_edge_invalid_index() { - use $crate::traits::GraphTopologyAddition; use $crate::traits::GraphTopologyDeletion; - let mut graph = Graph::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); + let (mut graph, _, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); graph.delete_edge(e); let result = std::panic::catch_unwind(move || graph.delete_edge(e)); assert!(result.is_err(), "second deletion should panic"); @@ -1271,15 +987,10 @@ macro_rules! graph_topology_deletion_tests { #[test] fn vertices_after_delete() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, _) = make_test_graph(); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); graph.delete_vertex(vertices[2]); - assert_eq!( - graph.vertex_count(), - 9, - "unexpected vertex count after delete" - ); + assert_eq!(graph.vertex_count(), 9, "unexpected vertex count after delete"); assert_eq!( graph.vertices().count(), 9, @@ -1303,9 +1014,8 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incident_vertices_after_delete_edge() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let (mut graph, _, edges, _) = make_test_graph(); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, _, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); let e = edges[2].0; graph.delete_edge(e); for &(f, v1, v2) in edges.iter().filter(|&&(f, _, _)| f != e) { @@ -1319,16 +1029,11 @@ macro_rules! graph_topology_deletion_tests { #[test] fn edges_after_delete() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, edges, _) = make_test_graph(); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, vertices, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); graph.delete_vertex(vertices[2]); assert_eq!(graph.edge_count(), 12, "unexpected edge count after delete"); - assert_eq!( - graph.edges().count(), - 12, - "unexpected edge iterator count after delete" - ); + assert_eq!(graph.edges().count(), 12, "unexpected edge iterator count after delete"); for i in [2, 5, 6, 7, 8, 9] { assert!( !graph.edges().any(|e| e == edges[i].0), @@ -1347,9 +1052,8 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incident_edges_after_delete_edge() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, edges, incidences) = make_test_graph(); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, vertices, edges, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); graph.delete_edge(edges[2].0); for i in 0..10 { let mut expected: Vec<_> = incidences[i] @@ -1380,9 +1084,8 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incident_edges_after_delete_vertex() { - use $crate::traits::GraphTopology; - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, incidences) = make_test_graph(); + use $crate::traits::{GraphTopology, GraphTopologyDeletion}; + let (mut graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); graph.delete_vertex(vertices[2]); for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { let mut expected: Vec<_> = incidences[i] @@ -1414,7 +1117,7 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incidences_after_delete_vertex() { use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, incidences) = make_test_graph(); + let (mut graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); graph.delete_vertex(vertices[2]); for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { let remaining = incidences[i] @@ -1429,7 +1132,7 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incidences_after_delete_edge() { use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, edges, incidences) = make_test_graph(); + let (mut graph, vertices, edges, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); // Deletes the edge from vertices[1] to vertices[2]. graph.delete_edge(edges[2].0); for i in 0..10 { @@ -1442,6 +1145,7 @@ macro_rules! graph_topology_deletion_tests { } } + // TODO: Move out of macro. fn assert_vertex_incidences( graph: &$T, v: <$T as $crate::traits::GraphTopology>::Vertex, @@ -1478,7 +1182,7 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incidence_cursor_after_delete_vertex() { use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, incidences) = make_test_graph(); + let (mut graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); graph.delete_vertex(vertices[2]); for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { let remaining = incidences[i] @@ -1493,7 +1197,7 @@ macro_rules! graph_topology_deletion_tests { #[test] fn incidence_cursor_after_delete_edge() { use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, edges, incidences) = make_test_graph(); + let (mut graph, vertices, edges, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); // Deletes the edge from vertices[1] to vertices[2]. graph.delete_edge(edges[2].0); for i in 0..10 { @@ -1506,6 +1210,7 @@ macro_rules! graph_topology_deletion_tests { } } + // TODO: Move out of macro. fn assert_vertex_incidence_cursor( graph: &$T, v: <$T as $crate::traits::GraphTopology>::Vertex, @@ -1514,7 +1219,7 @@ macro_rules! graph_topology_deletion_tests { <$T as $crate::traits::GraphTopology>::Edge, >>, ) { - use $crate::traits::IncidenceCursor; + use $crate::traits::{GraphTopology, IncidenceCursor}; let mut cursor = graph.incidence_cursor(v); while let Some(incidence) = cursor.next(graph) { let pos = expected @@ -1535,3 +1240,46 @@ macro_rules! graph_topology_deletion_tests { } }; } + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_deletion_tests { + ($T:ty) => { + #[test] + fn delete_vertex_add_vertex() { + use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; + let (mut graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); + graph.delete_vertex(v); + assert_eq!( + graph.vertex_count(), + 0, + "unexpected vertex count after delete" + ); + assert_ne!( + graph.add_vertex(), + v, + "unexpected duplicate vertex after delete" + ); + assert_eq!( + graph.vertex_count(), + 1, + "unexpected vertex count after re-add" + ); + } + + #[test] + fn delete_edge_add_edge() { + use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; + let (mut graph, vertices, e): ($T, _, _) = + $crate::testing::fixtures::MakeTestGraph::single_edge(); + graph.delete_edge(e); + assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); + assert_ne!( + graph.add_edge(vertices[0], vertices[1]), + e, + "unexpected duplicate edge after re-add" + ); + assert_eq!(graph.edge_count(), 1, "unexpected edge count after re-add"); + } + }; +} diff --git a/tests/bfs.rs b/tests/bfs.rs index 293ea12..babd76a 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -1,13 +1,11 @@ mod append_graph_tests { use grapherity::models::AppendGraph; - grapherity::graph_topology_test_fixtures!(AppendGraph); grapherity::bfs_tests!(AppendGraph); } mod graph_tests { use grapherity::models::Graph; - grapherity::graph_topology_test_fixtures!(Graph); grapherity::bfs_tests!(Graph); } diff --git a/tests/dfs.rs b/tests/dfs.rs index ec7e039..fcbbc3b 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -1,13 +1,11 @@ mod append_graph_tests { use grapherity::models::AppendGraph; - grapherity::graph_topology_test_fixtures!(AppendGraph); grapherity::dfs_tests!(AppendGraph); } mod graph_tests { use grapherity::models::Graph; - grapherity::graph_topology_test_fixtures!(Graph); grapherity::dfs_tests!(Graph); } diff --git a/tests/dijkstra.rs b/tests/dijkstra.rs index b495546..e655000 100644 --- a/tests/dijkstra.rs +++ b/tests/dijkstra.rs @@ -1,13 +1,11 @@ mod append_graph_tests { use grapherity::models::AppendGraph; - grapherity::graph_topology_test_fixtures!(AppendGraph); grapherity::dijkstra_tests!(AppendGraph); } mod graph_tests { use grapherity::models::Graph; - grapherity::graph_topology_test_fixtures!(Graph); grapherity::dijkstra_tests!(Graph); } -- 2.43.0 From 95601a7a5a741c6af8822046573bf79af5cf46dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 29 Aug 2026 22:39:45 +0200 Subject: [PATCH 29/67] Add todos --- src/models/append_graph.rs | 2 ++ src/models/graph.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 84730c3..47f26d5 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -149,6 +149,7 @@ impl GraphTopology for AppendGraph { self.vertices.len() } + // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn vertex_map(&self, default: T) -> ElementMap { ElementMap::new(default, |v| v.0, self.vertex_capacity()) } @@ -157,6 +158,7 @@ impl GraphTopology for AppendGraph { self.incidences.len() / 2 } + // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn edge_map(&self, default: T) -> ElementMap { ElementMap::new(default, |e| e.0 / 2, self.edge_capacity()) } diff --git a/src/models/graph.rs b/src/models/graph.rs index 5eda23a..11a8ff9 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -241,6 +241,7 @@ impl GraphTopology for Graph { self.vertices.len() } + // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn vertex_map(&self, default: T) -> ElementMap { ElementMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) } @@ -249,6 +250,7 @@ impl GraphTopology for Graph { self.incidences.len() / 2 } + // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn edge_map(&self, default: T) -> ElementMap { ElementMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) } -- 2.43.0 From 6d3a206995c39f8d4d21c45de5ad2afe4a6f56ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 31 Aug 2026 19:44:05 +0200 Subject: [PATCH 30/67] Hide mod testing from documentation --- src/lib.rs | 1 + src/testing.rs | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c2515f4..d2ea1fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,4 +149,5 @@ pub mod prelude { }; } +#[doc(hidden)] pub mod testing; diff --git a/src/testing.rs b/src/testing.rs index b92fa0d..227af35 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -3,7 +3,5 @@ pub(crate) mod bfs_testing; pub(crate) mod dfs_testing; pub(crate) mod dijkstra_testing; -pub(crate) mod graph_topology_testing; - -#[doc(hidden)] pub mod fixtures; +pub(crate) mod graph_topology_testing; -- 2.43.0 From df5170f2f508f9de05491ee37119a76f217bfe5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 31 Aug 2026 19:44:55 +0200 Subject: [PATCH 31/67] Refactor algorithm tests to minimize macro code, and move into tests/ The tests only declare a list of tests in the macro, no longer the test functions themselves. This allows better tooling support (e.g. IDE and rustfmt). --- src/testing.rs | 3 - src/testing/bfs_testing.rs | 261 --------------------- src/testing/dfs_testing.rs | 308 ------------------------- src/testing/dijkstra_testing.rs | 331 -------------------------- tests/bfs.rs | 304 +++++++++++++++++++++++- tests/dfs.rs | 395 +++++++++++++++++++++++++++++++- tests/dijkstra.rs | 345 +++++++++++++++++++++++++++- 7 files changed, 1032 insertions(+), 915 deletions(-) delete mode 100644 src/testing/bfs_testing.rs delete mode 100644 src/testing/dfs_testing.rs delete mode 100644 src/testing/dijkstra_testing.rs diff --git a/src/testing.rs b/src/testing.rs index 227af35..64c099c 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,7 +1,4 @@ //! Test fixture and test macros for graph topology and algorithm implementations. -pub(crate) mod bfs_testing; -pub(crate) mod dfs_testing; -pub(crate) mod dijkstra_testing; pub mod fixtures; pub(crate) mod graph_topology_testing; diff --git a/src/testing/bfs_testing.rs b/src/testing/bfs_testing.rs deleted file mode 100644 index 2d5bf23..0000000 --- a/src/testing/bfs_testing.rs +++ /dev/null @@ -1,261 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! bfs_tests { - ($T:ty) => { - #[test] - fn bfs_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let result = $crate::algorithms::bfs(&graph, v); - assert_eq!( - result.distances[v], - Some(0), - "unexpected distance of source vertex" - ); - assert_eq!( - result.predecessors[v], None, - "unexpected predecessor of source vertex" - ); - } - - #[test] - fn bfs_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - let result = $crate::algorithms::bfs(&graph, vertices[0]); - assert_eq!( - result.distances[vertices[0]], - Some(0), - "unexpected distance of source vertex" - ); - assert_eq!( - result.predecessors[vertices[0]], None, - "unexpected predecessor of source vertex" - ); - for &v in &vertices[1..3] { - assert_eq!( - result.distances[v], None, - "disconnected vertex {v:?} should have no distance" - ); - assert_eq!( - result.predecessors[v], None, - "disconnected vertex {v:?} should have no predecessor" - ); - } - } - - #[test] - fn bfs() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - let result = $crate::algorithms::bfs(&graph, vertices[0]); - assert_bfs_distances(&result.distances, &vertices); - assert_bfs_predecessors(&result.predecessors, &vertices); - } - - #[test] - fn bfs_distances_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let distances = $crate::algorithms::bfs_distances(&graph, v); - assert_eq!( - distances[v], - Some(0), - "unexpected distance of source vertex" - ); - } - - #[test] - fn bfs_distances_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - let distances = $crate::algorithms::bfs_distances(&graph, vertices[0]); - assert_eq!( - distances[vertices[0]], - Some(0), - "unexpected distance of source vertex" - ); - for &v in &vertices[1..3] { - assert_eq!( - distances[v], None, - "disconnected vertex {v:?} should have no distance" - ); - } - } - - #[test] - fn bfs_distances() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - let distances = $crate::algorithms::bfs_distances(&graph, vertices[0]); - assert_bfs_distances(&distances, &vertices); - } - - #[test] - fn bfs_find_source() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - $crate::algorithms::bfs_find(&graph, v, v), - Some(0), - "source should be found at distance 0" - ); - } - - #[test] - fn bfs_find_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - assert_eq!( - $crate::algorithms::bfs_find(&graph, vertices[0], vertices[1]), - None, - "disconnected target should not be found" - ); - } - - #[test] - fn bfs_find() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - let expected_distances = [ - Some(0), - Some(1), - Some(2), - Some(2), - Some(2), - Some(3), - Some(3), - Some(3), - Some(3), - Some(4), - ]; - for i in 0..10 { - assert_eq!( - $crate::algorithms::bfs_find(&graph, vertices[0], vertices[i]), - expected_distances[i], - "unexpected distance from {:?} to {:?}", - vertices[0], - vertices[i] - ); - } - } - - #[test] - fn bfs_find_where_source_matches() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - $crate::algorithms::bfs_find_where(&graph, v, |u| u == v), - Some((v, 0)), - "source should match with distance 0" - ); - } - - #[test] - fn bfs_find_where_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - assert_eq!( - $crate::algorithms::bfs_find_where(&graph, vertices[0], |v| v == vertices[1]), - None, - "disconnected vertex {:?} should not be found", - vertices[1] - ); - } - - #[test] - fn bfs_find_where_no_match() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!( - $crate::algorithms::bfs_find_where(&graph, vertices[0], |_| false), - None, - "no vertex should match an always-false predicate" - ); - } - - #[test] - fn bfs_find_where_nearest() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - // vertices[5], vertices[6], vertices[7], vertices[8] are all at distance 3 from vertices[0]. - // vertices[9] is at distance 4. Predicate matches vertices[7], vertices[8], vertices[9]. - // BFS must return one of the distance-3 ones, not vertices[9]. - let result = $crate::algorithms::bfs_find_where(&graph, vertices[0], |v| { - v == vertices[7] || v == vertices[8] || v == vertices[9] - }); - assert!(result.is_some(), "expected a match"); - let (found, distance) = result.unwrap(); - assert_eq!( - distance, 3, - "unexpected distance to nearest matching vertex {found:?}", - ); - assert!( - found == vertices[7] || found == vertices[8], - "unexpected nearest match vertex {found:?}, should be {:?} or {:?}", - vertices[7], - vertices[8] - ); - } - - // TODO: Move out of macro. - fn assert_bfs_distances( - distances: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - let expected = [ - Some(0), - Some(1), - Some(2), - Some(2), - Some(2), - Some(3), - Some(3), - Some(3), - Some(3), - Some(4), - ]; - for i in 0..10 { - assert_eq!( - distances[vertices[i]], expected[i], - "unexpected BFS distance from {:?} to {:?}", - vertices[0], vertices[i] - ); - } - } - - // TODO: Move out of macro. - fn assert_bfs_predecessors( - predecessors: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option<<$T as $crate::traits::GraphTopology>::Vertex>, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - assert_eq!( - predecessors[vertices[0]], None, - "source should have no predecessor" - ); - // Each non-source vertex's predecessor must be adjacent and at distance one less. - let expected_predecessors = [ - vec![None], - vec![Some(vertices[0])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[2])], - vec![Some(vertices[2]), Some(vertices[3])], - vec![Some(vertices[4])], - vec![Some(vertices[4])], - vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], - ]; - for i in 1..10 { - assert!( - expected_predecessors[i].contains(&predecessors[vertices[i]]), - "unexpected predecessor {:?} of {:?}", - predecessors[vertices[i]], - vertices[i] - ); - } - } - }; -} diff --git a/src/testing/dfs_testing.rs b/src/testing/dfs_testing.rs deleted file mode 100644 index e57ad56..0000000 --- a/src/testing/dfs_testing.rs +++ /dev/null @@ -1,308 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! dfs_tests { - ($T:ty) => { - #[test] - fn dfs_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let result = $crate::algorithms::dfs(&graph, v); - assert!(result.visited[v], "source vertex should be visited"); - assert_eq!( - result.predecessors[v], None, - "unexpected predecessor of source vertex" - ); - } - - #[test] - fn dfs_disconnected() { - let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); - let result = $crate::algorithms::dfs(&graph, vertices[0]); - assert!( - result.visited[vertices[0]], - "source vertex should be visited" - ); - assert_eq!( - result.predecessors[vertices[0]], None, - "unexpected predecessor of source vertex" - ); - for &v in &vertices[1..3] { - assert!( - !result.visited[v], - "disconnected vertex {v:?} should not be visited" - ); - assert_eq!( - result.predecessors[v], None, - "disconnected vertex {v:?} should have no predecessor" - ); - } - } - - #[test] - fn dfs() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - let result = $crate::algorithms::dfs(&graph, vertices[0]); - assert_dfs_visited(&result.visited, &vertices); - assert_dfs_predecessors(&graph, &result.visited, &result.predecessors, &vertices); - } - - #[test] - fn dfs_visited_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let visited = $crate::algorithms::dfs_visited(&graph, v); - assert!(visited[v], "source vertex should be visited"); - } - - #[test] - fn dfs_visited_disconnected() { - let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); - let visited = $crate::algorithms::dfs_visited(&graph, vertices[0]); - assert!(visited[vertices[0]], "source vertex should be visited"); - for &v in &vertices[1..3] { - assert!( - !visited[v], - "disconnected vertex {v:?} should not be visited" - ); - } - } - - #[test] - fn dfs_visited() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - let visited = $crate::algorithms::dfs_visited(&graph, vertices[0]); - assert_dfs_visited(&visited, &vertices); - } - - #[test] - fn dfs_find_source() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert!( - $crate::algorithms::dfs_find(&graph, v, v), - "source should find itself" - ); - } - - #[test] - fn dfs_find_disconnected() { - let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); - assert!( - !$crate::algorithms::dfs_find(&graph, vertices[0], vertices[1]), - "disconnected target should not be found" - ); - } - - #[test] - fn dfs_find() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for i in 0..10 { - assert!( - $crate::algorithms::dfs_find(&graph, vertices[0], vertices[i]), - "vertex {:?} should be reachable from {:?}", - vertices[i], - vertices[0] - ); - } - } - - #[test] - fn dfs_find_where_source_matches() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - $crate::algorithms::dfs_find_where(&graph, v, |u| u == v), - Some(v), - "source should find itself" - ); - } - - #[test] - fn dfs_find_where_disconnected() { - let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); - assert_eq!( - $crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), - None, - "disconnected vertex {:?} should not be found", - vertices[1] - ); - } - - #[test] - fn dfs_find_where_no_match() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!( - $crate::algorithms::dfs_find_where(&graph, vertices[0], |_| false), - None, - "no vertex should match an always-false predicate" - ); - } - - #[test] - fn dfs_find_where_adjacent() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!( - $crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), - Some(vertices[1]), - "expected to find adjacent vertex" - ); - } - - #[test] - fn dfs_find_where() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!( - $crate::algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[9]), - Some(vertices[9]), - "expected to find connected vertex" - ); - } - - // TODO: Move out of macro. - fn assert_dfs_visited( - visited: &$crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - for i in 0..10 { - assert!( - visited[vertices[i]], - "vertex {:?} should be visited", - vertices[i] - ); - } - } - - // TODO: Move out of macro. - fn assert_dfs_predecessors( - graph: &$T, - visited: &$crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Vertex, bool>, - predecessors: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option<<$T as $crate::traits::GraphTopology>::Vertex>, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - use $crate::traits::GraphTopology; - assert_eq!( - predecessors[vertices[0]], None, - "source should have no predecessor" - ); - for i in 1..10 { - let v = vertices[i]; - let p = predecessors[v].expect(&format!("vertex {v:?} should have a predecessor")); - assert!( - visited[p], - "predecessor {p:?} of vertex {v:?} should be visited" - ); - assert!( - graph.are_adjacent(v, p), - "predecessor {p:?} of vertex {v:?} should be adjacent" - ); - } - } - - #[test] - fn dfs_find_path_source_equals_target() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - $crate::algorithms::dfs_find_path(&graph, v, v), - Some(vec![]), - "path from source to itself should be empty" - ); - } - - #[test] - fn dfs_find_path_disconnected() { - let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); - assert_eq!( - $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[1]), - None, - "no path should exist to disconnected vertex" - ); - } - - #[test] - fn dfs_find_path_adjacent() { - let (graph, vertices, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); - let path = $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[1]) - .expect("path should exist between adjacent vertices"); - assert_eq!(path.len(), 1, "unexpected path length between adjacent vertices"); - assert_eq!(path[0], e, "path should use the connecting edge"); - } - - #[test] - fn dfs_find_path() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - let path = $crate::algorithms::dfs_find_path(&graph, vertices[0], vertices[9]) - .expect(&format!( - "path should exist between connected vertices {:?} and {:?}", - vertices[0], vertices[9] - )); - assert_valid_path(&graph, &path, vertices[0], vertices[9]); - } - - #[test] - fn dfs_find_path_where_source_matches() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - $crate::algorithms::dfs_find_path_where(&graph, v, |u| u == v), - Some(vec![]), - "path from source to itself should be empty" - ); - } - - #[test] - fn dfs_find_path_where_disconnected() { - let (graph, vertices): ($T, _) = $crate::testing::fixtures::MakeTestGraph::disconnected(); - assert_eq!( - $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), - None, - "no path should exist to disconnected vertex" - ); - } - - #[test] - fn dfs_find_path_where_no_match() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!( - $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |_| false), - None, - "no path should exist when predicate never matches" - ); - } - - #[test] - fn dfs_find_path_where() { - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - let path = - $crate::algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]) - .expect(&format!( - "path should exist between connected vertices {:?} and {:?}", - vertices[0], vertices[9] - )); - assert_valid_path(&graph, &path, vertices[0], vertices[9]); - } - - // TODO: Move out of macro. - fn assert_valid_path( - graph: &$T, - path: &[<$T as $crate::traits::GraphTopology>::Edge], - source: <$T as $crate::traits::GraphTopology>::Vertex, - target: <$T as $crate::traits::GraphTopology>::Vertex, - ) { - use $crate::traits::GraphTopology; - assert!(!path.is_empty(), "path should be non-empty"); - // Walks the path: tracks current vertex, confirm each edge is incident to it. - let mut current = source; - for (i, &e) in path.iter().enumerate() { - let (v1, v2) = graph.incident_vertices(e); - assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); - assert!( - v1 == current || v2 == current, - "path edge {e:?} (index {i}, from {v1:?} to {v2:?}) is not incident to current path vertex {current:?}" - ); - current = if v1 == current { v2 } else { v1 }; - } - assert_eq!( - current, target, - "path should end at target {target:?}, but ended at {current:?}" - ); - } - }; -} diff --git a/src/testing/dijkstra_testing.rs b/src/testing/dijkstra_testing.rs deleted file mode 100644 index fc3d333..0000000 --- a/src/testing/dijkstra_testing.rs +++ /dev/null @@ -1,331 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! dijkstra_tests { - ($T:ty) => { - #[test] - fn dijkstra_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let result = $crate::algorithms::dijkstra(&graph, v, |_| { - panic!("unexpected call of weight functor") - }); - assert_single_vertex(&result, v); - } - - #[test] - fn dijkstra_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - let result = $crate::algorithms::dijkstra(&graph, vertices[0], |_| { - panic!("unexpected call of weight functor") - }); - assert_single_vertex(&result, vertices[0]); - assert_disconnected(&result, &vertices[1..3]); - } - - #[test] - fn dijkstra_zero_weight_loop() { - let (graph, vertices, _, weights) = make_two_edge_path_with_zero_weight_loops(); - let result = $crate::algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); - assert_single_vertex(&result, vertices[0]); - for i in 1..3 { - assert_eq!( - result.predecessors[vertices[i]], - Some(vertices[i - 1]), - "unexpected predecessor of vertex {:?}", - vertices[i] - ); - assert_eq!( - result.distances[vertices[i]], - Some(i.try_into().unwrap()), - "unexpected distance of vertex {:?}", - vertices[i] - ); - } - } - - #[test] - fn dijkstra() { - let (graph, vertices, edges, _, weights) = make_test_graph_weighted(); - let result = $crate::algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); - assert_test_graph(&result, &vertices); - } - - #[test] - fn dijkstra_distances() { - let (graph, vertices, edges, _, weights) = make_test_graph_weighted(); - let distances = - $crate::algorithms::dijkstra_distances(&graph, vertices[0], |e| weights[e]); - assert_distances_test_graph(&distances, &vertices); - } - - #[test] - fn dijkstra_unweighted_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let result = $crate::algorithms::dijkstra_unweighted(&graph, v); - assert_single_vertex(&result, v) - } - - #[test] - fn dijkstra_unweighted_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - let result = $crate::algorithms::dijkstra_unweighted(&graph, vertices[0]); - assert_single_vertex(&result, vertices[0]); - assert_disconnected(&result, &vertices[1..3]); - } - - #[test] - fn dijkstra_unweighted() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - let result = $crate::algorithms::dijkstra_unweighted(&graph, vertices[0]); - assert_unweighted_test_graph(&result, &vertices); - } - - #[test] - fn dijkstra_distances_unweighted_single_vertex() { - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, v); - assert_distances_single_vertex(&distances, v); - } - - #[test] - fn dijkstra_distances_unweighted_disconnected() { - let (graph, vertices): ($T, _) = - $crate::testing::fixtures::MakeTestGraph::disconnected(); - let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); - assert_distances_single_vertex(&distances, vertices[0]); - assert_distances_disconnected(&distances, &vertices[1..3]); - } - - #[test] - fn dijkstra_distances_unweighted() { - let (graph, vertices, _, _): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - let distances = $crate::algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); - assert_distances_unweighted_test_graph(&distances, &vertices); - } - - // TODO: Move out of macro. - fn assert_single_vertex( - result: &$crate::algorithms::DijkstraResult< - <$T as $crate::traits::GraphTopology>::Vertex, - >, - v: <$T as $crate::traits::GraphTopology>::Vertex, - ) { - assert_distances_single_vertex(&result.distances, v); - assert_eq!( - result.predecessors[v], None, - "unexpected predecessor of source vertex", - ); - } - - // TODO: Move out of macro. - fn assert_distances_single_vertex( - distances: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option, - >, - v: <$T as $crate::traits::GraphTopology>::Vertex, - ) { - assert_eq!( - distances[v], - Some(0), - "unexpected distance of source vertex" - ); - } - - // TODO: Move out of macro. - fn assert_disconnected( - result: &$crate::algorithms::DijkstraResult< - <$T as $crate::traits::GraphTopology>::Vertex, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - assert_distances_disconnected(&result.distances, &vertices); - for &v in vertices { - assert_eq!( - result.predecessors[v], None, - "unexpected predecessor of disconnected vertex {v:?}", - ); - } - } - - // TODO: Move out of macro. - fn assert_distances_disconnected( - distances: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - for &v in vertices { - assert_eq!( - distances[v], None, - "unexpected distance of disconnected vertex {v:?}", - ); - } - } - - // TODO: Move out of macro. - fn assert_test_graph( - result: &$crate::algorithms::DijkstraResult< - <$T as $crate::traits::GraphTopology>::Vertex, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - assert_distances_test_graph(&result.distances, &vertices); - let expected_predecessors_from_v0 = [ - vec![None], - vec![Some(vertices[0])], - vec![Some(vertices[4])], - vec![Some(vertices[1])], - vec![Some(vertices[7])], - vec![Some(vertices[9])], - vec![Some(vertices[3])], - vec![Some(vertices[9])], - vec![Some(vertices[7])], - vec![Some(vertices[6])], - ]; - for i in 0..10 { - assert!( - expected_predecessors_from_v0[i].contains(&result.predecessors[vertices[i]]), - "unexpected predecessor {:?} of {:?}", - result.predecessors[vertices[i]], - vertices[i] - ); - } - } - - // TODO: Move out of macro. - fn assert_distances_test_graph( - distances: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - let expected_distances_from_v0 = [ - Some(0), - Some(1), - Some(65), - Some(4), - Some(58), - Some(43), - Some(14), - Some(46), - Some(145), - Some(29), - ]; - for i in 0..10 { - assert_eq!( - distances[vertices[i]], expected_distances_from_v0[i], - "unexpected distance from {:?} to {:?}", - vertices[0], vertices[i] - ); - } - } - - // TODO: Move out of macro. - fn assert_unweighted_test_graph( - result: &$crate::algorithms::DijkstraResult< - <$T as $crate::traits::GraphTopology>::Vertex, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - assert_distances_unweighted_test_graph(&result.distances, &vertices); - let expected_predecessors_from_v0 = [ - vec![None], - vec![Some(vertices[0])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[2])], - vec![Some(vertices[2]), Some(vertices[3])], - vec![Some(vertices[4])], - vec![Some(vertices[4])], - vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], - ]; - for i in 0..10 { - assert!( - expected_predecessors_from_v0[i].contains(&result.predecessors[vertices[i]]), - "unexpected predecessor {:?} of {:?}", - result.predecessors[vertices[i]], - vertices[i] - ); - } - } - - // TODO: Move out of macro. - fn assert_distances_unweighted_test_graph( - distances: &$crate::maps::ElementMap< - <$T as $crate::traits::GraphTopology>::Vertex, - Option, - >, - vertices: &[<$T as $crate::traits::GraphTopology>::Vertex], - ) { - let expected_distances_from_v0 = [ - Some(0), - Some(1), - Some(2), - Some(2), - Some(2), - Some(3), - Some(3), - Some(3), - Some(3), - Some(4), - ]; - for i in 0..10 { - assert_eq!( - distances[vertices[i]], expected_distances_from_v0[i], - "unexpected distance from {:?} to {:?}", - vertices[0], vertices[i] - ); - } - } - - // TODO: Move out of macro. - fn make_test_graph_weighted() -> ( - $T, - [<$T as $crate::traits::GraphTopology>::Vertex; 10], - [( - <$T as $crate::traits::GraphTopology>::Edge, - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Vertex, - ); 18], - [Vec< - $crate::traits::Incidence< - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Edge, - >, - >; 10], - $crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, - ) { - use $crate::traits::GraphTopology; - let (graph, vertices, edges, incidences): ($T, _, _, _) = - $crate::testing::fixtures::MakeTestGraph::standard(); - let mut weights = graph.edge_map(99); - for i in [1, 3, 7, 10, 12, 14, 15, 17] { - weights[edges[i].0] = i.try_into().unwrap(); - } - (graph, vertices, edges, incidences, weights) - } - - // TODO: Move out of macro. - fn make_two_edge_path_with_zero_weight_loops() -> ( - $T, - [<$T as $crate::traits::GraphTopology>::Vertex; 3], - [<$T as $crate::traits::GraphTopology>::Edge; 4], - $crate::maps::ElementMap<<$T as $crate::traits::GraphTopology>::Edge, u32>, - ) { - use $crate::traits::GraphTopology; - let (graph, vertices, edges): ($T, _, _) = - $crate::testing::fixtures::MakeTestGraph::two_edge_path_with_loops(); - let mut weights = graph.edge_map(1); - weights[edges[2]] = 0; - weights[edges[3]] = 0; - (graph, vertices, edges, weights) - } - }; -} diff --git a/tests/bfs.rs b/tests/bfs.rs index babd76a..913234d 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -1,11 +1,307 @@ -mod append_graph_tests { +use std::fmt::Debug; + +use grapherity::algorithms; +use grapherity::maps::ElementMap; +use grapherity::testing::fixtures::MakeTestGraph; +use grapherity::traits::GraphTopology; + +macro_rules! bfs_tests { + ($T:ty) => { + bfs_tests!(@wrap $T, + bfs_single_vertex, + bfs_disconnected, + bfs, + bfs_distances_single_vertex, + bfs_distances_disconnected, + bfs_distances, + bfs_find_source, + bfs_find_disconnected, + bfs_find, + bfs_find_where_source_matches, + bfs_find_where_disconnected, + bfs_find_where_no_match, + bfs_find_where_nearest, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + super::$name::<$T>(); + } + )* + }; +} + +mod append_graph { use grapherity::models::AppendGraph; - grapherity::bfs_tests!(AppendGraph); + bfs_tests!(AppendGraph); } -mod graph_tests { +mod graph { use grapherity::models::Graph; - grapherity::bfs_tests!(Graph); + bfs_tests!(Graph); +} + +fn bfs_single_vertex() +where + G::Vertex: Debug, +{ + let (graph, v) = G::single_vertex(); + let result = algorithms::bfs(&graph, v); + assert_eq!( + result.distances[v], + Some(0), + "unexpected distance of source vertex" + ); + assert_eq!( + result.predecessors[v], None, + "unexpected predecessor of source vertex" + ); +} + +fn bfs_disconnected() +where + G::Vertex: Debug, +{ + let (graph, vertices) = G::disconnected(); + let result = algorithms::bfs(&graph, vertices[0]); + assert_eq!( + result.distances[vertices[0]], + Some(0), + "unexpected distance of source vertex" + ); + assert_eq!( + result.predecessors[vertices[0]], None, + "unexpected predecessor of source vertex" + ); + for &v in &vertices[1..3] { + assert_eq!( + result.distances[v], None, + "disconnected vertex {v:?} should have no distance" + ); + assert_eq!( + result.predecessors[v], None, + "disconnected vertex {v:?} should have no predecessor" + ); + } +} + +fn bfs() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let result = algorithms::bfs(&graph, vertices[0]); + assert_bfs_distances::(&result.distances, &vertices); + assert_bfs_predecessors::(&result.predecessors, &vertices); +} + +fn bfs_distances_single_vertex() { + let (graph, v) = G::single_vertex(); + let distances = algorithms::bfs_distances(&graph, v); + assert_eq!( + distances[v], + Some(0), + "unexpected distance of source vertex" + ); +} + +fn bfs_distances_disconnected() +where + G::Vertex: Debug, +{ + let (graph, vertices) = G::disconnected(); + let distances = algorithms::bfs_distances(&graph, vertices[0]); + assert_eq!( + distances[vertices[0]], + Some(0), + "unexpected distance of source vertex" + ); + for &v in &vertices[1..3] { + assert_eq!( + distances[v], None, + "disconnected vertex {v:?} should have no distance" + ); + } +} + +fn bfs_distances() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let distances = algorithms::bfs_distances(&graph, vertices[0]); + assert_bfs_distances::(&distances, &vertices); +} + +fn bfs_find_source() { + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::bfs_find(&graph, v, v), + Some(0), + "source should be found at distance 0" + ); +} + +fn bfs_find_disconnected() { + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::bfs_find(&graph, vertices[0], vertices[1]), + None, + "disconnected target should not be found" + ); +} + +fn bfs_find() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let expected_distances = [ + Some(0), + Some(1), + Some(2), + Some(2), + Some(2), + Some(3), + Some(3), + Some(3), + Some(3), + Some(4), + ]; + for i in 0..10 { + assert_eq!( + algorithms::bfs_find(&graph, vertices[0], vertices[i]), + expected_distances[i], + "unexpected distance from {:?} to {:?}", + vertices[0], + vertices[i] + ); + } +} + +fn bfs_find_where_source_matches() +where + G::Vertex: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::bfs_find_where(&graph, v, |u| u == v), + Some((v, 0)), + "source should match with distance 0" + ); +} + +fn bfs_find_where_disconnected() +where + G::Vertex: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::bfs_find_where(&graph, vertices[0], |v| v == vertices[1]), + None, + "disconnected vertex {:?} should not be found", + vertices[1] + ); +} + +fn bfs_find_where_no_match() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::bfs_find_where(&graph, vertices[0], |_| false), + None, + "no vertex should match an always-false predicate" + ); +} + +fn bfs_find_where_nearest() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // vertices[5], vertices[6], vertices[7], vertices[8] are all at distance 3 from vertices[0]. + // vertices[9] is at distance 4. Predicate matches vertices[7], vertices[8], vertices[9]. + // BFS must return one of the distance-3 ones, not vertices[9]. + let result = algorithms::bfs_find_where(&graph, vertices[0], |v| { + v == vertices[7] || v == vertices[8] || v == vertices[9] + }); + assert!(result.is_some(), "expected a match"); + let (found, distance) = result.unwrap(); + assert_eq!( + distance, 3, + "unexpected distance to nearest matching vertex {found:?}", + ); + assert!( + found == vertices[7] || found == vertices[8], + "unexpected nearest match vertex {found:?}, should be {:?} or {:?}", + vertices[7], + vertices[8] + ); +} + +fn assert_bfs_distances( + distances: &ElementMap>, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + let expected = [ + Some(0), + Some(1), + Some(2), + Some(2), + Some(2), + Some(3), + Some(3), + Some(3), + Some(3), + Some(4), + ]; + for i in 0..10 { + assert_eq!( + distances[vertices[i]], expected[i], + "unexpected distance from {:?} to {:?}", + vertices[0], vertices[i] + ); + } +} + +fn assert_bfs_predecessors( + predecessors: &ElementMap>, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + assert_eq!( + predecessors[vertices[0]], None, + "source should have no predecessor" + ); + // Each non-source vertex's predecessor must be adjacent and at distance one less. + let expected_predecessors = [ + vec![None], + vec![Some(vertices[0])], + vec![Some(vertices[1])], + vec![Some(vertices[1])], + vec![Some(vertices[1])], + vec![Some(vertices[2])], + vec![Some(vertices[2]), Some(vertices[3])], + vec![Some(vertices[4])], + vec![Some(vertices[4])], + vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], + ]; + for i in 1..10 { + assert!( + expected_predecessors[i].contains(&predecessors[vertices[i]]), + "unexpected predecessor {:?} of {:?}", + predecessors[vertices[i]], + vertices[i] + ); + } } diff --git a/tests/dfs.rs b/tests/dfs.rs index fcbbc3b..4999cc9 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -1,11 +1,398 @@ -mod append_graph_tests { +use std::fmt::Debug; + +use grapherity::algorithms; +use grapherity::maps::ElementMap; +use grapherity::testing::fixtures::MakeTestGraph; +use grapherity::traits::GraphTopology; + +macro_rules! dfs_tests { + ($T:ty) => { + dfs_tests!(@wrap $T, + dfs_single_vertex, + dfs_disconnected, + dfs, + dfs_visited_single_vertex, + dfs_visited_disconnected, + dfs_visited, + dfs_find_source, + dfs_find_disconnected, + dfs_find, + dfs_find_where_source_matches, + dfs_find_where_disconnected, + dfs_find_where_no_match, + dfs_find_where_adjacent, + dfs_find_where, + dfs_find_path_source_equals_target, + dfs_find_path_disconnected, + dfs_find_path_adjacent, + dfs_find_path, + dfs_find_path_where_source_matches, + dfs_find_path_where_disconnected, + dfs_find_path_where_no_match, + dfs_find_path_where, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + super::$name::<$T>(); + } + )* + }; +} + +mod append_graph { use grapherity::models::AppendGraph; - grapherity::dfs_tests!(AppendGraph); + dfs_tests!(AppendGraph); } -mod graph_tests { +mod graph { use grapherity::models::Graph; - grapherity::dfs_tests!(Graph); + dfs_tests!(Graph); +} + +fn dfs_single_vertex() +where + G::Vertex: Debug, +{ + let (graph, v) = G::single_vertex(); + let result = algorithms::dfs(&graph, v); + assert!(result.visited[v], "source vertex should be visited"); + assert_eq!( + result.predecessors[v], None, + "unexpected predecessor of source vertex" + ); +} + +fn dfs_disconnected() +where + G::Vertex: Debug, +{ + let (graph, vertices) = G::disconnected(); + let result = algorithms::dfs(&graph, vertices[0]); + assert!( + result.visited[vertices[0]], + "source vertex should be visited" + ); + assert_eq!( + result.predecessors[vertices[0]], None, + "unexpected predecessor of source vertex" + ); + for &v in &vertices[1..3] { + assert!( + !result.visited[v], + "disconnected vertex {v:?} should not be visited" + ); + assert_eq!( + result.predecessors[v], None, + "disconnected vertex {v:?} should have no predecessor" + ); + } +} + +fn dfs() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let result = algorithms::dfs(&graph, vertices[0]); + assert_dfs_visited::(&result.visited, &vertices); + assert_dfs_predecessors(&graph, &result.visited, &result.predecessors, &vertices); +} + +fn dfs_visited_single_vertex() { + let (graph, v) = G::single_vertex(); + let visited = algorithms::dfs_visited(&graph, v); + assert!(visited[v], "source vertex should be visited"); +} + +fn dfs_visited_disconnected() +where + G::Vertex: Debug, +{ + let (graph, vertices) = G::disconnected(); + let visited = algorithms::dfs_visited(&graph, vertices[0]); + assert!(visited[vertices[0]], "source vertex should be visited"); + for &v in &vertices[1..3] { + assert!( + !visited[v], + "disconnected vertex {v:?} should not be visited" + ); + } +} + +fn dfs_visited() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let visited = algorithms::dfs_visited(&graph, vertices[0]); + assert_dfs_visited::(&visited, &vertices); +} + +fn dfs_find_source() { + let (graph, v) = G::single_vertex(); + assert!( + algorithms::dfs_find(&graph, v, v), + "source should find itself" + ); +} + +fn dfs_find_disconnected() { + let (graph, vertices) = G::disconnected(); + assert!( + !algorithms::dfs_find(&graph, vertices[0], vertices[1]), + "disconnected target should not be found" + ); +} + +fn dfs_find() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + for i in 0..10 { + assert!( + algorithms::dfs_find(&graph, vertices[0], vertices[i]), + "vertex {:?} should be reachable from {:?}", + vertices[i], + vertices[0] + ); + } +} + +fn dfs_find_where_source_matches() +where + G::Vertex: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::dfs_find_where(&graph, v, |u| u == v), + Some(v), + "source should find itself" + ); +} + +fn dfs_find_where_disconnected() +where + G::Vertex: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), + None, + "disconnected vertex {:?} should not be found", + vertices[1] + ); +} + +fn dfs_find_where_no_match() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::dfs_find_where(&graph, vertices[0], |_| false), + None, + "no vertex should match an always-false predicate" + ); +} + +fn dfs_find_where_adjacent() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), + Some(vertices[1]), + "expected to find adjacent vertex" + ); +} + +fn dfs_find_where() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[9]), + Some(vertices[9]), + "expected to find connected vertex" + ); +} + +fn dfs_find_path_source_equals_target() +where + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::dfs_find_path(&graph, v, v), + Some(vec![]), + "path from source to itself should be empty" + ); +} + +fn dfs_find_path_disconnected() +where + G::Edge: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::dfs_find_path(&graph, vertices[0], vertices[1]), + None, + "no path should exist to disconnected vertex" + ); +} + +fn dfs_find_path_adjacent() +where + G::Edge: Debug, +{ + let (graph, vertices, e) = G::single_edge(); + let path = algorithms::dfs_find_path(&graph, vertices[0], vertices[1]) + .expect("path should exist between adjacent vertices"); + assert_eq!( + path.len(), + 1, + "unexpected path length between adjacent vertices" + ); + assert_eq!(path[0], e, "path should use the connecting edge"); +} + +fn dfs_find_path() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let path = algorithms::dfs_find_path(&graph, vertices[0], vertices[9]).expect(&format!( + "path should exist between connected vertices {:?} and {:?}", + vertices[0], vertices[9] + )); + assert_valid_path(&graph, &path, vertices[0], vertices[9]); +} + +fn dfs_find_path_where_source_matches() +where + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::dfs_find_path_where(&graph, v, |u| u == v), + Some(vec![]), + "path from source to itself should be empty" + ); +} + +fn dfs_find_path_where_disconnected() +where + G::Edge: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), + None, + "no path should exist to disconnected vertex" + ); +} + +fn dfs_find_path_where_no_match() +where + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::dfs_find_path_where(&graph, vertices[0], |_| false), + None, + "no path should exist when predicate never matches" + ); +} + +fn dfs_find_path_where() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let path = algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]).expect( + &format!( + "path should exist between connected vertices {:?} and {:?}", + vertices[0], vertices[9] + ), + ); + assert_valid_path(&graph, &path, vertices[0], vertices[9]); +} + +fn assert_dfs_visited( + visited: &ElementMap, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + for i in 0..10 { + assert!( + visited[vertices[i]], + "vertex {:?} should be visited", + vertices[i] + ); + } +} + +fn assert_dfs_predecessors( + graph: &G, + visited: &ElementMap, + predecessors: &ElementMap>, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + assert_eq!( + predecessors[vertices[0]], None, + "source should have no predecessor" + ); + for i in 1..10 { + let v = vertices[i]; + let p = predecessors[v].expect(&format!("vertex {v:?} should have a predecessor")); + assert!( + visited[p], + "predecessor {p:?} of vertex {v:?} should be visited" + ); + assert!( + graph.are_adjacent(v, p), + "predecessor {p:?} of vertex {v:?} should be adjacent" + ); + } +} + +fn assert_valid_path( + graph: &G, + path: &[G::Edge], + source: G::Vertex, + target: G::Vertex, +) where + G::Vertex: Debug, + G::Edge: Debug, +{ + assert!(!path.is_empty(), "path should be non-empty"); + // Walks the path: tracks current vertex, confirm each edge is incident to it. + let mut current = source; + for (i, &e) in path.iter().enumerate() { + let (v1, v2) = graph.incident_vertices(e); + assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); + assert!( + v1 == current || v2 == current, + "path edge {e:?} (idx {i}, {v1:?} to {v2:?}) not incident to vertex {current:?}" + ); + current = if v1 == current { v2 } else { v1 }; + } + assert_eq!( + current, target, + "path should end at target {target:?}, but ended at {current:?}" + ); } diff --git a/tests/dijkstra.rs b/tests/dijkstra.rs index e655000..0945aa9 100644 --- a/tests/dijkstra.rs +++ b/tests/dijkstra.rs @@ -1,11 +1,348 @@ -mod append_graph_tests { +use std::fmt::Debug; +use std::hash::Hash; + +use grapherity::algorithms; +use grapherity::algorithms::DijkstraResult; +use grapherity::maps::ElementMap; +use grapherity::testing::fixtures::MakeTestGraph; +use grapherity::traits::{GraphTopology, Incidence}; + +macro_rules! dijkstra_tests { + ($T:ty) => { + dijkstra_tests!(@wrap $T, + dijkstra_single_vertex, + dijkstra_disconnected, + dijkstra_zero_weight_loop, + dijkstra, + dijkstra_distances, + dijkstra_unweighted_single_vertex, + dijkstra_unweighted_disconnected, + dijkstra_unweighted, + dijkstra_distances_unweighted_single_vertex, + dijkstra_distances_unweighted_disconnected, + dijkstra_distances_unweighted, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + super::$name::<$T>(); + } + )* + }; +} + +mod append_graph { use grapherity::models::AppendGraph; - grapherity::dijkstra_tests!(AppendGraph); + dijkstra_tests!(AppendGraph); } -mod graph_tests { +mod graph { use grapherity::models::Graph; - grapherity::dijkstra_tests!(Graph); + dijkstra_tests!(Graph); +} + +fn dijkstra_single_vertex() +where + G::Vertex: Debug + Hash, +{ + let (graph, v) = G::single_vertex(); + let result = algorithms::dijkstra(&graph, v, |_| panic!("unexpected call of weight functor")); + assert_single_vertex::(&result, v); +} + +fn dijkstra_disconnected() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices) = G::disconnected(); + let result = algorithms::dijkstra(&graph, vertices[0], |_| { + panic!("unexpected call of weight functor") + }); + assert_single_vertex::(&result, vertices[0]); + assert_disconnected::(&result, &vertices[1..3]); +} + +fn dijkstra_zero_weight_loop() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices, _, weights) = make_two_edge_path_with_zero_weight_loops::(); + let result = algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); + assert_single_vertex::(&result, vertices[0]); + for i in 1..3 { + assert_eq!( + result.predecessors[vertices[i]], + Some(vertices[i - 1]), + "unexpected predecessor of vertex {:?}", + vertices[i] + ); + assert_eq!( + result.distances[vertices[i]], + Some(i.try_into().unwrap()), + "unexpected distance of vertex {:?}", + vertices[i] + ); + } +} + +fn dijkstra() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices, _, _, weights) = make_test_graph_weighted::(); + let result = algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); + assert_test_graph::(&result, &vertices); +} + +fn dijkstra_distances() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices, _, _, weights) = make_test_graph_weighted::(); + let distances = algorithms::dijkstra_distances(&graph, vertices[0], |e| weights[e]); + assert_distances_test_graph::(&distances, &vertices); +} + +fn dijkstra_unweighted_single_vertex() +where + G::Vertex: Debug + Hash, +{ + let (graph, v) = G::single_vertex(); + let result = algorithms::dijkstra_unweighted(&graph, v); + assert_single_vertex::(&result, v) +} + +fn dijkstra_unweighted_disconnected() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices) = G::disconnected(); + let result = algorithms::dijkstra_unweighted(&graph, vertices[0]); + assert_single_vertex::(&result, vertices[0]); + assert_disconnected::(&result, &vertices[1..3]); +} + +fn dijkstra_unweighted() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices, _, _) = G::standard(); + let result = algorithms::dijkstra_unweighted(&graph, vertices[0]); + assert_unweighted_test_graph::(&result, &vertices); +} + +fn dijkstra_distances_unweighted_single_vertex() +where + G::Vertex: Hash, +{ + let (graph, v) = G::single_vertex(); + let distances = algorithms::dijkstra_distances_unweighted(&graph, v); + assert_distances_single_vertex::(&distances, v); +} + +fn dijkstra_distances_unweighted_disconnected() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices) = G::disconnected(); + let distances = algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); + assert_distances_single_vertex::(&distances, vertices[0]); + assert_distances_disconnected::(&distances, &vertices[1..3]); +} + +fn dijkstra_distances_unweighted() +where + G::Vertex: Debug + Hash, +{ + let (graph, vertices, _, _) = G::standard(); + let distances = algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); + assert_distances_unweighted_test_graph::(&distances, &vertices); +} + +fn assert_single_vertex(result: &DijkstraResult, v: G::Vertex) +where + G::Vertex: Debug, +{ + assert_distances_single_vertex::(&result.distances, v); + assert_eq!( + result.predecessors[v], None, + "unexpected predecessor of source vertex", + ); +} + +fn assert_distances_single_vertex( + distances: &ElementMap>, + v: G::Vertex, +) { + assert_eq!( + distances[v], + Some(0), + "unexpected distance of source vertex" + ); +} + +fn assert_disconnected(result: &DijkstraResult, vertices: &[G::Vertex]) +where + G::Vertex: Debug, +{ + assert_distances_disconnected::(&result.distances, &vertices); + for &v in vertices { + assert_eq!( + result.predecessors[v], None, + "unexpected predecessor of disconnected vertex {v:?}", + ); + } +} + +fn assert_distances_disconnected( + distances: &ElementMap>, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + for &v in vertices { + assert_eq!( + distances[v], None, + "unexpected distance of disconnected vertex {v:?}", + ); + } +} + +fn assert_test_graph(result: &DijkstraResult, vertices: &[G::Vertex]) +where + G::Vertex: Debug, +{ + assert_distances_test_graph::(&result.distances, &vertices); + let expected_predecessors_from_v0 = [ + vec![None], + vec![Some(vertices[0])], + vec![Some(vertices[4])], + vec![Some(vertices[1])], + vec![Some(vertices[7])], + vec![Some(vertices[9])], + vec![Some(vertices[3])], + vec![Some(vertices[9])], + vec![Some(vertices[7])], + vec![Some(vertices[6])], + ]; + for i in 0..10 { + assert!( + expected_predecessors_from_v0[i].contains(&result.predecessors[vertices[i]]), + "unexpected predecessor {:?} of {:?}", + result.predecessors[vertices[i]], + vertices[i] + ); + } +} + +fn assert_distances_test_graph( + distances: &ElementMap>, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + let expected_distances_from_v0 = [ + Some(0), + Some(1), + Some(65), + Some(4), + Some(58), + Some(43), + Some(14), + Some(46), + Some(145), + Some(29), + ]; + for i in 0..10 { + assert_eq!( + distances[vertices[i]], expected_distances_from_v0[i], + "unexpected distance from {:?} to {:?}", + vertices[0], vertices[i] + ); + } +} + +fn assert_unweighted_test_graph( + result: &DijkstraResult, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + assert_distances_unweighted_test_graph::(&result.distances, &vertices); + let expected_predecessors_from_v0 = [ + vec![None], + vec![Some(vertices[0])], + vec![Some(vertices[1])], + vec![Some(vertices[1])], + vec![Some(vertices[1])], + vec![Some(vertices[2])], + vec![Some(vertices[2]), Some(vertices[3])], + vec![Some(vertices[4])], + vec![Some(vertices[4])], + vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], + ]; + for i in 0..10 { + assert!( + expected_predecessors_from_v0[i].contains(&result.predecessors[vertices[i]]), + "unexpected predecessor {:?} of {:?}", + result.predecessors[vertices[i]], + vertices[i] + ); + } +} + +fn assert_distances_unweighted_test_graph( + distances: &ElementMap>, + vertices: &[G::Vertex], +) where + G::Vertex: Debug, +{ + let expected_distances_from_v0 = [ + Some(0), + Some(1), + Some(2), + Some(2), + Some(2), + Some(3), + Some(3), + Some(3), + Some(3), + Some(4), + ]; + for i in 0..10 { + assert_eq!( + distances[vertices[i]], expected_distances_from_v0[i], + "unexpected distance from {:?} to {:?}", + vertices[0], vertices[i] + ); + } +} + +fn make_test_graph_weighted() -> ( + G, + [G::Vertex; 10], + [(G::Edge, G::Vertex, G::Vertex); 18], + [Vec>; 10], + ElementMap, +) { + let (graph, vertices, edges, incidences) = G::standard(); + let mut weights = graph.edge_map(99); + for i in [1, 3, 7, 10, 12, 14, 15, 17] { + weights[edges[i].0] = i.try_into().unwrap(); + } + (graph, vertices, edges, incidences, weights) +} + +fn make_two_edge_path_with_zero_weight_loops() +-> (G, [G::Vertex; 3], [G::Edge; 4], ElementMap) { + let (graph, vertices, edges) = G::two_edge_path_with_loops(); + let mut weights = graph.edge_map(1); + weights[edges[2]] = 0; + weights[edges[3]] = 0; + (graph, vertices, edges, weights) } -- 2.43.0 From 0ff830ef48713e019959b9ede950648cf625f2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 31 Aug 2026 19:49:42 +0200 Subject: [PATCH 32/67] Refactor graph trait tests to minimize macro code, and split into separate files per macro The tests only declare a list of tests in the macro, no longer the test functions themselves. This allows better tooling support (e.g. IDE and rustfmt). --- src/testing.rs | 3 + ...raph_topology_addition_deletion_testing.rs | 62 + .../graph_topology_addition_testing.rs | 152 ++ .../graph_topology_deletion_testing.rs | 542 +++++ src/testing/graph_topology_testing.rs | 2017 +++++++---------- 5 files changed, 1528 insertions(+), 1248 deletions(-) create mode 100644 src/testing/graph_topology_addition_deletion_testing.rs create mode 100644 src/testing/graph_topology_addition_testing.rs create mode 100644 src/testing/graph_topology_deletion_testing.rs diff --git a/src/testing.rs b/src/testing.rs index 64c099c..d730642 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,4 +1,7 @@ //! Test fixture and test macros for graph topology and algorithm implementations. pub mod fixtures; +pub(crate) mod graph_topology_addition_deletion_testing; +pub(crate) mod graph_topology_addition_testing; +pub(crate) mod graph_topology_deletion_testing; pub(crate) mod graph_topology_testing; diff --git a/src/testing/graph_topology_addition_deletion_testing.rs b/src/testing/graph_topology_addition_deletion_testing.rs new file mode 100644 index 0000000..14fb7be --- /dev/null +++ b/src/testing/graph_topology_addition_deletion_testing.rs @@ -0,0 +1,62 @@ +use std::fmt::Debug; + +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{GraphTopologyAddition, GraphTopologyDeletion}; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_deletion_tests { + ($T:ty) => { + $crate::graph_topology_addition_deletion_tests!(@wrap $T, + delete_vertex_add_vertex, + delete_edge_add_edge, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_addition_deletion_testing::$name::<$T>(); + } + )* + }; +} + +pub(crate) fn delete_vertex_add_vertex() +where + G::Vertex: Debug, +{ + let (mut graph, v) = G::single_vertex(); + graph.delete_vertex(v); + assert_eq!( + graph.vertex_count(), + 0, + "unexpected vertex count after delete" + ); + assert_ne!( + graph.add_vertex(), + v, + "unexpected duplicate vertex after delete" + ); + assert_eq!( + graph.vertex_count(), + 1, + "unexpected vertex count after re-add" + ); +} + +pub(crate) fn delete_edge_add_edge() +where + G::Edge: Debug, +{ + let (mut graph, vertices, e) = G::single_edge(); + graph.delete_edge(e); + assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); + assert_ne!( + graph.add_edge(vertices[0], vertices[1]), + e, + "unexpected duplicate edge after re-add" + ); + assert_eq!(graph.edge_count(), 1, "unexpected edge count after re-add"); +} diff --git a/src/testing/graph_topology_addition_testing.rs b/src/testing/graph_topology_addition_testing.rs new file mode 100644 index 0000000..386fa8d --- /dev/null +++ b/src/testing/graph_topology_addition_testing.rs @@ -0,0 +1,152 @@ +use std::fmt::Debug; + +use crate::traits::GraphTopologyAddition; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_tests { + ($T:ty) => { + $crate::graph_topology_addition_tests!(@wrap $T, + reserve_vertices_increases_capacity, + reserve_vertices_prevents_reallocation_on_add, + reserve_vertices_does_not_affect_edge_capacity, + reserve_edges_increases_capacity, + reserve_edges_prevents_reallocation_on_add, + reserve_edges_does_not_affect_vertex_capacity, + add_vertex, + add_edge, + vertex_map_new_vertex, + edge_map_new_edge, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_addition_testing::$name::<$T>(); + } + )* + }; +} + +pub(crate) fn reserve_vertices_increases_capacity() { + let mut graph = G::default(); + let capacity_before = graph.vertex_capacity(); + graph.reserve_vertices(capacity_before + 10); + assert!( + graph.vertex_capacity() > capacity_before, + "expected sufficient capacity increase after reserve" + ); +} + +pub(crate) fn reserve_vertices_prevents_reallocation_on_add() { + let mut graph = G::default(); + graph.reserve_vertices(10); + let capacity_before = graph.vertex_capacity(); + for _ in 0..10 { + graph.add_vertex(); + } + assert_eq!( + graph.vertex_capacity(), + capacity_before, + "writes within reserved capacity should not reallocate" + ); +} + +pub(crate) fn reserve_vertices_does_not_affect_edge_capacity() { + let mut graph = G::default(); + let capacity_before = graph.edge_capacity(); + graph.reserve_vertices(10); + assert_eq!( + graph.edge_capacity(), + capacity_before, + "reserve_vertices must not change edge capacity" + ); +} + +pub(crate) fn reserve_edges_increases_capacity() { + let mut graph = G::default(); + let capacity_before = graph.edge_capacity(); + graph.reserve_edges(capacity_before + 10); + assert!( + graph.edge_capacity() > capacity_before, + "expected sufficient capacity increase after reserve" + ); +} + +pub(crate) fn reserve_edges_prevents_reallocation_on_add() { + let mut graph = G::default(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + graph.reserve_edges(10); + let capacity_before = graph.edge_capacity(); + for _ in 0..10 { + graph.add_edge(v1, v2); + } + assert_eq!( + graph.edge_capacity(), + capacity_before, + "writes within reserved capacity should not reallocate" + ); +} + +pub(crate) fn reserve_edges_does_not_affect_vertex_capacity() { + let mut graph = G::default(); + let capacity_before = graph.vertex_capacity(); + graph.reserve_edges(10); + assert_eq!( + graph.vertex_capacity(), + capacity_before, + "reserve_edges must not change vertex capacity" + ); +} + +pub(crate) fn add_vertex() +where + G::Vertex: Debug, +{ + let mut graph = G::default(); + let v = graph.add_vertex(); + assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); +} + +pub(crate) fn add_edge() +where + G::Edge: Debug, +{ + let mut graph = G::default(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + let e = graph.add_edge(v1, v2); + assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); +} + +pub(crate) fn vertex_map_new_vertex() { + let mut graph = G::default(); + let mut map = graph.vertex_map(27); + let v = graph.add_vertex(); + assert_eq!( + map[v], 27, + "unexpected value from default map read for new vertex" + ); + map[v] = 9; + assert_eq!( + map[v], 9, + "unexpected value from map read after write for new vertex" + ); +} + +pub(crate) fn edge_map_new_edge() { + let mut graph = G::default(); + let v1 = graph.add_vertex(); + let v2 = graph.add_vertex(); + let mut map = graph.edge_map(28); + let e = graph.add_edge(v1, v2); + assert_eq!(map[e], 28); + map[e] = 8; + assert_eq!( + map[e], 8, + "unexpected value from map read after write for new edge" + ); +} diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs new file mode 100644 index 0000000..d530df7 --- /dev/null +++ b/src/testing/graph_topology_deletion_testing.rs @@ -0,0 +1,542 @@ +use std::fmt::Debug; + +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{ + GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, IncidenceCursor, +}; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_deletion_tests { + ($T:ty) => { + $crate::graph_topology_deletion_tests!(@wrap $T, + delete_vertex, + delete_vertex_loop, + delete_vertex_invalid_index, + delete_vertex_connected, + delete_edge, + delete_edge_loop, + delete_edge_multiple, + delete_edge_invalid_index, + vertices_after_delete, + incident_vertices_after_delete_edge, + edges_after_delete, + incident_edges_after_delete_edge, + incident_edges_after_delete_vertex, + incidences_after_delete_vertex, + incidences_after_delete_edge, + incidence_cursor_after_delete_vertex, + incidence_cursor_after_delete_edge, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_deletion_testing::$name::<$T>(); + } + )* + }; +} + +pub(crate) fn delete_vertex() { + let (mut graph, v) = G::single_vertex(); + assert_eq!( + graph.vertex_count(), + 1, + "unexpected vertex count before delete" + ); + graph.delete_vertex(v); + assert_eq!( + graph.vertex_count(), + 0, + "unexpected vertex count after delete" + ); +} + +pub(crate) fn delete_vertex_loop() { + let (mut graph, v, _) = G::loop_edge(); + assert_eq!( + graph.vertex_count(), + 1, + "unexpected vertex count before delete" + ); + assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); + assert!( + graph.are_adjacent(v, v), + "expected vertex to be self-adjacent before delete" + ); + assert_eq!(graph.degree(v), 2, "unexpected vertex degree before delete"); + graph.delete_vertex(v); + assert_eq!( + graph.vertex_count(), + 0, + "unexpected vertex count after delete" + ); + assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); +} + +pub(crate) fn delete_vertex_invalid_index() { + let (mut graph, v) = G::single_vertex(); + graph.delete_vertex(v); + let result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || graph.delete_vertex(v))); + assert!(result.is_err(), "second deletion should panic"); +} + +pub(crate) fn delete_vertex_connected() +where + G::Vertex: Debug, +{ + let (mut graph, vertices, _, _) = G::standard(); + assert_eq!( + graph.vertex_count(), + 10, + "unexpected vertex count before delete" + ); + assert_eq!( + graph.edge_count(), + 18, + "unexpected edge count before delete" + ); + graph.delete_vertex(vertices[2]); + assert_eq!( + graph.vertex_count(), + 9, + "unexpected vertex count after delete" + ); + assert_eq!(graph.edge_count(), 12, "unexpected edge count after delete"); + let expected_edges = [ + (vertices[0], vertices[1]), + (vertices[1], vertices[3]), + (vertices[1], vertices[4]), + (vertices[3], vertices[6]), + (vertices[4], vertices[4]), + (vertices[4], vertices[7]), + (vertices[4], vertices[8]), + (vertices[5], vertices[9]), + (vertices[6], vertices[9]), + (vertices[7], vertices[8]), + (vertices[7], vertices[9]), + ]; + for (v, u) in expected_edges { + assert!( + graph.are_adjacent(v, u), + "expected {v:?} and {u:?} to be adjacent after delete" + ); + } + for v in [vertices[1], vertices[4], vertices[5], vertices[6]] { + assert!( + !graph.adjacent_vertices(v).any(|u| u == vertices[2]), + "unexpected adjacency of {v:?} to deleted vertex {:?}", + vertices[2] + ); + } +} + +pub(crate) fn delete_edge() { + let (mut graph, vertices, e) = G::single_edge(); + assert_eq!( + graph.vertex_count(), + 2, + "unexpected vertex count before delete" + ); + assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); + assert!( + graph.are_adjacent(vertices[0], vertices[1]), + "expected vertices to be adjacent before delete" + ); + assert_eq!( + graph.degree(vertices[0]), + 1, + "unexpected vertex degree before delete" + ); + assert_eq!( + graph.degree(vertices[1]), + 1, + "unexpected vertex degree before delete" + ); + graph.delete_edge(e); + assert_eq!( + graph.vertex_count(), + 2, + "unexpected vertex count after delete" + ); + assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); + assert!( + !graph.are_adjacent(vertices[0], vertices[1]), + "unexpected adjacency after delete" + ); + assert_eq!( + graph.degree(vertices[0]), + 0, + "unexpected vertex degree after delete" + ); + assert_eq!( + graph.degree(vertices[1]), + 0, + "unexpected vertex degree after delete" + ); +} + +pub(crate) fn delete_edge_loop() { + let (mut graph, v, e) = G::loop_edge(); + assert_eq!( + graph.vertex_count(), + 1, + "unexpected vertex count before delete" + ); + assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); + assert!( + graph.are_adjacent(v, v), + "expected vertex to be self-adjacent before delete" + ); + assert_eq!(graph.degree(v), 2, "unexpected vertex degree before delete"); + graph.delete_edge(e); + assert_eq!( + graph.vertex_count(), + 1, + "unexpected vertex count after delete" + ); + assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); + assert!( + !graph.are_adjacent(v, v), + "unexpected adjacency after delete" + ); + assert_eq!(graph.degree(v), 0, "unexpected vertex degree after delete"); +} + +pub(crate) fn delete_edge_multiple() { + const K: usize = 2; + let (mut graph, vertices, edges) = G::multiple_edges::(); + assert_eq!( + graph.vertex_count(), + 2, + "unexpected vertex count before delete" + ); + assert_eq!(graph.edge_count(), K, "unexpected edge count before delete"); + assert!( + graph.are_adjacent(vertices[0], vertices[1]), + "expected vertices to be adjacent before delete" + ); + assert_eq!( + graph.degree(vertices[0]), + K, + "unexpected vertex degree before delete" + ); + assert_eq!( + graph.degree(vertices[1]), + K, + "unexpected vertex degree before delete" + ); + graph.delete_edge(edges[0]); + assert_eq!( + graph.vertex_count(), + 2, + "unexpected vertex count after delete" + ); + assert_eq!( + graph.edge_count(), + K - 1, + "unexpected edge count after delete" + ); + assert!( + graph.are_adjacent(vertices[0], vertices[1]), + "expected vertices to be adjacent after delete" + ); + assert_eq!( + graph.degree(vertices[0]), + K - 1, + "unexpected vertex degree after delete" + ); + assert_eq!( + graph.degree(vertices[1]), + K - 1, + "unexpected vertex degree after delete" + ); +} + +pub(crate) fn delete_edge_invalid_index() { + let (mut graph, _, e) = G::single_edge(); + graph.delete_edge(e); + let result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || graph.delete_edge(e))); + assert!(result.is_err(), "second deletion should panic"); +} + +pub(crate) fn vertices_after_delete() +where + G::Vertex: Debug, +{ + let (mut graph, vertices, _, _) = G::standard(); + graph.delete_vertex(vertices[2]); + assert_eq!( + graph.vertex_count(), + 9, + "unexpected vertex count after delete" + ); + assert_eq!( + graph.vertices().count(), + 9, + "unexpected vertex iterator count after delete" + ); + for v in graph.vertices() { + assert_ne!( + v, vertices[2], + "deleted vertex {:?} appeared in iterator", + vertices[2] + ); + } + for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { + assert!( + graph.vertices().any(|v| v == vertices[i]), + "expected vertex {:?} missing from iterator after delete", + vertices[i] + ); + } +} + +pub(crate) fn incident_vertices_after_delete_edge< + G: GraphTopologyDeletion + GraphTopologyAddition, +>() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, _, edges, _) = G::standard(); + let e = edges[2].0; + graph.delete_edge(e); + for &(f, v1, v2) in edges.iter().filter(|&&(f, _, _)| f != e) { + let (u1, u2) = graph.incident_vertices(f); + assert!( + (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), + "unexpected incident vertices {u1:?} and {u2:?} for edge {f:?} after delete" + ); + } +} + +pub(crate) fn edges_after_delete() +where + G::Edge: Debug, +{ + let (mut graph, vertices, edges, _) = G::standard(); + graph.delete_vertex(vertices[2]); + assert_eq!(graph.edge_count(), 12, "unexpected edge count after delete"); + assert_eq!( + graph.edges().count(), + 12, + "unexpected edge iterator count after delete" + ); + for i in [2, 5, 6, 7, 8, 9] { + assert!( + !graph.edges().any(|e| e == edges[i].0), + "deleted edge {:?} appeared in iterator", + edges[i].0 + ); + } + for i in [0, 1, 3, 4, 10, 11, 12, 13, 14, 15, 16, 17] { + assert!( + graph.edges().any(|e| e == edges[i].0), + "expected edge {:?} missing from iterator after delete", + edges[i].0 + ); + } +} + +pub(crate) fn incident_edges_after_delete_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, vertices, edges, incidences) = G::standard(); + graph.delete_edge(edges[2].0); + for i in 0..10 { + let mut expected: Vec<_> = incidences[i] + .iter() + .filter_map(|x| (x.edge != edges[2].0).then_some(x.edge)) + .collect(); + assert_eq!( + graph.incident_edges(vertices[i]).count(), + expected.len(), + "unexpected incident edge count for vertex {:?} after delete", + vertices[i] + ); + for e in graph.incident_edges(vertices[i]) { + let pos = expected.iter().position(|f| *f == e).expect(&format!( + "unexpected incident edge {e:?} of vertex {:?} after delete", + vertices[i] + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incident edges {:?} of vertex {:?} not matched after delete", + expected, + vertices[i] + ); + } +} + +pub(crate) fn incident_edges_after_delete_vertex() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, vertices, _, incidences) = G::standard(); + graph.delete_vertex(vertices[2]); + for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { + let mut expected: Vec<_> = incidences[i] + .iter() + .filter_map(|x| (x.vertex != vertices[2]).then_some(x.edge)) + .collect(); + assert_eq!( + graph.incident_edges(vertices[i]).count(), + expected.len(), + "unexpected incident edge count for vertex {:?} after delete", + vertices[i] + ); + for e in graph.incident_edges(vertices[i]) { + let pos = expected.iter().position(|f| *f == e).expect(&format!( + "unexpected incident edge {e:?} of vertex {:?} after delete", + vertices[i] + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incident edges {:?} of vertex {:?} not matched after delete", + expected, + vertices[i] + ); + } +} + +pub(crate) fn incidences_after_delete_vertex() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, vertices, _, incidences) = G::standard(); + graph.delete_vertex(vertices[2]); + for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { + let remaining = incidences[i] + .iter() + .filter(|x| x.vertex != vertices[2]) + .cloned() + .collect(); + assert_vertex_incidences(&graph, vertices[i], remaining); + } +} + +pub(crate) fn incidences_after_delete_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, vertices, edges, incidences) = G::standard(); + graph.delete_edge(edges[2].0); + for i in 0..10 { + let remaining = incidences[i] + .iter() + .filter(|x| x.edge != edges[2].0) + .cloned() + .collect(); + assert_vertex_incidences(&graph, vertices[i], remaining); + } +} + +pub(crate) fn incidence_cursor_after_delete_vertex< + G: GraphTopologyDeletion + GraphTopologyAddition, +>() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, vertices, _, incidences) = G::standard(); + graph.delete_vertex(vertices[2]); + for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { + let remaining = incidences[i] + .iter() + .filter(|x| x.vertex != vertices[2]) + .cloned() + .collect(); + assert_vertex_incidence_cursor(&graph, vertices[i], remaining); + } +} + +pub(crate) fn incidence_cursor_after_delete_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (mut graph, vertices, edges, incidences) = G::standard(); + graph.delete_edge(edges[2].0); + for i in 0..10 { + let remaining = incidences[i] + .iter() + .filter(|x| x.edge != edges[2].0) + .cloned() + .collect(); + assert_vertex_incidence_cursor(&graph, vertices[i], remaining); + } +} + +fn assert_vertex_incidences( + graph: &G, + v: G::Vertex, + mut expected: Vec>, +) where + G::Vertex: Debug, + G::Edge: Debug, +{ + assert_eq!( + graph.incidences(v).count(), + expected.len(), + "unexpected incidence count for vertex {:?} after delete", + v + ); + for incidence in graph.incidences(v) { + let pos = expected + .iter() + .position(|x| *x == incidence) + .expect(&format!( + "unexpected incidence {incidence:?} of vertex {:?} from iterator after delete", + v + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incidences {:?} of vertex {:?} not matched by iterator after delete", + expected, + v + ); +} + +fn assert_vertex_incidence_cursor( + graph: &G, + v: G::Vertex, + mut expected: Vec>, +) where + G::Vertex: Debug, + G::Edge: Debug, +{ + let mut cursor = graph.incidence_cursor(v); + while let Some(incidence) = cursor.next(graph) { + let pos = expected + .iter() + .position(|x| *x == incidence) + .expect(&format!( + "unexpected incidence {incidence:?} of vertex {:?} from cursor after delete", + v + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incidences {:?} of vertex {:?} not matched by cursor after delete", + expected, + v + ); +} diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index dd414b7..1d45f83 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -1,1285 +1,806 @@ +use std::fmt::Debug; + +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{Incidence, IncidenceCursor}; + #[doc(hidden)] #[macro_export] macro_rules! graph_topology_tests { ($T:ty) => { - #[test] - fn vertex_count_empty() { - use $crate::traits::GraphTopology; - let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); - assert_eq!(graph.vertex_count(), 0, "unexpected vertex count"); - } + $crate::graph_topology_tests!(@wrap $T, + vertex_count_empty, + vertex_count, + vertex_map, + edge_count_empty, + edge_count, + edge_map, + degree_zero, + degree, + loop_edge, + multiple_edges, + are_adjacent_vertex_self, + are_adjacent_single_edge, + are_adjacent, + vertices_empty, + vertices, + adjacent_vertices_empty, + adjacent_vertices, + adjacent_vertices_loop_edge, + adjacent_vertices_multiple_edges, + incident_vertices_single_edge, + incident_vertices_loop_edge, + incident_vertices_multiple_edges, + incident_vertices, + edges_empty, + edges, + incident_edges_empty, + incident_edges, + incident_edges_loop_edge, + incident_edges_multiple_edges, + incidences_empty, + incidences, + incidences_edge_consistency, + incidences_loop_edge, + incidences_multiple_edges, + incidence_cursor_empty, + incidence_cursor, + incidence_cursor_loop_edge, + incidence_cursor_multiple_edges, + incidence_cursor_copy, + incident_vertices_incidences_consistency, + incident_edges_incidences_consistency, + ); + }; - #[test] - fn vertex_count() { - use $crate::traits::GraphTopology; - let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); - } - - #[test] - fn vertex_map() { - use $crate::traits::GraphTopology; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let mut map = graph.vertex_map(27); - assert_eq!(map[v], 27, "unexpected value from default map read"); - map[v] = 9; - assert_eq!(map[v], 9, "unexpected value from map after write"); - } - - #[test] - fn edge_count_empty() { - use $crate::traits::GraphTopology; - let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); - assert_eq!(graph.edge_count(), 0, "unexpected edge count"); - } - - #[test] - fn edge_count() { - use $crate::traits::GraphTopology; - let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!(graph.edge_count(), 18, "unexpected edge count"); - } - - #[test] - fn edge_map() { - use $crate::traits::GraphTopology; - let (graph, _, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); - let mut map = graph.edge_map(28); - assert_eq!(map[e], 28, "unexpected value from default map read"); - map[e] = 8; - assert_eq!(map[e], 8, "unexpected value from map after write"); - } - - #[test] - fn degree_zero() { - use $crate::traits::GraphTopology; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!(graph.degree(v), 0, "unexpected non-zero degree"); - } - - #[test] - fn degree() { - use $crate::traits::GraphTopology; - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - let expected_degrees = [2, 5, 7, 2, 7, 2, 3, 3, 2, 3]; - for i in 0..graph.vertex_count() { - assert_eq!( - graph.degree(vertices[i]), - expected_degrees[i], - "unexpected degree of {:?}", - vertices[i] - ); + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_testing::$name::<$T>(); } - } + )* + }; +} - #[test] - fn loop_edge() { - use $crate::traits::GraphTopology; - let (graph, v, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - assert_eq!(graph.vertex_count(), 1, "unexpected vertex count"); - assert_eq!(graph.edge_count(), 1, "unexpected edge count"); - assert_eq!(graph.degree(v), 2, "unexpected degree"); - assert!( - graph.are_adjacent(v, v), - "vertex with loop edge should be adjacent to itself" - ); - } +pub(crate) fn vertex_count_empty() { + let graph = G::empty(); + assert_eq!(graph.vertex_count(), 0, "unexpected vertex count"); +} - #[test] - fn multiple_edges() { - use $crate::traits::GraphTopology; - const K: usize = 3; - let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); - assert_eq!(graph.vertex_count(), 2, "unexpected vertex count"); - assert_eq!(graph.edge_count(), K, "unexpected edge count"); - assert_eq!(graph.degree(vertices[0]), K, "unexpected degree of vertex {:?}", vertices[0]); - assert_eq!(graph.degree(vertices[1]), K, "unexpected degree of vertex {:?}", vertices[1]); - assert!(graph.are_adjacent(vertices[0], vertices[1]), "should be adjacent"); - } +pub(crate) fn vertex_count() { + let (graph, _, _, _) = G::standard(); + assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); +} - #[test] - fn are_adjacent_vertex_self() { - use $crate::traits::GraphTopology; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert!(!graph.are_adjacent(v, v), "should not be adjacent to itself"); - } +pub(crate) fn vertex_map() { + let (graph, v) = G::single_vertex(); + let mut map = graph.vertex_map(27); + assert_eq!(map[v], 27, "unexpected value from default map read"); + map[v] = 9; + assert_eq!(map[v], 9, "unexpected value from map after write"); +} - #[test] - fn are_adjacent_single_edge() { - use $crate::traits::GraphTopology; - let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); - assert!(graph.are_adjacent(vertices[0], vertices[1]), "should be adjacent"); - assert!(graph.are_adjacent(vertices[1], vertices[0]), "should be adjacent"); - } +pub(crate) fn edge_count_empty() { + let graph = G::empty(); + assert_eq!(graph.edge_count(), 0, "unexpected edge count"); +} - #[test] - fn are_adjacent() { - use $crate::traits::GraphTopology; - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert!( - graph.are_adjacent(vertices[0], vertices[1]), - "expected {:?} and {:?} to be adjacent", - vertices[0], - vertices[1] - ); - assert!( - graph.are_adjacent(vertices[9], vertices[5]), - "expected {:?} and {:?} to be adjacent", - vertices[9], - vertices[5] - ); - assert!( - !graph.are_adjacent(vertices[9], vertices[3]), - "unexpected adjacency of {:?} and {:?}", - vertices[9], - vertices[3] - ); +pub(crate) fn edge_count() { + let (graph, _, _, _) = G::standard(); + assert_eq!(graph.edge_count(), 18, "unexpected edge count"); +} - for i in 0..graph.vertex_count() { - let exp = match i { - 1 | 2 | 4 | 5 | 6 => true, - _ => false, - }; - assert_eq!( - graph.are_adjacent(vertices[2], vertices[i]), - exp, - "unexpected adjacency of {:?} and {:?}", - vertices[2], - vertices[i] - ); - assert_eq!( - graph.are_adjacent(vertices[i], vertices[2]), - exp, - "unexpected adjacency of {:?} and {:?}", - vertices[i], - vertices[2] - ); - } - } +pub(crate) fn edge_map() { + let (graph, _, e) = G::single_edge(); + let mut map = graph.edge_map(28); + assert_eq!(map[e], 28, "unexpected value from default map read"); + map[e] = 8; + assert_eq!(map[e], 8, "unexpected value from map after write"); +} - #[test] - fn vertices_empty() { - use $crate::traits::GraphTopology; - let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); - assert_eq!( - graph.vertices().count(), - 0, - "vertex iterator of empty graph should have no elements" - ); - } +pub(crate) fn degree_zero() { + let (graph, v) = G::single_vertex(); + assert_eq!(graph.degree(v), 0, "unexpected non-zero degree"); +} - #[test] - fn vertices() { - use $crate::traits::GraphTopology; - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!(graph.vertices().count(), 10, "unexpected vertex count"); +pub(crate) fn degree() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let expected_degrees = [2, 5, 7, 2, 7, 2, 3, 3, 2, 3]; + for i in 0..graph.vertex_count() { + assert_eq!( + graph.degree(vertices[i]), + expected_degrees[i], + "unexpected degree of {:?}", + vertices[i] + ); + } +} - // Expects each vertex to appear exactly once. - for v in graph.vertices() { - assert_eq!( - vertices.iter().filter(|&x| *x == v).count(), - 1, - "unexpected vertex {v:?} from iterator" - ); - } - } +pub(crate) fn loop_edge() { + let (graph, v, _) = G::loop_edge(); + assert_eq!(graph.vertex_count(), 1, "unexpected vertex count"); + assert_eq!(graph.edge_count(), 1, "unexpected edge count"); + assert_eq!(graph.degree(v), 2, "unexpected degree"); + assert!( + graph.are_adjacent(v, v), + "vertex with loop edge should be adjacent to itself" + ); +} - #[test] - fn adjacent_vertices_empty() { - use $crate::traits::GraphTopology; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - graph.adjacent_vertices(v).count(), - 0, - "adjacent vertex iterator of vertex with degree 0 should have no elements" - ); - } +pub(crate) fn multiple_edges() +where + G::Vertex: Debug, +{ + const K: usize = 3; + let (graph, vertices, _) = G::multiple_edges::(); + assert_eq!(graph.vertex_count(), 2, "unexpected vertex count"); + assert_eq!(graph.edge_count(), K, "unexpected edge count"); + assert_eq!( + graph.degree(vertices[0]), + K, + "unexpected degree of vertex {:?}", + vertices[0] + ); + assert_eq!( + graph.degree(vertices[1]), + K, + "unexpected degree of vertex {:?}", + vertices[1] + ); + assert!( + graph.are_adjacent(vertices[0], vertices[1]), + "should be adjacent" + ); +} - #[test] - fn adjacent_vertices() { - use $crate::traits::GraphTopology; - let (graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - // Checks adjacency of vertex 4. - assert_eq!( - graph.adjacent_vertices(vertices[4]).count(), - 7, - "unexpected adjacency count" - ); - let mut expected_adjacency = vec![ - vertices[1], - vertices[2], - vertices[2], - vertices[4], - vertices[4], - vertices[7], - vertices[8], - ]; - for v in graph.adjacent_vertices(vertices[4]) { - let i = expected_adjacency - .iter() - .position(|w| *w == v) - .expect(&format!( - "unexpected adjacent vertex {v:?} of {:?} from iterator", - vertices[4] - )); - expected_adjacency.swap_remove(i); - } - assert_eq!( - expected_adjacency.len(), - 0, - "expected adjacent vertices {:?} of {:?} were not matched by iterator", - expected_adjacency, +pub(crate) fn are_adjacent_vertex_self() { + let (graph, v) = G::single_vertex(); + assert!( + !graph.are_adjacent(v, v), + "should not be adjacent to itself" + ); +} + +pub(crate) fn are_adjacent_single_edge() { + let (graph, vertices, _) = G::single_edge(); + assert!( + graph.are_adjacent(vertices[0], vertices[1]), + "should be adjacent" + ); + assert!( + graph.are_adjacent(vertices[1], vertices[0]), + "should be adjacent" + ); +} + +pub(crate) fn are_adjacent() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert!( + graph.are_adjacent(vertices[0], vertices[1]), + "expected {:?} and {:?} to be adjacent", + vertices[0], + vertices[1] + ); + assert!( + graph.are_adjacent(vertices[9], vertices[5]), + "expected {:?} and {:?} to be adjacent", + vertices[9], + vertices[5] + ); + assert!( + !graph.are_adjacent(vertices[9], vertices[3]), + "unexpected adjacency of {:?} and {:?}", + vertices[9], + vertices[3] + ); + for i in 0..graph.vertex_count() { + let exp = match i { + 1 | 2 | 4 | 5 | 6 => true, + _ => false, + }; + assert_eq!( + graph.are_adjacent(vertices[2], vertices[i]), + exp, + "unexpected adjacency of {:?} and {:?}", + vertices[2], + vertices[i] + ); + assert_eq!( + graph.are_adjacent(vertices[i], vertices[2]), + exp, + "unexpected adjacency of {:?} and {:?}", + vertices[i], + vertices[2] + ); + } +} + +pub(crate) fn vertices_empty() { + let graph = G::empty(); + assert_eq!( + graph.vertices().count(), + 0, + "vertex iterator of empty graph should have no elements" + ); +} + +pub(crate) fn vertices() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!(graph.vertices().count(), 10, "unexpected vertex count"); + for v in graph.vertices() { + assert_eq!( + vertices.iter().filter(|&x| *x == v).count(), + 1, + "unexpected vertex {v:?} from iterator" + ); + } +} + +pub(crate) fn adjacent_vertices_empty() { + let (graph, v) = G::single_vertex(); + assert_eq!( + graph.adjacent_vertices(v).count(), + 0, + "adjacent vertex iterator of vertex with degree 0 should have no elements" + ); +} + +pub(crate) fn adjacent_vertices() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + graph.adjacent_vertices(vertices[4]).count(), + 7, + "unexpected adjacency count" + ); + let mut expected_adjacency = vec![ + vertices[1], + vertices[2], + vertices[2], + vertices[4], + vertices[4], + vertices[7], + vertices[8], + ]; + for v in graph.adjacent_vertices(vertices[4]) { + let i = expected_adjacency + .iter() + .position(|w| *w == v) + .expect(&format!( + "unexpected adjacent vertex {v:?} of {:?} from iterator", vertices[4] - ); - } + )); + expected_adjacency.swap_remove(i); + } + assert_eq!( + expected_adjacency.len(), + 0, + "expected adjacent vertices {:?} of {:?} were not matched by iterator", + expected_adjacency, + vertices[4] + ); +} - #[test] - fn adjacent_vertices_loop_edge() { - use $crate::traits::GraphTopology; - let (graph, v, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - let mut iter = graph.adjacent_vertices(v); - assert_eq!(iter.next(), Some(v), "vertex should be adjacent to itself"); - assert_eq!(iter.next(), Some(v), "vertex should be adjacent to itself twice"); - assert_eq!(iter.next(), None, "too many adjacent vertices from iterator"); - } +pub(crate) fn adjacent_vertices_loop_edge() +where + G::Vertex: Debug, +{ + let (graph, v, _) = G::loop_edge(); + let mut iter = graph.adjacent_vertices(v); + assert_eq!(iter.next(), Some(v), "vertex should be adjacent to itself"); + assert_eq!( + iter.next(), + Some(v), + "vertex should be adjacent to itself twice" + ); + assert_eq!( + iter.next(), + None, + "too many adjacent vertices from iterator" + ); +} - #[test] - fn adjacent_vertices_multiple_edges() { - use $crate::traits::GraphTopology; - const K: usize = 3; - let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); - for i in 0..2 { - let mut iter = graph.adjacent_vertices(vertices[i]); - for j in 0..K { - assert_eq!( - iter.next(), - Some(vertices[1 - i]), - "unexpected adjacent vertex {j} of vertex {:?}", - vertices[i] - ); - } - assert_eq!( - iter.next(), - None, - "too many adjacent vertices of vertex {:?} from iterator", - vertices[i] - ); - } - } - - #[test] - fn incident_vertices_single_edge() { - use $crate::traits::GraphTopology; - let (graph, vertices, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); - let (u, v) = graph.incident_vertices(e); - assert!( - (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), - "unexpected incident vertices {u:?} and {v:?} for edge {e:?}" - ); - } - - #[test] - fn incident_vertices_loop_edge() { - use $crate::traits::GraphTopology; - let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - assert_eq!( - graph.incident_vertices(e), - (v, v), - "unexpected incident vertices for loop edge {e:?}" - ); - } - - #[test] - fn incident_vertices_multiple_edges() { - use $crate::traits::GraphTopology; - let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::<2>(); - assert_ne!(edges[0], edges[1], "edges should be distinct"); - let (u, v) = graph.incident_vertices(edges[0]); - assert!( - (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), - "unexpected incident vertices {u:?} and {v:?} for first multi-edge {:?}", - edges[0] - ); - let (u, v) = graph.incident_vertices(edges[1]); - assert!( - (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), - "unexpected incident vertices {u:?} and {v:?} for second multi-edge {:?}", - edges[1] - ); - } - - #[test] - fn incident_vertices() { - use $crate::traits::GraphTopology; - let (graph, _, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for &(e, v1, v2) in edges.iter() { - let (u1, u2) = graph.incident_vertices(e); - assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {e:?}" - ); - } - } - - #[test] - fn edges_empty() { - use $crate::traits::GraphTopology; - let graph: $T = $crate::testing::fixtures::MakeTestGraph::empty(); - assert_eq!( - graph.edges().count(), - 0, - "edge iterator of empty graph should have no elements" - ); - } - - #[test] - fn edges() { - use $crate::traits::GraphTopology; - let (graph, _, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!(graph.edges().count(), 18, "unexpected edge count"); - - // Expects each edge to appear exactly once. - for e in graph.edges() { - assert_eq!( - edges.iter().filter(|&&(f, _, _)| f == e).count(), - 1, - "unexpected edge {e:?} from iterator" - ); - } - } - - #[test] - fn incident_edges_empty() { - use $crate::traits::GraphTopology; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - graph.incident_edges(v).count(), - 0, - "incident edge iterator of vertex with degree 0 should have no elements" - ); - } - - #[test] - fn incident_edges() { - use $crate::traits::GraphTopology; - let (graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for i in 0..10 { - assert_eq!( - graph.incident_edges(vertices[i]).count(), - incidences[i].len(), - "unexpected incident edge count for vertex {:?}", - vertices[i] - ); - let mut expected: Vec<_> = incidences[i].iter().map(|x| x.edge).collect(); - for e in graph.incident_edges(vertices[i]) { - let pos = expected - .iter() - .position(|f| *f == e) - .expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} from iterator", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incident edges {:?} of vertex {:?} were not matched by iterator", - expected, - vertices[i] - ); - } - } - - #[test] - fn incident_edges_loop_edge() { - use $crate::traits::GraphTopology; - let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - let mut iter = graph.incident_edges(v); - assert_eq!(iter.next(), Some(e), "loop edge should appear in incident edges"); - assert_eq!(iter.next(), Some(e), "loop edge should appear twice in incident edges"); - assert_eq!(iter.next(), None, "too many incident edges from iterator"); - } - - #[test] - fn incident_edges_multiple_edges() { - use $crate::traits::GraphTopology; - const K: usize = 3; - let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); - for i in 0..2 { - let mut expected = edges.to_vec(); - for e in graph.incident_edges(vertices[i]) { - let pos = expected - .iter() - .position(|f| *f == e) - .expect(&format!( - "unexpected incident edge {e:?} of vertex {:?}", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incident edges {:?} of vertex {:?} were not matched by iterator", - expected, - vertices[i] - ); - } - } - - #[test] - fn incidences_empty() { - use $crate::traits::GraphTopology; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!( - graph.incidences(v).count(), - 0, - "incidence iterator of vertex with degree 0 should have no elements" - ); - } - - #[test] - fn incidences() { - use $crate::traits::GraphTopology; - let (graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for i in 0..10 { - assert_eq!( - graph.incidences(vertices[i]).count(), - incidences[i].len(), - "unexpected incidence count for vertex {:?}", - vertices[i] - ); - let mut expected = incidences[i].clone(); - for incidence in graph.incidences(vertices[i]) { - let pos = expected - .iter() - .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from iterator", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incidences {:?} of vertex {:?} were not matched by iterator", - expected, - vertices[i] - ); - } - } - - #[test] - fn incidences_edge_consistency() { - use $crate::traits::GraphTopology; - // For each incidence (v, e) of u, the same edge e must appear in the incidences of v. - // For loop edges (u == v), the edge must appear exactly twice in the incidences of u. - let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for u in graph.vertices() { - for incidence in graph.incidences(u) { - if u == incidence.vertex { - assert_eq!( - graph.incidences(u).filter(|x| x.edge == incidence.edge).count(), - 2, - "loop edge {:?} should appear exactly twice in incidences of vertex {u:?}", - incidence.edge - ); - } else { - assert!( - graph - .incidences(incidence.vertex) - .any(|x| x.vertex == u && x.edge == incidence.edge), - "edge {:?} from incidences of vertex {u:?} missing in incidences of adjacent vertex {:?}", - incidence.edge, incidence.vertex - ); - } - } - } - } - - #[test] - fn incidences_loop_edge() { - use $crate::traits::{GraphTopology, Incidence}; - let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - let mut iter = graph.incidences(v); +pub(crate) fn adjacent_vertices_multiple_edges() +where + G::Vertex: Debug, +{ + const K: usize = 3; + let (graph, vertices, _) = G::multiple_edges::(); + for i in 0..2 { + let mut iter = graph.adjacent_vertices(vertices[i]); + for j in 0..K { assert_eq!( iter.next(), - Some(Incidence { vertex: v, edge: e }), - "vertex should be adjacent to itself" - ); - assert_eq!( - iter.next(), - Some(Incidence { vertex: v, edge: e }), - "vertex should be adjacent to itself twice" - ); - assert_eq!(iter.next(), None, "too many adjacent vertices from iterator"); - } - - #[test] - fn incidences_multiple_edges() { - use $crate::traits::GraphTopology; - const K: usize = 3; - let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); - for i in 0..2 { - let mut iter = graph.incidences(vertices[i]); - for j in 0..K { - let current = iter.next().expect(&format!( - "incidence {j} missing, expected {K} incidences for vertex {:?}", - vertices[i] - )); - assert_eq!( - current.vertex, - vertices[1 - i], - "unexpected adjacent vertex of vertex {:?} in incidence {j}", - vertices[i] - ); - assert_eq!( - edges.iter().filter(|e| **e == current.edge).count(), - 1, - "unexpected incident edge {:?} of vertex {:?}", - current.edge, - vertices[i], - ); - } - assert_eq!( - iter.next(), - None, - "too many adjacent vertices of {:?} from iterator", - vertices[i] - ); - } - } - - #[test] - fn incidence_cursor_empty() { - use $crate::traits::{GraphTopology, IncidenceCursor}; - let (graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - let mut cursor = graph.incidence_cursor(v); - assert_eq!( - cursor.next(&graph), - None, - "incidence cursor of vertex with degree 0 should immediately be exhausted" + Some(vertices[1 - i]), + "unexpected adjacent vertex {j} of vertex {:?}", + vertices[i] ); } - - #[test] - fn incidence_cursor() { - use $crate::traits::{GraphTopology, IncidenceCursor}; - let (graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for i in 0..10 { - let mut expected = incidences[i].clone(); - let mut cursor = graph.incidence_cursor(vertices[i]); - while let Some(incidence) = cursor.next(&graph) { - let pos = expected - .iter() - .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from cursor", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incidences {:?} of vertex {:?} were not matched by cursor", - expected, - vertices[i] - ); - } - } - - #[test] - fn incidence_cursor_loop_edge() { - use $crate::traits::{GraphTopology, Incidence, IncidenceCursor}; - let (graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - let mut cursor = graph.incidence_cursor(v); - assert_eq!( - cursor.next(&graph), - Some(Incidence { vertex: v, edge: e }), - "vertex should be adjacent to itself" - ); - assert_eq!( - cursor.next(&graph), - Some(Incidence { vertex: v, edge: e }), - "vertex should be adjacent to itself twice" - ); - assert_eq!(cursor.next(&graph), None, "too many incidences from cursor"); - } - - #[test] - fn incidence_cursor_multiple_edges() { - use $crate::traits::{GraphTopology, IncidenceCursor}; - const K: usize = 3; - let (graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); - for i in 0..2 { - let mut cursor = graph.incidence_cursor(vertices[i]); - for j in 0..K { - let current = cursor.next(&graph).expect(&format!( - "incidence {j} missing, expected {K} incidences for vertex {:?}", - vertices[i] - )); - assert_eq!( - current.vertex, - vertices[1 - i], - "unexpected adjacent vertex of vertex {:?} in incidence {j}", - vertices[i] - ); - assert_eq!( - edges.iter().filter(|e| **e == current.edge).count(), - 1, - "unexpected incident edge {:?} of vertex {:?}", - current.edge, - vertices[i], - ); - } - assert_eq!( - cursor.next(&graph), - None, - "too many incidences of {:?} from cursor", - vertices[i] - ); - } - } - - #[test] - fn incidence_cursor_copy() { - use $crate::traits::{GraphTopology, IncidenceCursor}; - let (graph, vertices, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::two_edge_path(); - // Traverses incidences of the middle vertex. - let mut c1 = graph.incidence_cursor(vertices[1]); - assert!(c1.next(&graph).is_some(), "expected first incidence"); - // Copies cursor mid-traversal. - let mut c2 = c1; - // Continues iteration with original cursor. - assert!(c1.next(&graph).is_some(), "expected second incidence from original cursor"); - assert!(c1.next(&graph).is_none(), "expected original cursor to be exhausted"); - // Replays from the copy point with the copied cursor. - assert!(c2.next(&graph).is_some(), "expected second incidence from copied cursor"); - assert!(c2.next(&graph).is_none(), "expected copied cursor to be exhausted"); - } - - #[test] - fn incident_vertices_incidences_consistency() { - use $crate::traits::GraphTopology; - let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for u in graph.vertices() { - for incidence in graph.incidences(u) { - let (w1, w2) = graph.incident_vertices(incidence.edge); - assert!( - (w1 == u && w2 == incidence.vertex) || (w1 == incidence.vertex && w2 == u), - "edge endpoints {w1:?} and {w2:?} are inconsistent with incidence ({:?}, {:?}) of vertex {u:?}", - incidence.vertex, incidence.edge - ); - } - } - } - - #[test] - fn incident_edges_incidences_consistency() { - use $crate::traits::GraphTopology; - let (graph, _, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - for u in graph.vertices() { - let mut expected: Vec<_> = graph.incidences(u).map(|x| x.edge).collect(); - for e in graph.incident_edges(u) { - let pos = expected - .iter() - .position(|f| *f == e) - .expect(&format!( - "incident edge {e:?} of vertex {u:?} missing from incidences" - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "incidence edges {:?} of vertex {u:?} not matched by incident_edges", - expected - ); - } - } - }; + assert_eq!( + iter.next(), + None, + "too many adjacent vertices of vertex {:?} from iterator", + vertices[i] + ); + } } -#[doc(hidden)] -#[macro_export] -macro_rules! graph_topology_addition_tests { - ($T:ty) => { - #[test] - fn reserve_vertices_increases_capacity() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let capacity_before = graph.vertex_capacity(); - graph.reserve_vertices(capacity_before + 10); - assert!( - graph.vertex_capacity() > capacity_before, - "expected sufficient capacity increase after reserve" - ); - } - - #[test] - fn reserve_vertices_prevents_reallocation_on_add() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - graph.reserve_vertices(10); - let capacity_before = graph.vertex_capacity(); - for _ in 0..10 { - graph.add_vertex(); - } - assert_eq!( - graph.vertex_capacity(), - capacity_before, - "writes within reserved capacity should not reallocate" - ); - } - - #[test] - fn reserve_vertices_does_not_affect_edge_capacity() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let capacity_before = graph.edge_capacity(); - graph.reserve_vertices(10); - assert_eq!( - graph.edge_capacity(), - capacity_before, - "reserve_vertices must not change edge capacity" - ); - } - - #[test] - fn reserve_edges_increases_capacity() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let capacity_before = graph.edge_capacity(); - graph.reserve_edges(capacity_before + 10); - assert!( - graph.edge_capacity() > capacity_before, - "expected sufficient capacity increase after reserve" - ); - } - - #[test] - fn reserve_edges_prevents_reallocation_on_add() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - graph.reserve_edges(10); - let capacity_before = graph.edge_capacity(); - for _ in 0..10 { - graph.add_edge(v1, v2); - } - assert_eq!( - graph.edge_capacity(), - capacity_before, - "writes within reserved capacity should not reallocate" - ); - } - - #[test] - fn reserve_edges_does_not_affect_vertex_capacity() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let capacity_before = graph.vertex_capacity(); - graph.reserve_edges(10); - assert_eq!( - graph.vertex_capacity(), - capacity_before, - "reserve_edges must not change vertex capacity" - ); - } - - #[test] - fn add_vertex() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let v = graph.add_vertex(); - assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); - } - - #[test] - fn add_edge() { - use $crate::traits::GraphTopologyAddition; - let mut graph = <$T>::default(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); - } - - #[test] - fn vertex_map_new_vertex() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::default(); - let mut map = graph.vertex_map(27); - let v = graph.add_vertex(); - assert_eq!( - map[v], 27, - "unexpected value from default map read for new vertex" - ); - map[v] = 9; - assert_eq!( - map[v], 9, - "unexpected value from map read after write for new vertex" - ); - } - - #[test] - fn edge_map_new_edge() { - use $crate::traits::{GraphTopology, GraphTopologyAddition}; - let mut graph = <$T>::default(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let mut map = graph.edge_map(28); - let e = graph.add_edge(v1, v2); - assert_eq!(map[e], 28,); - map[e] = 8; - assert_eq!( - map[e], 8, - "unexpected value from map read after write for new edge" - ); - } - }; +pub(crate) fn incident_vertices_single_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, e) = G::single_edge(); + let (u, v) = graph.incident_vertices(e); + assert!( + (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), + "unexpected incident vertices {u:?} and {v:?} for edge {e:?}" + ); } -#[doc(hidden)] -#[macro_export] -macro_rules! graph_topology_deletion_tests { - ($T:ty) => { - #[test] - fn delete_vertex() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - assert_eq!(graph.vertex_count(), 1, "unexpected vertex count before delete"); - graph.delete_vertex(v); - assert_eq!(graph.vertex_count(), 0, "unexpected vertex count after delete"); - } - - #[test] - fn delete_vertex_loop() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, v, _): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - assert_eq!(graph.vertex_count(), 1, "unexpected vertex count before delete"); - assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); - assert!( - graph.are_adjacent(v, v), - "expected vertex to be self-adjacent before delete" - ); - assert_eq!(graph.degree(v), 2, "unexpected vertex degree before delete"); - graph.delete_vertex(v); - assert_eq!(graph.vertex_count(), 0, "unexpected vertex count after delete"); - assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); - } - - #[test] - fn delete_vertex_invalid_index() { - use $crate::traits::GraphTopologyDeletion; - let (mut graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - graph.delete_vertex(v); - let result = std::panic::catch_unwind(move || graph.delete_vertex(v)); - assert!(result.is_err(), "second deletion should panic"); - } - - #[test] - fn delete_vertex_connected() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - assert_eq!(graph.vertex_count(), 10, "unexpected vertex count before delete"); - assert_eq!(graph.edge_count(), 18, "unexpected edge count before delete"); - graph.delete_vertex(vertices[2]); - assert_eq!(graph.vertex_count(), 9, "unexpected vertex count after delete"); - assert_eq!(graph.edge_count(), 12, "unexpected edge count after delete"); - let expected_edges = [ - (vertices[0], vertices[1]), - (vertices[1], vertices[3]), - (vertices[1], vertices[4]), - (vertices[3], vertices[6]), - (vertices[4], vertices[4]), - (vertices[4], vertices[7]), - (vertices[4], vertices[8]), - (vertices[5], vertices[9]), - (vertices[6], vertices[9]), - (vertices[7], vertices[8]), - (vertices[7], vertices[9]), - ]; - for (v, u) in expected_edges { - assert!( - graph.are_adjacent(v, u), - "expected {v:?} and {u:?} to be adjacent after delete" - ); - } - for v in [vertices[1], vertices[4], vertices[5], vertices[6]] { - assert!( - !graph.adjacent_vertices(v).any(|u| u == vertices[2]), - "unexpected adjacency of {v:?} to deleted vertex {:?}", - vertices[2] - ); - } - } - - #[test] - fn delete_edge() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, vertices, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); - assert_eq!(graph.vertex_count(), 2, "unexpected vertex count before delete"); - assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); - assert!( - graph.are_adjacent(vertices[0], vertices[1]), - "expected vertices to be adjacent before delete" - ); - assert_eq!(graph.degree(vertices[0]), 1, "unexpected vertex degree before delete"); - assert_eq!(graph.degree(vertices[1]), 1, "unexpected vertex degree before delete"); - graph.delete_edge(e); - assert_eq!(graph.vertex_count(), 2, "unexpected vertex count after delete"); - assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); - assert!( - !graph.are_adjacent(vertices[0], vertices[1]), - "unexpected adjacency after delete" - ); - assert_eq!(graph.degree(vertices[0]), 0, "unexpected vertex degree after delete"); - assert_eq!(graph.degree(vertices[1]), 0, "unexpected vertex degree after delete"); - } - - #[test] - fn delete_edge_loop() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, v, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::loop_edge(); - assert_eq!(graph.vertex_count(), 1, "unexpected vertex count before delete"); - assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); - assert!(graph.are_adjacent(v, v), "expected vertex to be self-adjacent before delete"); - assert_eq!(graph.degree(v), 2, "unexpected vertex degree before delete"); - graph.delete_edge(e); - assert_eq!(graph.vertex_count(), 1, "unexpected vertex count after delete"); - assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); - assert!(!graph.are_adjacent(v, v), "unexpected adjacency after delete"); - assert_eq!(graph.degree(v), 0, "unexpected vertex degree after delete"); - } - - #[test] - fn delete_edge_multiple() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - const K: usize = 2; - let (mut graph, vertices, edges): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::multiple_edges::(); - assert_eq!(graph.vertex_count(), 2, "unexpected vertex count before delete"); - assert_eq!(graph.edge_count(), K, "unexpected edge count before delete"); - assert!( - graph.are_adjacent(vertices[0], vertices[1]), - "expected vertices to be adjacent before delete" - ); - assert_eq!(graph.degree(vertices[0]), K, "unexpected vertex degree before delete"); - assert_eq!(graph.degree(vertices[1]), K, "unexpected vertex degree before delete"); - graph.delete_edge(edges[0]); - assert_eq!(graph.vertex_count(), 2, "unexpected vertex count after delete"); - assert_eq!(graph.edge_count(), K - 1, "unexpected edge count after delete"); - assert!( - graph.are_adjacent(vertices[0], vertices[1]), - "expected vertices to be adjacent after delete" - ); - assert_eq!(graph.degree(vertices[0]), K - 1, "unexpected vertex degree after delete"); - assert_eq!(graph.degree(vertices[1]), K - 1, "unexpected vertex degree after delete"); - } - - #[test] - fn delete_edge_invalid_index() { - use $crate::traits::GraphTopologyDeletion; - let (mut graph, _, e): ($T, _, _) = $crate::testing::fixtures::MakeTestGraph::single_edge(); - graph.delete_edge(e); - let result = std::panic::catch_unwind(move || graph.delete_edge(e)); - assert!(result.is_err(), "second deletion should panic"); - } - - #[test] - fn vertices_after_delete() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, vertices, _, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - graph.delete_vertex(vertices[2]); - assert_eq!(graph.vertex_count(), 9, "unexpected vertex count after delete"); - assert_eq!( - graph.vertices().count(), - 9, - "unexpected vertex iterator count after delete" - ); - for v in graph.vertices() { - assert_ne!( - v, vertices[2], - "deleted vertex {:?} appeared in iterator", - vertices[2] - ); - } - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - assert!( - graph.vertices().any(|v| v == vertices[i]), - "expected vertex {:?} missing from iterator after delete", - vertices[i] - ); - } - } - - #[test] - fn incident_vertices_after_delete_edge() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, _, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - let e = edges[2].0; - graph.delete_edge(e); - for &(f, v1, v2) in edges.iter().filter(|&&(f, _, _)| f != e) { - let (u1, u2) = graph.incident_vertices(f); - assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {f:?} after delete" - ); - } - } - - #[test] - fn edges_after_delete() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, vertices, edges, _): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - graph.delete_vertex(vertices[2]); - assert_eq!(graph.edge_count(), 12, "unexpected edge count after delete"); - assert_eq!(graph.edges().count(), 12, "unexpected edge iterator count after delete"); - for i in [2, 5, 6, 7, 8, 9] { - assert!( - !graph.edges().any(|e| e == edges[i].0), - "deleted edge {:?} appeared in iterator", - edges[i].0 - ); - } - for i in [0, 1, 3, 4, 10, 11, 12, 13, 14, 15, 16, 17] { - assert!( - graph.edges().any(|e| e == edges[i].0), - "expected edge {:?} missing from iterator after delete", - edges[i].0 - ); - } - } - - #[test] - fn incident_edges_after_delete_edge() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, vertices, edges, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - graph.delete_edge(edges[2].0); - for i in 0..10 { - let mut expected: Vec<_> = incidences[i] - .iter() - .filter_map(|x| (x.edge != edges[2].0).then_some(x.edge)) - .collect(); - assert_eq!( - graph.incident_edges(vertices[i]).count(), - expected.len(), - "unexpected incident edge count for vertex {:?} after delete", - vertices[i] - ); - for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} after delete", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incident edges {:?} of vertex {:?} not matched after delete", - expected, - vertices[i] - ); - } - } - - #[test] - fn incident_edges_after_delete_vertex() { - use $crate::traits::{GraphTopology, GraphTopologyDeletion}; - let (mut graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - graph.delete_vertex(vertices[2]); - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - let mut expected: Vec<_> = incidences[i] - .iter() - .filter_map(|x| (x.vertex != vertices[2]).then_some(x.edge)) - .collect(); - assert_eq!( - graph.incident_edges(vertices[i]).count(), - expected.len(), - "unexpected incident edge count for vertex {:?} after delete", - vertices[i] - ); - for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} after delete", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incident edges {:?} of vertex {:?} not matched after delete", - expected, - vertices[i] - ); - } - } - - #[test] - fn incidences_after_delete_vertex() { - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - graph.delete_vertex(vertices[2]); - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - let remaining = incidences[i] - .iter() - .filter(|x| x.vertex != vertices[2]) - .cloned() - .collect(); - assert_vertex_incidences(&graph, vertices[i], remaining); - } - } - - #[test] - fn incidences_after_delete_edge() { - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, edges, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - // Deletes the edge from vertices[1] to vertices[2]. - graph.delete_edge(edges[2].0); - for i in 0..10 { - let remaining = incidences[i] - .iter() - .filter(|x| x.edge != edges[2].0) - .cloned() - .collect(); - assert_vertex_incidences(&graph, vertices[i], remaining); - } - } - - // TODO: Move out of macro. - fn assert_vertex_incidences( - graph: &$T, - v: <$T as $crate::traits::GraphTopology>::Vertex, - mut expected: Vec<$crate::traits::Incidence< - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Edge, - >>, - ) { - use $crate::traits::GraphTopology; - assert_eq!( - graph.incidences(v).count(), - expected.len(), - "unexpected incidence count for vertex {:?} after delete", - v - ); - for incidence in graph.incidences(v) { - let pos = expected - .iter() - .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from iterator after delete", - v - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incidences {:?} of vertex {:?} not matched by iterator after delete", - expected, - v - ); - } - - #[test] - fn incidence_cursor_after_delete_vertex() { - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, _, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - graph.delete_vertex(vertices[2]); - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - let remaining = incidences[i] - .iter() - .filter(|x| x.vertex != vertices[2]) - .cloned() - .collect(); - assert_vertex_incidence_cursor(&graph, vertices[i], remaining); - } - } - - #[test] - fn incidence_cursor_after_delete_edge() { - use $crate::traits::GraphTopologyDeletion; - let (mut graph, vertices, edges, incidences): ($T, _, _, _) = $crate::testing::fixtures::MakeTestGraph::standard(); - // Deletes the edge from vertices[1] to vertices[2]. - graph.delete_edge(edges[2].0); - for i in 0..10 { - let remaining = incidences[i] - .iter() - .filter(|x| x.edge != edges[2].0) - .cloned() - .collect(); - assert_vertex_incidence_cursor(&graph, vertices[i], remaining); - } - } - - // TODO: Move out of macro. - fn assert_vertex_incidence_cursor( - graph: &$T, - v: <$T as $crate::traits::GraphTopology>::Vertex, - mut expected: Vec<$crate::traits::Incidence< - <$T as $crate::traits::GraphTopology>::Vertex, - <$T as $crate::traits::GraphTopology>::Edge, - >>, - ) { - use $crate::traits::{GraphTopology, IncidenceCursor}; - let mut cursor = graph.incidence_cursor(v); - while let Some(incidence) = cursor.next(graph) { - let pos = expected - .iter() - .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from cursor after delete", - v - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incidences {:?} of vertex {:?} not matched by cursor after delete", - expected, - v - ); - } - }; +pub(crate) fn incident_vertices_loop_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, v, e) = G::loop_edge(); + assert_eq!( + graph.incident_vertices(e), + (v, v), + "unexpected incident vertices for loop edge {e:?}" + ); } -#[doc(hidden)] -#[macro_export] -macro_rules! graph_topology_addition_deletion_tests { - ($T:ty) => { - #[test] - fn delete_vertex_add_vertex() { - use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; - let (mut graph, v): ($T, _) = $crate::testing::fixtures::MakeTestGraph::single_vertex(); - graph.delete_vertex(v); +pub(crate) fn incident_vertices_multiple_edges() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, edges) = G::multiple_edges::<2>(); + assert_ne!(edges[0], edges[1], "edges should be distinct"); + let (u, v) = graph.incident_vertices(edges[0]); + assert!( + (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), + "unexpected incident vertices {u:?} and {v:?} for first multi-edge {:?}", + edges[0] + ); + let (u, v) = graph.incident_vertices(edges[1]); + assert!( + (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), + "unexpected incident vertices {u:?} and {v:?} for second multi-edge {:?}", + edges[1] + ); +} + +pub(crate) fn incident_vertices() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, _, edges, _) = G::standard(); + for &(e, v1, v2) in edges.iter() { + let (u1, u2) = graph.incident_vertices(e); + assert!( + (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), + "unexpected incident vertices {u1:?} and {u2:?} for edge {e:?}" + ); + } +} + +pub(crate) fn edges_empty() { + let graph = G::empty(); + assert_eq!( + graph.edges().count(), + 0, + "edge iterator of empty graph should have no elements" + ); +} + +pub(crate) fn edges() +where + G::Edge: Debug, +{ + let (graph, _, edges, _) = G::standard(); + assert_eq!(graph.edges().count(), 18, "unexpected edge count"); + for e in graph.edges() { + assert_eq!( + edges.iter().filter(|&&(f, _, _)| f == e).count(), + 1, + "unexpected edge {e:?} from iterator" + ); + } +} + +pub(crate) fn incident_edges_empty() { + let (graph, v) = G::single_vertex(); + assert_eq!( + graph.incident_edges(v).count(), + 0, + "incident edge iterator of vertex with degree 0 should have no elements" + ); +} + +pub(crate) fn incident_edges() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, incidences) = G::standard(); + for i in 0..10 { + assert_eq!( + graph.incident_edges(vertices[i]).count(), + incidences[i].len(), + "unexpected incident edge count for vertex {:?}", + vertices[i] + ); + let mut expected: Vec<_> = incidences[i].iter().map(|x| x.edge).collect(); + for e in graph.incident_edges(vertices[i]) { + let pos = expected.iter().position(|f| *f == e).expect(&format!( + "unexpected incident edge {e:?} of vertex {:?} from iterator", + vertices[i] + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incident edges {:?} of vertex {:?} were not matched by iterator", + expected, + vertices[i] + ); + } +} + +pub(crate) fn incident_edges_loop_edge() +where + G::Edge: Debug, +{ + let (graph, v, e) = G::loop_edge(); + let mut iter = graph.incident_edges(v); + assert_eq!( + iter.next(), + Some(e), + "loop edge should appear in incident edges" + ); + assert_eq!( + iter.next(), + Some(e), + "loop edge should appear twice in incident edges" + ); + assert_eq!(iter.next(), None, "too many incident edges from iterator"); +} + +pub(crate) fn incident_edges_multiple_edges() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + const K: usize = 3; + let (graph, vertices, edges) = G::multiple_edges::(); + for i in 0..2 { + let mut expected = edges.to_vec(); + for e in graph.incident_edges(vertices[i]) { + let pos = expected.iter().position(|f| *f == e).expect(&format!( + "unexpected incident edge {e:?} of vertex {:?}", + vertices[i] + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incident edges {:?} of vertex {:?} were not matched by iterator", + expected, + vertices[i] + ); + } +} + +pub(crate) fn incidences_empty() { + let (graph, v) = G::single_vertex(); + assert_eq!( + graph.incidences(v).count(), + 0, + "incidence iterator of vertex with degree 0 should have no elements" + ); +} + +pub(crate) fn incidences() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, incidences) = G::standard(); + for i in 0..10 { + assert_eq!( + graph.incidences(vertices[i]).count(), + incidences[i].len(), + "unexpected incidence count for vertex {:?}", + vertices[i] + ); + let mut expected = incidences[i].clone(); + for incidence in graph.incidences(vertices[i]) { + let pos = expected + .iter() + .position(|x| *x == incidence) + .expect(&format!( + "unexpected incidence {incidence:?} of vertex {:?} from iterator", + vertices[i] + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incidences {:?} of vertex {:?} were not matched by iterator", + expected, + vertices[i] + ); + } +} + +pub(crate) fn incidences_edge_consistency() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, _, _, _) = G::standard(); + for u in graph.vertices() { + for incidence in graph.incidences(u) { + if u == incidence.vertex { + assert_eq!( + graph + .incidences(u) + .filter(|x| x.edge == incidence.edge) + .count(), + 2, + "loop edge {:?} should appear exactly twice in incidences of vertex {u:?}", + incidence.edge + ); + } else { + assert!( + graph + .incidences(incidence.vertex) + .any(|x| x.vertex == u && x.edge == incidence.edge), + "edge {:?} from incidences of vertex {u:?} missing in incidences of \ + adjacent vertex {:?}", + incidence.edge, + incidence.vertex + ); + } + } + } +} + +pub(crate) fn incidences_loop_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, v, e) = G::loop_edge(); + let mut iter = graph.incidences(v); + assert_eq!( + iter.next(), + Some(Incidence { vertex: v, edge: e }), + "vertex should be adjacent to itself" + ); + assert_eq!( + iter.next(), + Some(Incidence { vertex: v, edge: e }), + "vertex should be adjacent to itself twice" + ); + assert_eq!( + iter.next(), + None, + "too many adjacent vertices from iterator" + ); +} + +pub(crate) fn incidences_multiple_edges() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + const K: usize = 3; + let (graph, vertices, edges) = G::multiple_edges::(); + for i in 0..2 { + let mut iter = graph.incidences(vertices[i]); + for j in 0..K { + let current = iter.next().expect(&format!( + "incidence {j} missing, expected {K} incidences for vertex {:?}", + vertices[i] + )); assert_eq!( - graph.vertex_count(), - 0, - "unexpected vertex count after delete" - ); - assert_ne!( - graph.add_vertex(), - v, - "unexpected duplicate vertex after delete" + current.vertex, + vertices[1 - i], + "unexpected adjacent vertex of vertex {:?} in incidence {j}", + vertices[i] ); assert_eq!( - graph.vertex_count(), + edges.iter().filter(|e| **e == current.edge).count(), 1, - "unexpected vertex count after re-add" + "unexpected incident edge {:?} of vertex {:?}", + current.edge, + vertices[i], ); } - - #[test] - fn delete_edge_add_edge() { - use $crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyDeletion}; - let (mut graph, vertices, e): ($T, _, _) = - $crate::testing::fixtures::MakeTestGraph::single_edge(); - graph.delete_edge(e); - assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); - assert_ne!( - graph.add_edge(vertices[0], vertices[1]), - e, - "unexpected duplicate edge after re-add" - ); - assert_eq!(graph.edge_count(), 1, "unexpected edge count after re-add"); - } - }; + assert_eq!( + iter.next(), + None, + "too many adjacent vertices of {:?} from iterator", + vertices[i] + ); + } +} + +pub(crate) fn incidence_cursor_empty() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + let mut cursor = graph.incidence_cursor(v); + assert_eq!( + cursor.next(&graph), + None, + "incidence cursor of vertex with degree 0 should immediately be exhausted" + ); +} + +pub(crate) fn incidence_cursor() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, incidences) = G::standard(); + for i in 0..10 { + let mut expected = incidences[i].clone(); + let mut cursor = graph.incidence_cursor(vertices[i]); + while let Some(incidence) = cursor.next(&graph) { + let pos = expected + .iter() + .position(|x| *x == incidence) + .expect(&format!( + "unexpected incidence {incidence:?} of vertex {:?} from cursor", + vertices[i] + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incidences {:?} of vertex {:?} were not matched by cursor", + expected, + vertices[i] + ); + } +} + +pub(crate) fn incidence_cursor_loop_edge() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, v, e) = G::loop_edge(); + let mut cursor = graph.incidence_cursor(v); + assert_eq!( + cursor.next(&graph), + Some(Incidence { vertex: v, edge: e }), + "vertex should be adjacent to itself" + ); + assert_eq!( + cursor.next(&graph), + Some(Incidence { vertex: v, edge: e }), + "vertex should be adjacent to itself twice" + ); + assert_eq!(cursor.next(&graph), None, "too many incidences from cursor"); +} + +pub(crate) fn incidence_cursor_multiple_edges() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + const K: usize = 3; + let (graph, vertices, edges) = G::multiple_edges::(); + for i in 0..2 { + let mut cursor = graph.incidence_cursor(vertices[i]); + for j in 0..K { + let current = cursor.next(&graph).expect(&format!( + "incidence {j} missing, expected {K} incidences for vertex {:?}", + vertices[i] + )); + assert_eq!( + current.vertex, + vertices[1 - i], + "unexpected adjacent vertex of vertex {:?} in incidence {j}", + vertices[i] + ); + assert_eq!( + edges.iter().filter(|e| **e == current.edge).count(), + 1, + "unexpected incident edge {:?} of vertex {:?}", + current.edge, + vertices[i], + ); + } + assert_eq!( + cursor.next(&graph), + None, + "too many incidences of {:?} from cursor", + vertices[i] + ); + } +} + +pub(crate) fn incidence_cursor_copy() { + let (graph, vertices, _) = G::two_edge_path(); + let mut c1 = graph.incidence_cursor(vertices[1]); + assert!(c1.next(&graph).is_some(), "expected first incidence"); + let mut c2 = c1; + assert!( + c1.next(&graph).is_some(), + "expected second incidence from original cursor" + ); + assert!( + c1.next(&graph).is_none(), + "expected original cursor to be exhausted" + ); + assert!( + c2.next(&graph).is_some(), + "expected second incidence from copied cursor" + ); + assert!( + c2.next(&graph).is_none(), + "expected copied cursor to be exhausted" + ); +} + +pub(crate) fn incident_vertices_incidences_consistency() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, _, _, _) = G::standard(); + for u in graph.vertices() { + for incidence in graph.incidences(u) { + let (w1, w2) = graph.incident_vertices(incidence.edge); + assert!( + (w1 == u && w2 == incidence.vertex) || (w1 == incidence.vertex && w2 == u), + "edge endpoints {w1:?} and {w2:?} are inconsistent with incidence \ + ({:?}, {:?}) of vertex {u:?}", + incidence.vertex, + incidence.edge + ); + } + } +} + +pub(crate) fn incident_edges_incidences_consistency() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, _, _, _) = G::standard(); + for u in graph.vertices() { + let mut expected: Vec<_> = graph.incidences(u).map(|x| x.edge).collect(); + for e in graph.incident_edges(u) { + let pos = expected.iter().position(|f| *f == e).expect(&format!( + "incident edge {e:?} of vertex {u:?} missing from incidences" + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "incidence edges {:?} of vertex {u:?} not matched by incident_edges", + expected + ); + } } -- 2.43.0 From 5825ef3b47f5fecab8ccc385da84cc0c0927de73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 2 Sep 2026 09:30:37 +0200 Subject: [PATCH 33/67] Change all graph tests to pub to avoid unused-warnings --- src/testing.rs | 8 +- ...raph_topology_addition_deletion_testing.rs | 5 +- .../graph_topology_addition_testing.rs | 21 ++--- .../graph_topology_deletion_testing.rs | 35 ++++---- src/testing/graph_topology_testing.rs | 83 ++++++++++--------- 5 files changed, 78 insertions(+), 74 deletions(-) diff --git a/src/testing.rs b/src/testing.rs index d730642..7b6b7d6 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,7 +1,7 @@ //! Test fixture and test macros for graph topology and algorithm implementations. pub mod fixtures; -pub(crate) mod graph_topology_addition_deletion_testing; -pub(crate) mod graph_topology_addition_testing; -pub(crate) mod graph_topology_deletion_testing; -pub(crate) mod graph_topology_testing; +pub mod graph_topology_addition_deletion_testing; +pub mod graph_topology_addition_testing; +pub mod graph_topology_deletion_testing; +pub mod graph_topology_testing; diff --git a/src/testing/graph_topology_addition_deletion_testing.rs b/src/testing/graph_topology_addition_deletion_testing.rs index 14fb7be..ce01b82 100644 --- a/src/testing/graph_topology_addition_deletion_testing.rs +++ b/src/testing/graph_topology_addition_deletion_testing.rs @@ -23,7 +23,7 @@ macro_rules! graph_topology_addition_deletion_tests { }; } -pub(crate) fn delete_vertex_add_vertex() +pub fn delete_vertex_add_vertex() where G::Vertex: Debug, { @@ -46,7 +46,7 @@ where ); } -pub(crate) fn delete_edge_add_edge() +pub fn delete_edge_add_edge() where G::Edge: Debug, { @@ -60,3 +60,4 @@ where ); assert_eq!(graph.edge_count(), 1, "unexpected edge count after re-add"); } + diff --git a/src/testing/graph_topology_addition_testing.rs b/src/testing/graph_topology_addition_testing.rs index 386fa8d..7123300 100644 --- a/src/testing/graph_topology_addition_testing.rs +++ b/src/testing/graph_topology_addition_testing.rs @@ -30,7 +30,7 @@ macro_rules! graph_topology_addition_tests { }; } -pub(crate) fn reserve_vertices_increases_capacity() { +pub fn reserve_vertices_increases_capacity() { let mut graph = G::default(); let capacity_before = graph.vertex_capacity(); graph.reserve_vertices(capacity_before + 10); @@ -40,7 +40,7 @@ pub(crate) fn reserve_vertices_increases_capacity() { ); } -pub(crate) fn reserve_vertices_prevents_reallocation_on_add() { +pub fn reserve_vertices_prevents_reallocation_on_add() { let mut graph = G::default(); graph.reserve_vertices(10); let capacity_before = graph.vertex_capacity(); @@ -54,7 +54,7 @@ pub(crate) fn reserve_vertices_prevents_reallocation_on_add() { +pub fn reserve_vertices_does_not_affect_edge_capacity() { let mut graph = G::default(); let capacity_before = graph.edge_capacity(); graph.reserve_vertices(10); @@ -65,7 +65,7 @@ pub(crate) fn reserve_vertices_does_not_affect_edge_capacity() { +pub fn reserve_edges_increases_capacity() { let mut graph = G::default(); let capacity_before = graph.edge_capacity(); graph.reserve_edges(capacity_before + 10); @@ -75,7 +75,7 @@ pub(crate) fn reserve_edges_increases_capacity() { ); } -pub(crate) fn reserve_edges_prevents_reallocation_on_add() { +pub fn reserve_edges_prevents_reallocation_on_add() { let mut graph = G::default(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -91,7 +91,7 @@ pub(crate) fn reserve_edges_prevents_reallocation_on_add() { +pub fn reserve_edges_does_not_affect_vertex_capacity() { let mut graph = G::default(); let capacity_before = graph.vertex_capacity(); graph.reserve_edges(10); @@ -102,7 +102,7 @@ pub(crate) fn reserve_edges_does_not_affect_vertex_capacity() +pub fn add_vertex() where G::Vertex: Debug, { @@ -111,7 +111,7 @@ where assert_ne!(graph.add_vertex(), v, "unexpected duplicate vertex"); } -pub(crate) fn add_edge() +pub fn add_edge() where G::Edge: Debug, { @@ -122,7 +122,7 @@ where assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); } -pub(crate) fn vertex_map_new_vertex() { +pub fn vertex_map_new_vertex() { let mut graph = G::default(); let mut map = graph.vertex_map(27); let v = graph.add_vertex(); @@ -137,7 +137,7 @@ pub(crate) fn vertex_map_new_vertex() { ); } -pub(crate) fn edge_map_new_edge() { +pub fn edge_map_new_edge() { let mut graph = G::default(); let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); @@ -150,3 +150,4 @@ pub(crate) fn edge_map_new_edge() { "unexpected value from map read after write for new edge" ); } + diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs index d530df7..1f1f684 100644 --- a/src/testing/graph_topology_deletion_testing.rs +++ b/src/testing/graph_topology_deletion_testing.rs @@ -40,7 +40,7 @@ macro_rules! graph_topology_deletion_tests { }; } -pub(crate) fn delete_vertex() { +pub fn delete_vertex() { let (mut graph, v) = G::single_vertex(); assert_eq!( graph.vertex_count(), @@ -55,7 +55,7 @@ pub(crate) fn delete_vertex() ); } -pub(crate) fn delete_vertex_loop() { +pub fn delete_vertex_loop() { let (mut graph, v, _) = G::loop_edge(); assert_eq!( graph.vertex_count(), @@ -77,7 +77,7 @@ pub(crate) fn delete_vertex_loop() { +pub fn delete_vertex_invalid_index() { let (mut graph, v) = G::single_vertex(); graph.delete_vertex(v); let result = @@ -85,7 +85,7 @@ pub(crate) fn delete_vertex_invalid_index() +pub fn delete_vertex_connected() where G::Vertex: Debug, { @@ -135,7 +135,7 @@ where } } -pub(crate) fn delete_edge() { +pub fn delete_edge() { let (mut graph, vertices, e) = G::single_edge(); assert_eq!( graph.vertex_count(), @@ -180,7 +180,7 @@ pub(crate) fn delete_edge() { ); } -pub(crate) fn delete_edge_loop() { +pub fn delete_edge_loop() { let (mut graph, v, e) = G::loop_edge(); assert_eq!( graph.vertex_count(), @@ -207,7 +207,7 @@ pub(crate) fn delete_edge_loop assert_eq!(graph.degree(v), 0, "unexpected vertex degree after delete"); } -pub(crate) fn delete_edge_multiple() { +pub fn delete_edge_multiple() { const K: usize = 2; let (mut graph, vertices, edges) = G::multiple_edges::(); assert_eq!( @@ -257,7 +257,7 @@ pub(crate) fn delete_edge_multiple() { +pub fn delete_edge_invalid_index() { let (mut graph, _, e) = G::single_edge(); graph.delete_edge(e); let result = @@ -265,7 +265,7 @@ pub(crate) fn delete_edge_invalid_index() +pub fn vertices_after_delete() where G::Vertex: Debug, { @@ -297,7 +297,7 @@ where } } -pub(crate) fn incident_vertices_after_delete_edge< +pub fn incident_vertices_after_delete_edge< G: GraphTopologyDeletion + GraphTopologyAddition, >() where @@ -316,7 +316,7 @@ where } } -pub(crate) fn edges_after_delete() +pub fn edges_after_delete() where G::Edge: Debug, { @@ -344,7 +344,7 @@ where } } -pub(crate) fn incident_edges_after_delete_edge() +pub fn incident_edges_after_delete_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -378,7 +378,7 @@ where } } -pub(crate) fn incident_edges_after_delete_vertex() +pub fn incident_edges_after_delete_vertex() where G::Vertex: Debug, G::Edge: Debug, @@ -412,7 +412,7 @@ where } } -pub(crate) fn incidences_after_delete_vertex() +pub fn incidences_after_delete_vertex() where G::Vertex: Debug, G::Edge: Debug, @@ -429,7 +429,7 @@ where } } -pub(crate) fn incidences_after_delete_edge() +pub fn incidences_after_delete_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -446,7 +446,7 @@ where } } -pub(crate) fn incidence_cursor_after_delete_vertex< +pub fn incidence_cursor_after_delete_vertex< G: GraphTopologyDeletion + GraphTopologyAddition, >() where @@ -465,7 +465,7 @@ where } } -pub(crate) fn incidence_cursor_after_delete_edge() +pub fn incidence_cursor_after_delete_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -540,3 +540,4 @@ fn assert_vertex_incidence_cursor( v ); } + diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 1d45f83..cdb5dc8 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -62,17 +62,17 @@ macro_rules! graph_topology_tests { }; } -pub(crate) fn vertex_count_empty() { +pub fn vertex_count_empty() { let graph = G::empty(); assert_eq!(graph.vertex_count(), 0, "unexpected vertex count"); } -pub(crate) fn vertex_count() { +pub fn vertex_count() { let (graph, _, _, _) = G::standard(); assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); } -pub(crate) fn vertex_map() { +pub fn vertex_map() { let (graph, v) = G::single_vertex(); let mut map = graph.vertex_map(27); assert_eq!(map[v], 27, "unexpected value from default map read"); @@ -80,17 +80,17 @@ pub(crate) fn vertex_map() { assert_eq!(map[v], 9, "unexpected value from map after write"); } -pub(crate) fn edge_count_empty() { +pub fn edge_count_empty() { let graph = G::empty(); assert_eq!(graph.edge_count(), 0, "unexpected edge count"); } -pub(crate) fn edge_count() { +pub fn edge_count() { let (graph, _, _, _) = G::standard(); assert_eq!(graph.edge_count(), 18, "unexpected edge count"); } -pub(crate) fn edge_map() { +pub fn edge_map() { let (graph, _, e) = G::single_edge(); let mut map = graph.edge_map(28); assert_eq!(map[e], 28, "unexpected value from default map read"); @@ -98,12 +98,12 @@ pub(crate) fn edge_map() { assert_eq!(map[e], 8, "unexpected value from map after write"); } -pub(crate) fn degree_zero() { +pub fn degree_zero() { let (graph, v) = G::single_vertex(); assert_eq!(graph.degree(v), 0, "unexpected non-zero degree"); } -pub(crate) fn degree() +pub fn degree() where G::Vertex: Debug, { @@ -119,7 +119,7 @@ where } } -pub(crate) fn loop_edge() { +pub fn loop_edge() { let (graph, v, _) = G::loop_edge(); assert_eq!(graph.vertex_count(), 1, "unexpected vertex count"); assert_eq!(graph.edge_count(), 1, "unexpected edge count"); @@ -130,7 +130,7 @@ pub(crate) fn loop_edge() { ); } -pub(crate) fn multiple_edges() +pub fn multiple_edges() where G::Vertex: Debug, { @@ -156,7 +156,7 @@ where ); } -pub(crate) fn are_adjacent_vertex_self() { +pub fn are_adjacent_vertex_self() { let (graph, v) = G::single_vertex(); assert!( !graph.are_adjacent(v, v), @@ -164,7 +164,7 @@ pub(crate) fn are_adjacent_vertex_self() { ); } -pub(crate) fn are_adjacent_single_edge() { +pub fn are_adjacent_single_edge() { let (graph, vertices, _) = G::single_edge(); assert!( graph.are_adjacent(vertices[0], vertices[1]), @@ -176,7 +176,7 @@ pub(crate) fn are_adjacent_single_edge() { ); } -pub(crate) fn are_adjacent() +pub fn are_adjacent() where G::Vertex: Debug, { @@ -221,7 +221,7 @@ where } } -pub(crate) fn vertices_empty() { +pub fn vertices_empty() { let graph = G::empty(); assert_eq!( graph.vertices().count(), @@ -230,7 +230,7 @@ pub(crate) fn vertices_empty() { ); } -pub(crate) fn vertices() +pub fn vertices() where G::Vertex: Debug, { @@ -245,7 +245,7 @@ where } } -pub(crate) fn adjacent_vertices_empty() { +pub fn adjacent_vertices_empty() { let (graph, v) = G::single_vertex(); assert_eq!( graph.adjacent_vertices(v).count(), @@ -254,7 +254,7 @@ pub(crate) fn adjacent_vertices_empty() { ); } -pub(crate) fn adjacent_vertices() +pub fn adjacent_vertices() where G::Vertex: Debug, { @@ -292,7 +292,7 @@ where ); } -pub(crate) fn adjacent_vertices_loop_edge() +pub fn adjacent_vertices_loop_edge() where G::Vertex: Debug, { @@ -311,7 +311,7 @@ where ); } -pub(crate) fn adjacent_vertices_multiple_edges() +pub fn adjacent_vertices_multiple_edges() where G::Vertex: Debug, { @@ -336,7 +336,7 @@ where } } -pub(crate) fn incident_vertices_single_edge() +pub fn incident_vertices_single_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -349,7 +349,7 @@ where ); } -pub(crate) fn incident_vertices_loop_edge() +pub fn incident_vertices_loop_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -362,7 +362,7 @@ where ); } -pub(crate) fn incident_vertices_multiple_edges() +pub fn incident_vertices_multiple_edges() where G::Vertex: Debug, G::Edge: Debug, @@ -383,7 +383,7 @@ where ); } -pub(crate) fn incident_vertices() +pub fn incident_vertices() where G::Vertex: Debug, G::Edge: Debug, @@ -398,7 +398,7 @@ where } } -pub(crate) fn edges_empty() { +pub fn edges_empty() { let graph = G::empty(); assert_eq!( graph.edges().count(), @@ -407,7 +407,7 @@ pub(crate) fn edges_empty() { ); } -pub(crate) fn edges() +pub fn edges() where G::Edge: Debug, { @@ -422,7 +422,7 @@ where } } -pub(crate) fn incident_edges_empty() { +pub fn incident_edges_empty() { let (graph, v) = G::single_vertex(); assert_eq!( graph.incident_edges(v).count(), @@ -431,7 +431,7 @@ pub(crate) fn incident_edges_empty() { ); } -pub(crate) fn incident_edges() +pub fn incident_edges() where G::Vertex: Debug, G::Edge: Debug, @@ -461,7 +461,7 @@ where } } -pub(crate) fn incident_edges_loop_edge() +pub fn incident_edges_loop_edge() where G::Edge: Debug, { @@ -480,7 +480,7 @@ where assert_eq!(iter.next(), None, "too many incident edges from iterator"); } -pub(crate) fn incident_edges_multiple_edges() +pub fn incident_edges_multiple_edges() where G::Vertex: Debug, G::Edge: Debug, @@ -505,7 +505,7 @@ where } } -pub(crate) fn incidences_empty() { +pub fn incidences_empty() { let (graph, v) = G::single_vertex(); assert_eq!( graph.incidences(v).count(), @@ -514,7 +514,7 @@ pub(crate) fn incidences_empty() { ); } -pub(crate) fn incidences() +pub fn incidences() where G::Vertex: Debug, G::Edge: Debug, @@ -547,7 +547,7 @@ where } } -pub(crate) fn incidences_edge_consistency() +pub fn incidences_edge_consistency() where G::Vertex: Debug, G::Edge: Debug, @@ -580,7 +580,7 @@ where } } -pub(crate) fn incidences_loop_edge() +pub fn incidences_loop_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -604,7 +604,7 @@ where ); } -pub(crate) fn incidences_multiple_edges() +pub fn incidences_multiple_edges() where G::Vertex: Debug, G::Edge: Debug, @@ -641,7 +641,7 @@ where } } -pub(crate) fn incidence_cursor_empty() +pub fn incidence_cursor_empty() where G::Vertex: Debug, G::Edge: Debug, @@ -655,7 +655,7 @@ where ); } -pub(crate) fn incidence_cursor() +pub fn incidence_cursor() where G::Vertex: Debug, G::Edge: Debug, @@ -683,7 +683,7 @@ where } } -pub(crate) fn incidence_cursor_loop_edge() +pub fn incidence_cursor_loop_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -703,7 +703,7 @@ where assert_eq!(cursor.next(&graph), None, "too many incidences from cursor"); } -pub(crate) fn incidence_cursor_multiple_edges() +pub fn incidence_cursor_multiple_edges() where G::Vertex: Debug, G::Edge: Debug, @@ -740,7 +740,7 @@ where } } -pub(crate) fn incidence_cursor_copy() { +pub fn incidence_cursor_copy() { let (graph, vertices, _) = G::two_edge_path(); let mut c1 = graph.incidence_cursor(vertices[1]); assert!(c1.next(&graph).is_some(), "expected first incidence"); @@ -763,7 +763,7 @@ pub(crate) fn incidence_cursor_copy() { ); } -pub(crate) fn incident_vertices_incidences_consistency() +pub fn incident_vertices_incidences_consistency() where G::Vertex: Debug, G::Edge: Debug, @@ -783,7 +783,7 @@ where } } -pub(crate) fn incident_edges_incidences_consistency() +pub fn incident_edges_incidences_consistency() where G::Vertex: Debug, G::Edge: Debug, @@ -804,3 +804,4 @@ where ); } } + -- 2.43.0 From 8a1d491943ea5c0902a53b9ad41aff4af00ed0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 09:06:22 +0200 Subject: [PATCH 34/67] Format test code --- src/testing/graph_topology_addition_deletion_testing.rs | 1 - src/testing/graph_topology_addition_testing.rs | 1 - src/testing/graph_topology_deletion_testing.rs | 9 ++------- src/testing/graph_topology_testing.rs | 1 - 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/testing/graph_topology_addition_deletion_testing.rs b/src/testing/graph_topology_addition_deletion_testing.rs index ce01b82..a7b9b72 100644 --- a/src/testing/graph_topology_addition_deletion_testing.rs +++ b/src/testing/graph_topology_addition_deletion_testing.rs @@ -60,4 +60,3 @@ where ); assert_eq!(graph.edge_count(), 1, "unexpected edge count after re-add"); } - diff --git a/src/testing/graph_topology_addition_testing.rs b/src/testing/graph_topology_addition_testing.rs index 7123300..e48b7c7 100644 --- a/src/testing/graph_topology_addition_testing.rs +++ b/src/testing/graph_topology_addition_testing.rs @@ -150,4 +150,3 @@ pub fn edge_map_new_edge() { "unexpected value from map read after write for new edge" ); } - diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs index 1f1f684..dc1f866 100644 --- a/src/testing/graph_topology_deletion_testing.rs +++ b/src/testing/graph_topology_deletion_testing.rs @@ -297,9 +297,7 @@ where } } -pub fn incident_vertices_after_delete_edge< - G: GraphTopologyDeletion + GraphTopologyAddition, ->() +pub fn incident_vertices_after_delete_edge() where G::Vertex: Debug, G::Edge: Debug, @@ -446,9 +444,7 @@ where } } -pub fn incidence_cursor_after_delete_vertex< - G: GraphTopologyDeletion + GraphTopologyAddition, ->() +pub fn incidence_cursor_after_delete_vertex() where G::Vertex: Debug, G::Edge: Debug, @@ -540,4 +536,3 @@ fn assert_vertex_incidence_cursor( v ); } - diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index cdb5dc8..b3bb449 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -804,4 +804,3 @@ where ); } } - -- 2.43.0 From a8b6f7591cd46a03a429c9bc5b1482229dde5532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 09:10:03 +0200 Subject: [PATCH 35/67] Move common test helpers to fixtures module --- src/testing/fixtures.rs | 91 +++++++++++++++++++++++++++++++++++++++ tests/bfs.rs | 94 ++++------------------------------------- tests/dfs.rs | 51 +++------------------- tests/dijkstra.rs | 94 +++++++++-------------------------------- 4 files changed, 126 insertions(+), 204 deletions(-) diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index d4c2109..eaa259c 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -1,4 +1,6 @@ +use crate::maps::ElementMap; use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; +use std::fmt::Debug; pub trait MakeTestGraph: GraphTopology + Sized { fn standard() -> ( @@ -162,3 +164,92 @@ impl MakeTestGraph for G { (graph, vertices, [e0, e1, e2, e3]) } } + +pub fn assert_standard_unweighted_distances_v0(actual_for: F, vertices: &[V]) +where + V: Debug + Copy, + F: Fn(V) -> Option, +{ + let expected = [0, 1, 2, 2, 2, 3, 3, 3, 3, 4]; + for (i, v) in vertices.iter().enumerate() { + assert_eq!( + actual_for(*v), + Some(expected[i]), + "unexpected distance from vertex {:?} to vertex {:?}", + vertices[0], + vertices[i] + ); + } +} + +pub fn assert_standard_unweighted_predecessors_v0( + actual: &ElementMap>, + vertices: &[V], +) { + let expected = [ + vec![None], + vec![Some(vertices[0])], + vec![Some(vertices[1])], + vec![Some(vertices[1])], + vec![Some(vertices[1])], + vec![Some(vertices[2])], + vec![Some(vertices[2]), Some(vertices[3])], + vec![Some(vertices[4])], + vec![Some(vertices[4])], + vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], + ]; + for i in 0..10 { + assert!( + expected[i].contains(&actual[vertices[i]]), + "unexpected predecessor {:?} of vertex {:?}", + actual[vertices[i]], + vertices[i] + ); + } +} + +/// Asserts that the path given as `path_option` is a valid path in `graph` +/// +/// Walks the path and asserts that it is `Some`, that it is empty if and only if +/// `source == target`, that its edges are pairwise incident in the order given, and that it starts +/// at `source` and ends at `target`. +pub fn assert_valid_path( + graph: &G, + path_option: Option>, + source: G::Vertex, + targets: &[G::Vertex], +) -> Vec +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let path = path_option.expect(&format!( + "path should exist between source vertex {:?} and connected targets {:?}", + source, targets + )); + + if targets.contains(&source) { + assert!( + path.is_empty(), + "path from source to itself should be empty" + ); + } else { + assert!(!path.is_empty(), "path should not be empty"); + // Walks the path: tracks current vertex, confirms each edge is incident to it. + let mut current = source; + for (i, &e) in path.iter().enumerate() { + let (v1, v2) = graph.incident_vertices(e); + assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); + assert!( + v1 == current || v2 == current, + "path edge {e:?} (index {i}, vertices {v1:?} to {v2:?}) not incident to vertex {current:?}" + ); + current = if v1 == current { v2 } else { v1 }; + } + assert!( + targets.contains(¤t), + "path should end at a target vertex ({targets:?}), but ended at {current:?}" + ); + } + path +} diff --git a/tests/bfs.rs b/tests/bfs.rs index 913234d..89278fd 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -1,9 +1,8 @@ use std::fmt::Debug; use grapherity::algorithms; -use grapherity::maps::ElementMap; +use grapherity::testing::fixtures; use grapherity::testing::fixtures::MakeTestGraph; -use grapherity::traits::GraphTopology; macro_rules! bfs_tests { ($T:ty) => { @@ -96,8 +95,8 @@ where { let (graph, vertices, _, _) = G::standard(); let result = algorithms::bfs(&graph, vertices[0]); - assert_bfs_distances::(&result.distances, &vertices); - assert_bfs_predecessors::(&result.predecessors, &vertices); + fixtures::assert_standard_unweighted_distances_v0(|v| result.distances[v], &vertices); + fixtures::assert_standard_unweighted_predecessors_v0(&result.predecessors, &vertices); } fn bfs_distances_single_vertex() { @@ -135,7 +134,7 @@ where { let (graph, vertices, _, _) = G::standard(); let distances = algorithms::bfs_distances(&graph, vertices[0]); - assert_bfs_distances::(&distances, &vertices); + fixtures::assert_standard_unweighted_distances_v0(|v| distances[v], &vertices); } fn bfs_find_source() { @@ -161,27 +160,10 @@ where G::Vertex: Debug, { let (graph, vertices, _, _) = G::standard(); - let expected_distances = [ - Some(0), - Some(1), - Some(2), - Some(2), - Some(2), - Some(3), - Some(3), - Some(3), - Some(3), - Some(4), - ]; - for i in 0..10 { - assert_eq!( - algorithms::bfs_find(&graph, vertices[0], vertices[i]), - expected_distances[i], - "unexpected distance from {:?} to {:?}", - vertices[0], - vertices[i] - ); - } + fixtures::assert_standard_unweighted_distances_v0( + |v| algorithms::bfs_find(&graph, vertices[0], v), + &vertices, + ); } fn bfs_find_where_source_matches() @@ -245,63 +227,3 @@ where vertices[8] ); } - -fn assert_bfs_distances( - distances: &ElementMap>, - vertices: &[G::Vertex], -) where - G::Vertex: Debug, -{ - let expected = [ - Some(0), - Some(1), - Some(2), - Some(2), - Some(2), - Some(3), - Some(3), - Some(3), - Some(3), - Some(4), - ]; - for i in 0..10 { - assert_eq!( - distances[vertices[i]], expected[i], - "unexpected distance from {:?} to {:?}", - vertices[0], vertices[i] - ); - } -} - -fn assert_bfs_predecessors( - predecessors: &ElementMap>, - vertices: &[G::Vertex], -) where - G::Vertex: Debug, -{ - assert_eq!( - predecessors[vertices[0]], None, - "source should have no predecessor" - ); - // Each non-source vertex's predecessor must be adjacent and at distance one less. - let expected_predecessors = [ - vec![None], - vec![Some(vertices[0])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[2])], - vec![Some(vertices[2]), Some(vertices[3])], - vec![Some(vertices[4])], - vec![Some(vertices[4])], - vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], - ]; - for i in 1..10 { - assert!( - expected_predecessors[i].contains(&predecessors[vertices[i]]), - "unexpected predecessor {:?} of {:?}", - predecessors[vertices[i]], - vertices[i] - ); - } -} diff --git a/tests/dfs.rs b/tests/dfs.rs index 4999cc9..6e35afc 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -2,6 +2,7 @@ use std::fmt::Debug; use grapherity::algorithms; use grapherity::maps::ElementMap; +use grapherity::testing::fixtures; use grapherity::testing::fixtures::MakeTestGraph; use grapherity::traits::GraphTopology; @@ -257,12 +258,7 @@ where let (graph, vertices, e) = G::single_edge(); let path = algorithms::dfs_find_path(&graph, vertices[0], vertices[1]) .expect("path should exist between adjacent vertices"); - assert_eq!( - path.len(), - 1, - "unexpected path length between adjacent vertices" - ); - assert_eq!(path[0], e, "path should use the connecting edge"); + assert_eq!(path, [e], "path should contain only the connecting edge"); } fn dfs_find_path() @@ -271,11 +267,8 @@ where G::Edge: Debug, { let (graph, vertices, _, _) = G::standard(); - let path = algorithms::dfs_find_path(&graph, vertices[0], vertices[9]).expect(&format!( - "path should exist between connected vertices {:?} and {:?}", - vertices[0], vertices[9] - )); - assert_valid_path(&graph, &path, vertices[0], vertices[9]); + let path = algorithms::dfs_find_path(&graph, vertices[0], vertices[9]); + fixtures::assert_valid_path(&graph, path, vertices[0], &[vertices[9]]); } fn dfs_find_path_where_source_matches() @@ -320,13 +313,8 @@ where G::Edge: Debug, { let (graph, vertices, _, _) = G::standard(); - let path = algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]).expect( - &format!( - "path should exist between connected vertices {:?} and {:?}", - vertices[0], vertices[9] - ), - ); - assert_valid_path(&graph, &path, vertices[0], vertices[9]); + let path = algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[9]); + _ = fixtures::assert_valid_path(&graph, path, vertices[0], &[vertices[9]]); } fn assert_dfs_visited( @@ -369,30 +357,3 @@ fn assert_dfs_predecessors( ); } } - -fn assert_valid_path( - graph: &G, - path: &[G::Edge], - source: G::Vertex, - target: G::Vertex, -) where - G::Vertex: Debug, - G::Edge: Debug, -{ - assert!(!path.is_empty(), "path should be non-empty"); - // Walks the path: tracks current vertex, confirm each edge is incident to it. - let mut current = source; - for (i, &e) in path.iter().enumerate() { - let (v1, v2) = graph.incident_vertices(e); - assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); - assert!( - v1 == current || v2 == current, - "path edge {e:?} (idx {i}, {v1:?} to {v2:?}) not incident to vertex {current:?}" - ); - current = if v1 == current { v2 } else { v1 }; - } - assert_eq!( - current, target, - "path should end at target {target:?}, but ended at {current:?}" - ); -} diff --git a/tests/dijkstra.rs b/tests/dijkstra.rs index 0945aa9..f7c5423 100644 --- a/tests/dijkstra.rs +++ b/tests/dijkstra.rs @@ -4,6 +4,7 @@ use std::hash::Hash; use grapherity::algorithms; use grapherity::algorithms::DijkstraResult; use grapherity::maps::ElementMap; +use grapherity::testing::fixtures; use grapherity::testing::fixtures::MakeTestGraph; use grapherity::traits::{GraphTopology, Incidence}; @@ -94,18 +95,19 @@ fn dijkstra() where G::Vertex: Debug + Hash, { - let (graph, vertices, _, _, weights) = make_test_graph_weighted::(); + let (graph, vertices, _, _, weights) = make_standard_weighted::(); let result = algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); - assert_test_graph::(&result, &vertices); + assert_standard_weighted_distances_v0::(&result.distances, &vertices); + assert_standard_weighted_predecessors_v0::(&result.predecessors, &vertices); } fn dijkstra_distances() where G::Vertex: Debug + Hash, { - let (graph, vertices, _, _, weights) = make_test_graph_weighted::(); + let (graph, vertices, _, _, weights) = make_standard_weighted::(); let distances = algorithms::dijkstra_distances(&graph, vertices[0], |e| weights[e]); - assert_distances_test_graph::(&distances, &vertices); + assert_standard_weighted_distances_v0::(&distances, &vertices); } fn dijkstra_unweighted_single_vertex() @@ -133,7 +135,8 @@ where { let (graph, vertices, _, _) = G::standard(); let result = algorithms::dijkstra_unweighted(&graph, vertices[0]); - assert_unweighted_test_graph::(&result, &vertices); + fixtures::assert_standard_unweighted_distances_v0(|v| result.distances[v], &vertices); + fixtures::assert_standard_unweighted_predecessors_v0(&result.predecessors, &vertices); } fn dijkstra_distances_unweighted_single_vertex() @@ -161,7 +164,7 @@ where { let (graph, vertices, _, _) = G::standard(); let distances = algorithms::dijkstra_distances_unweighted(&graph, vertices[0]); - assert_distances_unweighted_test_graph::(&distances, &vertices); + fixtures::assert_standard_unweighted_distances_v0(|v| distances[v], &vertices); } fn assert_single_vertex(result: &DijkstraResult, v: G::Vertex) @@ -213,12 +216,13 @@ fn assert_distances_disconnected( } } -fn assert_test_graph(result: &DijkstraResult, vertices: &[G::Vertex]) -where +fn assert_standard_weighted_predecessors_v0( + actual: &ElementMap>, + vertices: &[G::Vertex], +) where G::Vertex: Debug, { - assert_distances_test_graph::(&result.distances, &vertices); - let expected_predecessors_from_v0 = [ + let expected = [ vec![None], vec![Some(vertices[0])], vec![Some(vertices[4])], @@ -232,21 +236,21 @@ where ]; for i in 0..10 { assert!( - expected_predecessors_from_v0[i].contains(&result.predecessors[vertices[i]]), + expected[i].contains(&actual[vertices[i]]), "unexpected predecessor {:?} of {:?}", - result.predecessors[vertices[i]], + actual[vertices[i]], vertices[i] ); } } -fn assert_distances_test_graph( - distances: &ElementMap>, +fn assert_standard_weighted_distances_v0( + actual: &ElementMap>, vertices: &[G::Vertex], ) where G::Vertex: Debug, { - let expected_distances_from_v0 = [ + let expected = [ Some(0), Some(1), Some(65), @@ -260,70 +264,14 @@ fn assert_distances_test_graph( ]; for i in 0..10 { assert_eq!( - distances[vertices[i]], expected_distances_from_v0[i], + actual[vertices[i]], expected[i], "unexpected distance from {:?} to {:?}", vertices[0], vertices[i] ); } } -fn assert_unweighted_test_graph( - result: &DijkstraResult, - vertices: &[G::Vertex], -) where - G::Vertex: Debug, -{ - assert_distances_unweighted_test_graph::(&result.distances, &vertices); - let expected_predecessors_from_v0 = [ - vec![None], - vec![Some(vertices[0])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[1])], - vec![Some(vertices[2])], - vec![Some(vertices[2]), Some(vertices[3])], - vec![Some(vertices[4])], - vec![Some(vertices[4])], - vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], - ]; - for i in 0..10 { - assert!( - expected_predecessors_from_v0[i].contains(&result.predecessors[vertices[i]]), - "unexpected predecessor {:?} of {:?}", - result.predecessors[vertices[i]], - vertices[i] - ); - } -} - -fn assert_distances_unweighted_test_graph( - distances: &ElementMap>, - vertices: &[G::Vertex], -) where - G::Vertex: Debug, -{ - let expected_distances_from_v0 = [ - Some(0), - Some(1), - Some(2), - Some(2), - Some(2), - Some(3), - Some(3), - Some(3), - Some(3), - Some(4), - ]; - for i in 0..10 { - assert_eq!( - distances[vertices[i]], expected_distances_from_v0[i], - "unexpected distance from {:?} to {:?}", - vertices[0], vertices[i] - ); - } -} - -fn make_test_graph_weighted() -> ( +fn make_standard_weighted() -> ( G, [G::Vertex; 10], [(G::Edge, G::Vertex, G::Vertex); 18], -- 2.43.0 From 73af8a16caad5f954d1c185e822a9397601d5354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 09:43:45 +0200 Subject: [PATCH 36/67] Add algorithms::bfs_find_path --- src/algorithms.rs | 112 +++++++++++++++++++++++++++++++++++++++++++-- tests/bfs.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 3 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index 7719e0f..ca3fdd4 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -425,10 +425,8 @@ where F: FnMut(G::Vertex, G::Vertex) -> bool, { let mut distances = graph.vertex_map(None); - let mut queue = VecDeque::new(); - distances[source] = Some(0); - queue.push_back(source); + let mut queue = VecDeque::from([source]); while let Some(v) = queue.pop_front() { for neighbor in graph.adjacent_vertices(v) { @@ -451,6 +449,114 @@ where } } +/// [Breadth-first search] from `source` for `target`, returns some shortest path as a sequence of +/// edges. +/// +/// Traverses vertices in `graph` starting from `source` until `target` is found, exploring all +/// neighbors before descending further. Returns some shortest path from `source` to `target` as a +/// sequence of edges in traversal order if `target` is a valid vertex of `graph` connected to +/// `source`, or `None` otherwise. The returned path is empty if and only if `source` equals +/// `target`. +/// +/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*. +/// +/// # Panics +/// +/// Panics if `source` is not a valid vertex of `graph`. +/// +/// # Examples +/// +/// ``` +/// # use grapherity::prelude::*; +/// # use grapherity::algorithms::bfs_find_path; +/// # use grapherity::models::Graph; +/// let mut graph = Graph::new(); +/// let source = graph.add_vertex(); +/// let target = graph.add_vertex(); +/// let e = graph.add_edge(source, target); +/// +/// assert_eq!(bfs_find_path(&graph, source, target), Some(vec![e])); +/// ``` +/// +/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search +pub fn bfs_find_path(graph: &G, source: G::Vertex, target: G::Vertex) -> Option> +where + G: GraphTopology, +{ + bfs_find_path_where(graph, source, |v| v == target) +} + +/// [Breadth-first search] from `source` for a vertex matching `predicate`, returns some shortest +/// path as a sequence of edges. +/// +/// Traverses vertices in `graph` starting from `source` until a vertex satisfying `predicate` is +/// found, exploring all neighbors before descending further. Returns some shortest path from +/// `source` to the first vertex satisfying `predicate` as a sequence of edges in traversal order, +/// or `None` if no such vertex is connected to `source`. The returned path is empty if and only if +/// `source` satisfies `predicate`. +/// +/// Time complexity is *O(|V| + |E|)*, space complexity is *O(|V|)*. +/// +/// # Panics +/// +/// Panics if `source` is not a valid vertex of `graph`. +/// +/// # Examples +/// +/// ``` +/// # use grapherity::prelude::*; +/// # use grapherity::algorithms::bfs_find_path_where; +/// # use grapherity::models::Graph; +/// let mut graph = Graph::new(); +/// let source = graph.add_vertex(); +/// let target = graph.add_vertex(); +/// let e = graph.add_edge(source, target); +/// +/// assert_eq!(bfs_find_path_where(&graph, source, |v| v == target), Some(vec![e])); +/// ``` +/// +/// [Breadth-first search]: https://en.wikipedia.org/wiki/Breadth-first_search +pub fn bfs_find_path_where(graph: &G, source: G::Vertex, predicate: P) -> Option> +where + G: GraphTopology, + P: Fn(G::Vertex) -> bool, +{ + if predicate(source) { + return Some(vec![]); + } + + let mut visited = graph.vertex_map(false); + visited[source] = true; + let mut predecessor: ElementMap> = + graph.vertex_map(None); + let mut queue = VecDeque::from([source]); + + while let Some(v) = queue.pop_front() { + for Incidence { + vertex: neighbor, + edge, + } in graph.incidences(v) + { + if !visited[neighbor] { + visited[neighbor] = true; + predecessor[neighbor] = Some((v, edge)); + if predicate(neighbor) { + let mut path = vec![edge]; + let mut current = v; + while let Some((prev, e)) = predecessor[current] { + path.push(e); + current = prev; + } + path.reverse(); + return Some(path); + } + queue.push_back(neighbor); + } + } + } + None +} + // TODO: 'visited' is already encoded in 'predecessors', except for 'source', which is visited, but has no predecessor. /// Return data type for [`dfs`]. pub struct DfsResult { diff --git a/tests/bfs.rs b/tests/bfs.rs index 89278fd..90f63e2 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -20,6 +20,14 @@ macro_rules! bfs_tests { bfs_find_where_disconnected, bfs_find_where_no_match, bfs_find_where_nearest, + bfs_find_path_source_equals_target, + bfs_find_path_disconnected, + bfs_find_path_adjacent, + bfs_find_path, + bfs_find_path_where_source_matches, + bfs_find_path_where_disconnected, + bfs_find_path_where_no_match, + bfs_find_path_where, ); }; @@ -227,3 +235,109 @@ where vertices[8] ); } + +fn bfs_find_path_source_equals_target() +where + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::bfs_find_path(&graph, v, v), + Some(vec![]), + "path from source to itself should be empty" + ); +} + +fn bfs_find_path_disconnected() +where + G::Edge: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::bfs_find_path(&graph, vertices[0], vertices[1]), + None, + "no path should exist to disconnected vertex" + ); +} + +fn bfs_find_path_adjacent() +where + G::Edge: Debug, +{ + let (graph, vertices, e) = G::single_edge(); + let path = algorithms::bfs_find_path(&graph, vertices[0], vertices[1]) + .expect("path should exist between adjacent vertices"); + assert_eq!(path, [e], "path should contain only the connecting edge"); +} + +fn bfs_find_path() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + fixtures::assert_standard_unweighted_distances_v0( + |v| { + let result = algorithms::bfs_find_path(&graph, vertices[0], v); + let path = fixtures::assert_valid_path(&graph, result, vertices[0], &[v]); + Some(u32::try_from(path.len()).unwrap()) + }, + &vertices, + ); +} + +fn bfs_find_path_where_source_matches() +where + G::Edge: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_eq!( + algorithms::bfs_find_path_where(&graph, v, |u| u == v), + Some(vec![]), + "path from source to itself should be empty" + ); +} + +fn bfs_find_path_where_disconnected() +where + G::Edge: Debug, +{ + let (graph, vertices) = G::disconnected(); + assert_eq!( + algorithms::bfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), + None, + "no path should exist to disconnected vertex" + ); +} + +fn bfs_find_path_where_no_match() +where + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + assert_eq!( + algorithms::bfs_find_path_where(&graph, vertices[0], |_| false), + None, + "no path should exist when predicate never matches" + ); +} + +fn bfs_find_path_where() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // vertices[7] and vertices[8] are at distance 3 from vertices[0]; vertices[9] is at distance 4. + // BFS must return one shortest path, so it must lead to one of the distance-3 matches. + let result = algorithms::bfs_find_path_where(&graph, vertices[0], |v| { + v == vertices[7] || v == vertices[8] || v == vertices[9] + }); + let path = + fixtures::assert_valid_path(&graph, result, vertices[0], &[vertices[7], vertices[8]]); + assert_eq!( + path.len(), + 3, + "BFS should return shortest path to a matching vertex" + ); +} -- 2.43.0 From c25a8a4fc1a25af7e7f9cbacc540277025bb8bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 19:49:45 +0200 Subject: [PATCH 37/67] Add FrozenGraph tests --- src/models/frozen_graph.rs | 67 ++++++++++++++++++++++++++++ src/testing/fixtures.rs | 89 +++++++++++++++++++++++++++++++++++++- tests/bfs.rs | 6 +++ tests/dfs.rs | 6 +++ tests/dijkstra.rs | 6 +++ 5 files changed, 172 insertions(+), 2 deletions(-) diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index b2980dc..0b94f47 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -248,3 +248,70 @@ impl GraphTopology for FrozenGraph { } } } + +#[cfg(test)] +mod trait_tests { + use super::FrozenGraph; + + crate::graph_topology_tests!(FrozenGraph); +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::testing::fixtures::MakeTestGraph; + use crate::traits::GraphTopology; + + #[test] + fn incident_vertices_opposite_handle() { + let (graph, [v0, v1], e) = FrozenGraph::single_edge(); + let f = Edge(graph.incidences[e.0].opposite); + let (u0, u1) = graph.incident_vertices(f); + assert!( + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for edge {f:?}" + ); + } + + #[test] + fn csr_sentinel_and_offsets_standard() { + let (graph, ..) = FrozenGraph::standard(); + assert_eq!( + *graph.vertices.last().unwrap(), + graph.incidences.len(), + "sentinel should equal total number of incidences" + ); + assert!( + graph.vertices.windows(2).all(|w| w[0] <= w[1]), + "incidence offsets of vertices should be monotonically non-decreasing" + ); + } + + #[test] + fn csr_sentinel_and_offsets_empty() { + let graph = FrozenGraph::empty(); + assert_eq!( + graph.vertices, + vec![0], + "empty graph should have a single sentinel entry of 0" + ); + assert!( + graph.incidences.is_empty(), + "empty graph should have no incidences" + ); + } + + #[test] + fn opposite_symmetry() { + let (graph, ..) = FrozenGraph::standard(); + for i in 0..graph.incidences.len() { + let opp = graph.incidences[i].opposite; + assert_ne!(opp, i, "incidence should not equal its own opposite"); + assert_eq!( + graph.incidences[opp].opposite, i, + "opposite of opposite of incidence should equal the incidence" + ); + } + } +} diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index eaa259c..3b02869 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -1,7 +1,9 @@ -use crate::maps::ElementMap; -use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; use std::fmt::Debug; +use crate::maps::ElementMap; +use crate::models::{AppendGraph, FrozenGraph}; +use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; + pub trait MakeTestGraph: GraphTopology + Sized { fn standard() -> ( Self, @@ -165,6 +167,89 @@ impl MakeTestGraph for G { } } +impl MakeTestGraph for FrozenGraph { + fn standard() -> ( + Self, + [Self::Vertex; 10], + [(Self::Edge, Self::Vertex, Self::Vertex); 18], + [Vec>; 10], + ) { + let (build, vertices, edges, incidences) = AppendGraph::standard(); + let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + ( + graph, + vertices.map(|v| vertex_map[v]), + edges.map(|(e, u, v)| (edge_map[e], vertex_map[u], vertex_map[v])), + incidences.map(|vec| { + vec.iter() + .map(|i| Incidence { + vertex: vertex_map[i.vertex], + edge: edge_map[i.edge], + }) + .collect() + }), + ) + } + + fn empty() -> Self { + FrozenGraph::from(&AppendGraph::empty()) + } + + fn single_vertex() -> (Self, Self::Vertex) { + let (build, v) = AppendGraph::single_vertex(); + let (graph, vertex_map, _) = FrozenGraph::from_graph(&build); + (graph, vertex_map[v]) + } + + fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge) { + let (build, vertices, e) = AppendGraph::single_edge(); + let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + (graph, vertices.map(|v| vertex_map[v]), edge_map[e]) + } + + fn disconnected() -> (Self, [Self::Vertex; 3]) { + let (build, vertices) = AppendGraph::disconnected(); + let (graph, vertex_map, _) = FrozenGraph::from_graph(&build); + (graph, vertices.map(|v| vertex_map[v])) + } + + fn loop_edge() -> (Self, Self::Vertex, Self::Edge) { + let (build, v, e) = AppendGraph::loop_edge(); + let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + (graph, vertex_map[v], edge_map[e]) + } + + fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]) { + let (build, vertices, edges) = AppendGraph::multiple_edges(); + let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + ( + graph, + vertices.map(|v| vertex_map[v]), + edges.map(|e| edge_map[e]), + ) + } + + fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]) { + let (build, vertices, edges) = AppendGraph::two_edge_path(); + let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + ( + graph, + vertices.map(|v| vertex_map[v]), + edges.map(|e| edge_map[e]), + ) + } + + fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]) { + let (build, vertices, edges) = AppendGraph::two_edge_path_with_loops(); + let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + ( + graph, + vertices.map(|v| vertex_map[v]), + edges.map(|e| edge_map[e]), + ) + } +} + pub fn assert_standard_unweighted_distances_v0(actual_for: F, vertices: &[V]) where V: Debug + Copy, diff --git a/tests/bfs.rs b/tests/bfs.rs index 90f63e2..53a1dfe 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -41,6 +41,12 @@ macro_rules! bfs_tests { }; } +mod frozen_graph { + use grapherity::models::FrozenGraph; + + bfs_tests!(FrozenGraph); +} + mod append_graph { use grapherity::models::AppendGraph; diff --git a/tests/dfs.rs b/tests/dfs.rs index 6e35afc..d49bbcb 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -44,6 +44,12 @@ macro_rules! dfs_tests { }; } +mod frozen_graph { + use grapherity::models::FrozenGraph; + + dfs_tests!(FrozenGraph); +} + mod append_graph { use grapherity::models::AppendGraph; diff --git a/tests/dijkstra.rs b/tests/dijkstra.rs index f7c5423..802098a 100644 --- a/tests/dijkstra.rs +++ b/tests/dijkstra.rs @@ -35,6 +35,12 @@ macro_rules! dijkstra_tests { }; } +mod frozen_graph { + use grapherity::models::FrozenGraph; + + dijkstra_tests!(FrozenGraph); +} + mod append_graph { use grapherity::models::AppendGraph; -- 2.43.0 From 1209d75e15553de5c8c4bd8c9e80df7d142e2545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 20:28:41 +0200 Subject: [PATCH 38/67] Use non-zero edge indices to reduce size of structs with Option fields --- src/models/append_graph.rs | 41 ++++++++++++++++++++++++-------------- src/models/graph.rs | 25 ++++++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 47f26d5..92adb4d 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -1,5 +1,7 @@ //! [`AppendGraph`], an undirected graph topology supporting addition only. +use std::num::NonZeroUsize; + use crate::maps::ElementMap; use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; @@ -10,12 +12,27 @@ use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCu #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Vertex(usize); +// TODO: Benchmark the cost of the index shift. An alternative could be to init AppendGraph::incidences with two dummy entries to avoid 0. /// An opaque handle identifying an edge in an [`AppendGraph`]. /// /// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// [`AppendGraph::add_edge`] and [`AppendGraph::edges`]. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -pub struct Edge(usize); +pub struct Edge(NonZeroUsize); + +impl Edge { + fn new(index: usize) -> Self { + Edge(NonZeroUsize::new(index + 1).unwrap()) + } + + fn index(self) -> usize { + self.0.get() - 1 + } + + fn normalize(&self) -> Self { + Self::new(self.index() & !1) + } +} /// An [`ElementMap`] for [`AppendGraph`] vertices. pub type AppendGraphVertexMap = ElementMap; @@ -26,13 +43,6 @@ pub type AppendGraphEdgeMap = ElementMap; /// An [`Incidence`] for [`AppendGraph`]. pub type AppendGraphIncidence = Incidence; -impl Edge { - fn normalize(&self) -> Self { - Self(self.0 & !1) - } -} - -// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173 struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, @@ -118,7 +128,7 @@ impl AppendGraph { adjacent: v2, }); self.vertices[v1.0].incidence_count += 1; - self.vertices[v1.0].first_incidence = Some(Edge(self.incidences.len() - 1)); + self.vertices[v1.0].first_incidence = Some(Edge::new(self.incidences.len() - 1)); } fn raw_incidences(&self, v: Vertex) -> impl Iterator { @@ -128,7 +138,7 @@ impl AppendGraph { fn step_incidence(&self, incidence: &mut Option) -> Option<(Vertex, Edge)> { let current = (*incidence)?; - let entry = self.incidences[current.0]; + let entry = self.incidences[current.index()]; *incidence = entry.next; Some((entry.adjacent, current)) } @@ -160,7 +170,7 @@ impl GraphTopology for AppendGraph { // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn edge_map(&self, default: T) -> ElementMap { - ElementMap::new(default, |e| e.0 / 2, self.edge_capacity()) + ElementMap::new(default, |e| e.index() / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { @@ -180,14 +190,15 @@ impl GraphTopology for AppendGraph { } fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { + let ei = e.index(); ( - self.incidences[e.0 ^ 1].adjacent, - self.incidences[e.0].adjacent, + self.incidences[ei ^ 1].adjacent, + self.incidences[ei].adjacent, ) } fn edges(&self) -> impl Iterator { - (0..self.incidences.len()).step_by(2).map(Edge) + (0..self.incidences.len()).step_by(2).map(Edge::new) } fn incident_edges(&self, v: Self::Vertex) -> impl Iterator { @@ -236,7 +247,7 @@ impl GraphTopologyAddition for AppendGraph { fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge { self.add_incidence(v1, v2); self.add_incidence(v2, v1); - Edge(self.incidences.len() - 2) + Edge::new(self.incidences.len() - 2) } } diff --git a/src/models/graph.rs b/src/models/graph.rs index 11a8ff9..c1be295 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -1,5 +1,6 @@ //! [`Graph`], an undirected graph topology supporting addition and deletion. +use std::num::NonZeroUsize; use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; @@ -31,10 +32,20 @@ pub type GraphIncidence = Incidence; #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct VertexSlot(usize); +// TODO: Benchmark the cost of the index shift. An alternative could be to init Graph::incidences with two dummy entries to avoid 0. #[derive(Copy, Clone, PartialEq, Eq, Debug)] -struct IncidenceSlot(usize); +struct IncidenceSlot(NonZeroUsize); + +impl IncidenceSlot { + fn new(index: usize) -> Self { + IncidenceSlot(NonZeroUsize::new(index + 1).unwrap()) + } + + fn index(self) -> usize { + self.0.get() - 1 + } +} -// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173 /// `pub` because [`Vertex`] references it as a type parameter of the underlying arena. Not /// intended for direct external use. #[doc(hidden)] @@ -147,7 +158,7 @@ impl Graph { adjacent: VertexSlot(v2.arr_idx()), }); self.vertices[v1].incidence_count += 1; - self.vertices[v1].first_incidence = Some(IncidenceSlot(edge.arr_idx())); + self.vertices[v1].first_incidence = Some(IncidenceSlot::new(edge.arr_idx())); edge } @@ -186,7 +197,7 @@ impl Graph { let first = vertex_header .first_incidence .expect("incident vertex without incidences, corrupt internal data state"); - if first.0 == e.arr_idx() { + if first.index() == e.arr_idx() { vertex_header.first_incidence = next; } else { let graph: &Graph = self; @@ -195,7 +206,7 @@ impl Graph { .find(|(_, f)| { graph.incidences[*f] .next - .is_some_and(|i| i.0 == e.arr_idx()) + .is_some_and(|i| i.index() == e.arr_idx()) }) .expect("cannot find previous incidence, corrupt internal data state"); self.incidences[previous].next = next; @@ -210,7 +221,7 @@ impl Graph { fn step_incidence(&self, incidence: &mut Option) -> Option<(VertexSlot, Edge)> { // TODO: Benchmark storing full Index (one read, larger entries) vs. slot + get_idx() (two reads, smaller entries). let current = (*incidence)?; - let e = self.incidences.get_idx(current.0).unwrap(); + let e = self.incidences.get_idx(current.index()).unwrap(); let entry = self.incidences[e]; *incidence = entry.next; Some((entry.adjacent, e)) @@ -371,7 +382,7 @@ impl GraphTopologyDeletion for Graph { if e_entry.adjacent != f_entry.adjacent { self.update_incidence_list(e, f_entry.adjacent, e_entry.next, false); self.update_incidence_list(f, e_entry.adjacent, f_entry.next, false); - } else if f_entry.next.is_some_and(|i| e.arr_idx() == i.0) { + } else if f_entry.next.is_some_and(|i| e.arr_idx() == i.index()) { self.update_incidence_list(f, e_entry.adjacent, e_entry.next, true); } else { self.update_incidence_list(e, e_entry.adjacent, f_entry.next, true); -- 2.43.0 From 3ad448974ec73a696ce5b3518ef556464dc2ffd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 20:40:15 +0200 Subject: [PATCH 39/67] Add new assert helper function for GraphTopologyDeletion tests --- src/models/append_graph.rs | 14 ++-- .../graph_topology_deletion_testing.rs | 69 +++++++++---------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 92adb4d..dda99a1 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -262,18 +262,16 @@ mod trait_tests { #[cfg(test)] mod tests { use super::*; + use crate::testing::fixtures::MakeTestGraph; #[test] fn incident_vertices_paired_index() { - let mut graph = AppendGraph::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - let f = Edge(e.0 + 1); - let (u1, u2) = graph.incident_vertices(f); + let (graph, [v0, v1], e) = AppendGraph::single_edge(); + let f = Edge::new(e.index() + 1); + let (u0, u1) = graph.incident_vertices(f); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {f:?}" + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for edge {f:?}" ); } } diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs index dc1f866..c51b42a 100644 --- a/src/testing/graph_topology_deletion_testing.rs +++ b/src/testing/graph_topology_deletion_testing.rs @@ -354,25 +354,7 @@ where .iter() .filter_map(|x| (x.edge != edges[2].0).then_some(x.edge)) .collect(); - assert_eq!( - graph.incident_edges(vertices[i]).count(), - expected.len(), - "unexpected incident edge count for vertex {:?} after delete", - vertices[i] - ); - for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} after delete", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incident edges {:?} of vertex {:?} not matched after delete", - expected, - vertices[i] - ); + assert_incident_edges_after_delete(&graph, vertices[i], &mut expected); } } @@ -388,25 +370,7 @@ where .iter() .filter_map(|x| (x.vertex != vertices[2]).then_some(x.edge)) .collect(); - assert_eq!( - graph.incident_edges(vertices[i]).count(), - expected.len(), - "unexpected incident edge count for vertex {:?} after delete", - vertices[i] - ); - for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} after delete", - vertices[i] - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incident edges {:?} of vertex {:?} not matched after delete", - expected, - vertices[i] - ); + assert_incident_edges_after_delete(&graph, vertices[i], &mut expected); } } @@ -536,3 +500,32 @@ fn assert_vertex_incidence_cursor( v ); } + +fn assert_incident_edges_after_delete( + graph: &G, + v: G::Vertex, + expected: &mut Vec, +) where + G::Vertex: Debug, + G::Edge: Debug, +{ + assert_eq!( + graph.incident_edges(v).count(), + expected.len(), + "unexpected incident edge count for vertex {:?} after delete", + v + ); + for e in graph.incident_edges(v) { + let pos = expected.iter().position(|f| *f == e).expect(&format!( + "unexpected incident edge {e:?} of vertex {:?} after delete", + v + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incident edges {:?} of vertex {:?} not matched after delete", + expected, + v + ); +} -- 2.43.0 From 8c3c6abc7821f904bdb365174a7794e077f9cd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 20:40:43 +0200 Subject: [PATCH 40/67] Updated todos --- src/algorithms.rs | 1 - src/models/frozen_graph.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index ca3fdd4..f42e4ad 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -557,7 +557,6 @@ where None } -// TODO: 'visited' is already encoded in 'predecessors', except for 'source', which is visited, but has no predecessor. /// Return data type for [`dfs`]. pub struct DfsResult { /// Vertex map indicating which vertices were visited during the search. diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 0b94f47..978e335 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -96,6 +96,7 @@ pub struct FrozenGraph { } impl FrozenGraph { + // TODO: Move this to a new FromGraph trait and adapt the MakeTestGraph impl to use helper functions like "fn fixture_empty() -> G { G::from_graph(&AppendGraph::empty()).0 }". /// Creates a graph instance as a copy of another graph. /// /// Returns the copied graph instance and two mappings of the `source` graph vertices and edges -- 2.43.0 From 016d127bd1afc1eedf3672f32e11351d67b4ccf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 4 Sep 2026 08:16:09 +0200 Subject: [PATCH 41/67] Update comment for pub, hidden Graph structs --- src/models/graph.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/graph.rs b/src/models/graph.rs index c1be295..37b1a75 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -46,16 +46,16 @@ impl IncidenceSlot { } } -/// `pub` because [`Vertex`] references it as a type parameter of the underlying arena. Not -/// intended for direct external use. +/// Referenced by [`Vertex`] as a type parameter of the underlying arena. Not intended for direct +/// external use. #[doc(hidden)] pub struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, } -/// `pub` because [`Edge`] references it as a type parameter of the underlying arena. Not intended -/// for direct external use. +/// Referenced by [`Edge`] as a type parameter of the underlying arena. Not intended for direct +/// external use. #[doc(hidden)] #[derive(Copy, Clone)] pub struct IncidenceEntry { -- 2.43.0 From 4c6a8a43e4dfdba7eae13d4ffebd8d59a02c28df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 4 Sep 2026 09:28:00 +0200 Subject: [PATCH 42/67] Refactor tests: use MakeTestGraph in model-specific tests, deconstruct vertex arrays where useful, adjust local variable names --- src/models/append_graph.rs | 28 +++---- src/models/frozen_graph.rs | 16 ++-- src/models/graph.rs | 69 ++++++++--------- src/testing/fixtures.rs | 12 +-- ...raph_topology_addition_deletion_testing.rs | 4 +- .../graph_topology_addition_testing.rs | 20 ++--- .../graph_topology_deletion_testing.rs | 44 +++++------ src/testing/graph_topology_testing.rs | 75 +++++++------------ src/traits.rs | 12 +-- tests/bfs.rs | 31 ++++---- tests/dfs.rs | 37 +++++---- 11 files changed, 153 insertions(+), 195 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index dda99a1..c20ed1d 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -90,10 +90,10 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// use grapherity::models::AppendGraph; /// /// let mut graph = AppendGraph::new(); -/// let v1 = graph.add_vertex(); -/// let v2 = graph.add_vertex(); -/// let e = graph.add_edge(v1, v2); -/// assert!(graph.are_adjacent(v1, v2)); +/// let u = graph.add_vertex(); +/// let v = graph.add_vertex(); +/// let e = graph.add_edge(u, v); +/// assert!(graph.are_adjacent(u, v)); /// ``` /// /// # Time and space complexity @@ -122,13 +122,13 @@ impl AppendGraph { /// Adds a single incidence of an edge, which is composed by two such incidences, to the /// incidences vector. - fn add_incidence(&mut self, v1: Vertex, v2: Vertex) { + fn add_incidence(&mut self, u: Vertex, v: Vertex) { self.incidences.push(IncidenceEntry { - next: self.vertices[v1.0].first_incidence.take(), - adjacent: v2, + next: self.vertices[u.0].first_incidence.take(), + adjacent: v, }); - self.vertices[v1.0].incidence_count += 1; - self.vertices[v1.0].first_incidence = Some(Edge::new(self.incidences.len() - 1)); + self.vertices[u.0].incidence_count += 1; + self.vertices[u.0].first_incidence = Some(Edge::new(self.incidences.len() - 1)); } fn raw_incidences(&self, v: Vertex) -> impl Iterator { @@ -177,8 +177,8 @@ impl GraphTopology for AppendGraph { self.vertices[v.0].incidence_count } - fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool { - self.adjacent_vertices(v1).any(|x| x == v2) + fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool { + self.adjacent_vertices(u).any(|x| x == v) } fn vertices(&self) -> impl Iterator { @@ -244,9 +244,9 @@ impl GraphTopologyAddition for AppendGraph { Vertex(self.vertices.len() - 1) } - fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge { - self.add_incidence(v1, v2); - self.add_incidence(v2, v1); + fn add_edge(&mut self, u: Self::Vertex, v: Self::Vertex) -> Self::Edge { + self.add_incidence(u, v); + self.add_incidence(v, u); Edge::new(self.incidences.len() - 2) } } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 978e335..5b62182 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -74,13 +74,13 @@ impl IncidenceCursor for FrozenGraphIncidenceCursor { /// /// // Constructs a FrozenGraph instance via AppendGraph. /// let mut graph = AppendGraph::new(); -/// let v1 = graph.add_vertex(); -/// let v2 = graph.add_vertex(); -/// let e = graph.add_edge(v1, v2); -/// let (graph, vertices, edges) = FrozenGraph::from_graph(&graph); +/// let u = graph.add_vertex(); +/// let v = graph.add_vertex(); +/// let e = graph.add_edge(u, v); +/// let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&graph); /// -/// // Queries the FrozenGraph instance. -/// assert!(graph.are_adjacent(vertices[v1], vertices[v2])); +/// // Queries the FrozenGraph instance via the returned map. +/// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); /// ``` /// /// # Time and space complexity @@ -210,8 +210,8 @@ impl GraphTopology for FrozenGraph { self.vertices[v.0 + 1] - self.vertices[v.0] } - fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool { - self.adjacent_vertices(v1).any(|x| x == v2) + fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool { + self.adjacent_vertices(u).any(|x| x == v) } fn vertices(&self) -> impl Iterator { diff --git a/src/models/graph.rs b/src/models/graph.rs index 37b1a75..3895d80 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -152,13 +152,13 @@ impl Graph { /// Adds a single incidence of an edge, which is composed by two such incidences, to the /// incidences arena, and returns its index. - fn add_incidence(&mut self, v1: Vertex, v2: Vertex) -> Edge { + fn add_incidence(&mut self, u: Vertex, v: Vertex) -> Edge { let edge = self.incidences.insert(IncidenceEntry { - next: self.vertices[v1].first_incidence.take(), - adjacent: VertexSlot(v2.arr_idx()), + next: self.vertices[u].first_incidence.take(), + adjacent: VertexSlot(v.arr_idx()), }); - self.vertices[v1].incidence_count += 1; - self.vertices[v1].first_incidence = Some(IncidenceSlot::new(edge.arr_idx())); + self.vertices[u].incidence_count += 1; + self.vertices[u].first_incidence = Some(IncidenceSlot::new(edge.arr_idx())); edge } @@ -270,8 +270,8 @@ impl GraphTopology for Graph { self.vertices[v].incidence_count } - fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool { - self.adjacent_vertices(v1).any(|x| x == v2) + fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool { + self.adjacent_vertices(u).any(|x| x == v) } fn vertices(&self) -> impl Iterator { @@ -284,16 +284,16 @@ impl GraphTopology for Graph { } fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { - let v2 = self + let v = self .vertices .get_idx(self.incidences[e].adjacent.0) .unwrap(); let f = self.incidences.get_idx(e.arr_idx() ^ 1).unwrap(); - let v1 = self + let u = self .vertices .get_idx(self.incidences[f].adjacent.0) .unwrap(); - (v1, v2) + (u, v) } fn edges(&self) -> impl Iterator { @@ -345,9 +345,9 @@ impl GraphTopologyAddition for Graph { }) } - fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge { - let first = self.add_incidence(v1, v2); - self.add_incidence(v2, v1); + fn add_edge(&mut self, u: Self::Vertex, v: Self::Vertex) -> Self::Edge { + let first = self.add_incidence(u, v); + self.add_incidence(v, u); first } } @@ -403,30 +403,25 @@ mod trait_tests { #[cfg(test)] mod tests { use super::*; + use crate::testing::fixtures::MakeTestGraph; #[test] fn incident_vertices_paired_index() { - let mut graph = Graph::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); + let (graph, [v0, v1], e) = Graph::single_edge(); let f = graph .incidences .get_idx(e.arr_idx() + 1) .expect("paired index should be valid"); - let (u1, u2) = graph.incident_vertices(f); + let (u0, u1) = graph.incident_vertices(f); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {f:?}" + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for edge {f:?}" ); } #[test] fn delete_edge_paired_index() { - let mut graph = Graph::new(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); + let (mut graph, _, e) = Graph::single_edge(); let f = graph .incidences .get_idx(e.arr_idx() + 1) @@ -437,9 +432,7 @@ mod tests { #[test] fn delete_edge_loop_paired_index() { - let mut graph = Graph::new(); - let v = graph.add_vertex(); - let e = graph.add_edge(v, v); + let (mut graph, _, e) = Graph::loop_edge(); let f = graph .incidences .get_idx(e.arr_idx() + 1) @@ -450,23 +443,21 @@ mod tests { #[test] fn reused_slot_returns_old_value() { - let mut graph = Graph::new(); - graph.add_vertex(); - let v1 = graph.add_vertex(); + let (mut graph, [_, v], _) = Graph::single_edge(); let mut map = graph.vertex_map(0); - map[v1] = 99; - graph.delete_vertex(v1); - let v2 = graph.add_vertex(); + map[v] = 99; + graph.delete_vertex(v); + let u = graph.add_vertex(); assert_eq!( - v1.arr_idx(), - v2.arr_idx(), - "precondition: new vertex {v1:?} should reuse slot of deleted vertex {v2:?}" + v.arr_idx(), + u.arr_idx(), + "precondition: new vertex {v:?} should reuse slot of deleted vertex {u:?}" ); - // ElementMap uses raw indices, not vertex identity. A new vertex v2 reusing the slot - // of previously deleted v1 sees the old value. Callers must reinitialize stale slots + // ElementMap uses raw indices, not vertex identity. A new vertex u reusing the slot + // of previously deleted v sees the old value. Callers must reinitialize stale slots // after deletion. assert_eq!( - map[v2], 99, + map[u], 99, "new vertex reusing slot of deleted vertex should return old value" ); } diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 3b02869..0701f91 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -58,7 +58,7 @@ impl MakeTestGraph for G { (vertices[7], vertices[8]), (vertices[7], vertices[9]), ] - .map(|(v1, v2)| (graph.add_edge(v1, v2), v1, v2)); + .map(|(u, v)| (graph.add_edge(u, v), u, v)); let i = |vertex, edge| Incidence { vertex, edge }; let incidences = [ vec![i(vertices[1], edges[0].0), i(vertices[1], edges[1].0)], @@ -323,13 +323,13 @@ where // Walks the path: tracks current vertex, confirms each edge is incident to it. let mut current = source; for (i, &e) in path.iter().enumerate() { - let (v1, v2) = graph.incident_vertices(e); - assert_ne!(v1, v2, "path should not contain loop edge {e:?}"); + let (u, v) = graph.incident_vertices(e); + assert_ne!(u, v, "path should not contain loop edge {e:?}"); assert!( - v1 == current || v2 == current, - "path edge {e:?} (index {i}, vertices {v1:?} to {v2:?}) not incident to vertex {current:?}" + u == current || v == current, + "path edge {e:?} (index {i}, vertices {u:?} to {v:?}) not incident to vertex {current:?}" ); - current = if v1 == current { v2 } else { v1 }; + current = if u == current { v } else { u }; } assert!( targets.contains(¤t), diff --git a/src/testing/graph_topology_addition_deletion_testing.rs b/src/testing/graph_topology_addition_deletion_testing.rs index a7b9b72..a6e6643 100644 --- a/src/testing/graph_topology_addition_deletion_testing.rs +++ b/src/testing/graph_topology_addition_deletion_testing.rs @@ -50,11 +50,11 @@ pub fn delete_edge_add_edge() where G::Edge: Debug, { - let (mut graph, vertices, e) = G::single_edge(); + let (mut graph, [v0, v1], e) = G::single_edge(); graph.delete_edge(e); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); assert_ne!( - graph.add_edge(vertices[0], vertices[1]), + graph.add_edge(v0, v1), e, "unexpected duplicate edge after re-add" ); diff --git a/src/testing/graph_topology_addition_testing.rs b/src/testing/graph_topology_addition_testing.rs index e48b7c7..77f68b1 100644 --- a/src/testing/graph_topology_addition_testing.rs +++ b/src/testing/graph_topology_addition_testing.rs @@ -77,12 +77,12 @@ pub fn reserve_edges_increases_capacity() { pub fn reserve_edges_prevents_reallocation_on_add() { let mut graph = G::default(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); + let u = graph.add_vertex(); + let v = graph.add_vertex(); graph.reserve_edges(10); let capacity_before = graph.edge_capacity(); for _ in 0..10 { - graph.add_edge(v1, v2); + graph.add_edge(u, v); } assert_eq!( graph.edge_capacity(), @@ -116,10 +116,10 @@ where G::Edge: Debug, { let mut graph = G::default(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); - let e = graph.add_edge(v1, v2); - assert_ne!(graph.add_edge(v1, v2), e, "unexpected duplicate edge"); + let u = graph.add_vertex(); + let v = graph.add_vertex(); + let e = graph.add_edge(u, v); + assert_ne!(graph.add_edge(u, v), e, "unexpected duplicate edge"); } pub fn vertex_map_new_vertex() { @@ -139,10 +139,10 @@ pub fn vertex_map_new_vertex() { pub fn edge_map_new_edge() { let mut graph = G::default(); - let v1 = graph.add_vertex(); - let v2 = graph.add_vertex(); + let u = graph.add_vertex(); + let v = graph.add_vertex(); let mut map = graph.edge_map(28); - let e = graph.add_edge(v1, v2); + let e = graph.add_edge(u, v); assert_eq!(map[e], 28); map[e] = 8; assert_eq!( diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs index c51b42a..d1d82e7 100644 --- a/src/testing/graph_topology_deletion_testing.rs +++ b/src/testing/graph_topology_deletion_testing.rs @@ -136,7 +136,7 @@ where } pub fn delete_edge() { - let (mut graph, vertices, e) = G::single_edge(); + let (mut graph, [v0, v1], e) = G::single_edge(); assert_eq!( graph.vertex_count(), 2, @@ -144,16 +144,16 @@ pub fn delete_edge() { ); assert_eq!(graph.edge_count(), 1, "unexpected edge count before delete"); assert!( - graph.are_adjacent(vertices[0], vertices[1]), + graph.are_adjacent(v0, v1), "expected vertices to be adjacent before delete" ); assert_eq!( - graph.degree(vertices[0]), + graph.degree(v0), 1, "unexpected vertex degree before delete" ); assert_eq!( - graph.degree(vertices[1]), + graph.degree(v1), 1, "unexpected vertex degree before delete" ); @@ -165,19 +165,11 @@ pub fn delete_edge() { ); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); assert!( - !graph.are_adjacent(vertices[0], vertices[1]), + !graph.are_adjacent(v0, v1), "unexpected adjacency after delete" ); - assert_eq!( - graph.degree(vertices[0]), - 0, - "unexpected vertex degree after delete" - ); - assert_eq!( - graph.degree(vertices[1]), - 0, - "unexpected vertex degree after delete" - ); + assert_eq!(graph.degree(v0), 0, "unexpected vertex degree after delete"); + assert_eq!(graph.degree(v1), 0, "unexpected vertex degree after delete"); } pub fn delete_edge_loop() { @@ -209,7 +201,7 @@ pub fn delete_edge_loop() { pub fn delete_edge_multiple() { const K: usize = 2; - let (mut graph, vertices, edges) = G::multiple_edges::(); + let (mut graph, [v0, v1], edges) = G::multiple_edges::(); assert_eq!( graph.vertex_count(), 2, @@ -217,16 +209,16 @@ pub fn delete_edge_multiple() ); assert_eq!(graph.edge_count(), K, "unexpected edge count before delete"); assert!( - graph.are_adjacent(vertices[0], vertices[1]), + graph.are_adjacent(v0, v1), "expected vertices to be adjacent before delete" ); assert_eq!( - graph.degree(vertices[0]), + graph.degree(v0), K, "unexpected vertex degree before delete" ); assert_eq!( - graph.degree(vertices[1]), + graph.degree(v1), K, "unexpected vertex degree before delete" ); @@ -242,16 +234,16 @@ pub fn delete_edge_multiple() "unexpected edge count after delete" ); assert!( - graph.are_adjacent(vertices[0], vertices[1]), + graph.are_adjacent(v0, v1), "expected vertices to be adjacent after delete" ); assert_eq!( - graph.degree(vertices[0]), + graph.degree(v0), K - 1, "unexpected vertex degree after delete" ); assert_eq!( - graph.degree(vertices[1]), + graph.degree(v1), K - 1, "unexpected vertex degree after delete" ); @@ -305,11 +297,11 @@ where let (mut graph, _, edges, _) = G::standard(); let e = edges[2].0; graph.delete_edge(e); - for &(f, v1, v2) in edges.iter().filter(|&&(f, _, _)| f != e) { - let (u1, u2) = graph.incident_vertices(f); + for &(f, v0, v1) in edges.iter().filter(|&&(f, _, _)| f != e) { + let (u0, u1) = graph.incident_vertices(f); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {f:?} after delete" + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for edge {f:?} after delete" ); } } diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index b3bb449..415fc52 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -135,25 +135,12 @@ where G::Vertex: Debug, { const K: usize = 3; - let (graph, vertices, _) = G::multiple_edges::(); + let (graph, [v0, v1], _) = G::multiple_edges::(); assert_eq!(graph.vertex_count(), 2, "unexpected vertex count"); assert_eq!(graph.edge_count(), K, "unexpected edge count"); - assert_eq!( - graph.degree(vertices[0]), - K, - "unexpected degree of vertex {:?}", - vertices[0] - ); - assert_eq!( - graph.degree(vertices[1]), - K, - "unexpected degree of vertex {:?}", - vertices[1] - ); - assert!( - graph.are_adjacent(vertices[0], vertices[1]), - "should be adjacent" - ); + assert_eq!(graph.degree(v0), K, "unexpected degree of vertex {v0:?}"); + assert_eq!(graph.degree(v1), K, "unexpected degree of vertex {v1:?}",); + assert!(graph.are_adjacent(v0, v1), "should be adjacent"); } pub fn are_adjacent_vertex_self() { @@ -165,15 +152,9 @@ pub fn are_adjacent_vertex_self() { } pub fn are_adjacent_single_edge() { - let (graph, vertices, _) = G::single_edge(); - assert!( - graph.are_adjacent(vertices[0], vertices[1]), - "should be adjacent" - ); - assert!( - graph.are_adjacent(vertices[1], vertices[0]), - "should be adjacent" - ); + let (graph, [v0, v1], _) = G::single_edge(); + assert!(graph.are_adjacent(v0, v1), "should be adjacent"); + assert!(graph.are_adjacent(v1, v0), "should be adjacent"); } pub fn are_adjacent() @@ -341,11 +322,11 @@ where G::Vertex: Debug, G::Edge: Debug, { - let (graph, vertices, e) = G::single_edge(); - let (u, v) = graph.incident_vertices(e); + let (graph, [v0, v1], e) = G::single_edge(); + let (u0, u1) = graph.incident_vertices(e); assert!( - (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), - "unexpected incident vertices {u:?} and {v:?} for edge {e:?}" + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for edge {e:?}" ); } @@ -367,20 +348,16 @@ where G::Vertex: Debug, G::Edge: Debug, { - let (graph, vertices, edges) = G::multiple_edges::<2>(); + let (graph, [v0, v1], edges) = G::multiple_edges::<2>(); assert_ne!(edges[0], edges[1], "edges should be distinct"); - let (u, v) = graph.incident_vertices(edges[0]); - assert!( - (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), - "unexpected incident vertices {u:?} and {v:?} for first multi-edge {:?}", - edges[0] - ); - let (u, v) = graph.incident_vertices(edges[1]); - assert!( - (u == vertices[0] && v == vertices[1]) || (u == vertices[1] && v == vertices[0]), - "unexpected incident vertices {u:?} and {v:?} for second multi-edge {:?}", - edges[1] - ); + for i in 0..2 { + let (u0, u1) = graph.incident_vertices(edges[i]); + assert!( + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for multi-edge {:?} (index {i})", + edges[i] + ); + } } pub fn incident_vertices() @@ -389,11 +366,11 @@ where G::Edge: Debug, { let (graph, _, edges, _) = G::standard(); - for &(e, v1, v2) in edges.iter() { - let (u1, u2) = graph.incident_vertices(e); + for &(e, v0, v1) in edges.iter() { + let (u0, u1) = graph.incident_vertices(e); assert!( - (u1 == v1 && u2 == v2) || (u1 == v2 && u2 == v1), - "unexpected incident vertices {u1:?} and {u2:?} for edge {e:?}" + (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), + "unexpected incident vertices {u0:?} and {u1:?} for edge {e:?}" ); } } @@ -741,8 +718,8 @@ where } pub fn incidence_cursor_copy() { - let (graph, vertices, _) = G::two_edge_path(); - let mut c1 = graph.incidence_cursor(vertices[1]); + let (graph, [_, v, _], _) = G::two_edge_path(); + let mut c1 = graph.incidence_cursor(v); assert!(c1.next(&graph).is_some(), "expected first incidence"); let mut c2 = c1; assert!( diff --git a/src/traits.rs b/src/traits.rs index fda781a..9fdfccd 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -101,14 +101,14 @@ pub trait GraphTopology { /// Panics if `v` is not a valid vertex of this graph. fn degree(&self, v: Self::Vertex) -> usize; - /// Returns `true` if there is at least one edge between `v1` and `v2`, and `false` otherwise. + /// Returns `true` if there is at least one edge between `u` and `v`, and `false` otherwise. /// /// A vertex is adjacent to itself if and only if it has a loop edge. /// /// # Panics /// - /// Panics if `v1` or `v2` is not a valid vertex of this graph. - fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool; + /// Panics if `u` or `v` is not a valid vertex of this graph. + fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool; /// Returns an iterator over all vertices in the graph. fn vertices(&self) -> impl Iterator; @@ -206,12 +206,12 @@ pub trait GraphTopologyAddition: GraphTopology + Default { /// Adds a new isolated vertex and returns its handle. fn add_vertex(&mut self) -> Self::Vertex; - /// Adds a new edge between `v1` and `v2` and returns its handle. + /// Adds a new edge between `u` and `v` and returns its handle. /// /// # Panics /// - /// Panics if `v1` or `v2` is not a valid vertex of this graph. - fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge; + /// Panics if `u` or `v` is not a valid vertex of this graph. + fn add_edge(&mut self, u: Self::Vertex, v: Self::Vertex) -> Self::Edge; } /// A trait that adds deletion operations to an undirected graph topology. diff --git a/tests/bfs.rs b/tests/bfs.rs index 53a1dfe..72cec83 100644 --- a/tests/bfs.rs +++ b/tests/bfs.rs @@ -161,9 +161,9 @@ fn bfs_find_source() { } fn bfs_find_disconnected() { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::bfs_find(&graph, vertices[0], vertices[1]), + algorithms::bfs_find(&graph, v0, v1), None, "disconnected target should not be found" ); @@ -196,12 +196,11 @@ fn bfs_find_where_disconnected() where G::Vertex: Debug, { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::bfs_find_where(&graph, vertices[0], |v| v == vertices[1]), + algorithms::bfs_find_where(&graph, v0, |v| v == v1), None, - "disconnected vertex {:?} should not be found", - vertices[1] + "disconnected vertex {v1:?} should not be found" ); } @@ -209,9 +208,9 @@ fn bfs_find_where_no_match() where G::Vertex: Debug, { - let (graph, vertices, _, _) = G::standard(); + let (graph, [v0, ..], _, _) = G::standard(); assert_eq!( - algorithms::bfs_find_where(&graph, vertices[0], |_| false), + algorithms::bfs_find_where(&graph, v0, |_| false), None, "no vertex should match an always-false predicate" ); @@ -258,9 +257,9 @@ fn bfs_find_path_disconnected() where G::Edge: Debug, { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::bfs_find_path(&graph, vertices[0], vertices[1]), + algorithms::bfs_find_path(&graph, v0, v1), None, "no path should exist to disconnected vertex" ); @@ -270,8 +269,8 @@ fn bfs_find_path_adjacent() where G::Edge: Debug, { - let (graph, vertices, e) = G::single_edge(); - let path = algorithms::bfs_find_path(&graph, vertices[0], vertices[1]) + let (graph, [v0, v1], e) = G::single_edge(); + let path = algorithms::bfs_find_path(&graph, v0, v1) .expect("path should exist between adjacent vertices"); assert_eq!(path, [e], "path should contain only the connecting edge"); } @@ -308,9 +307,9 @@ fn bfs_find_path_where_disconnected() where G::Edge: Debug, { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::bfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), + algorithms::bfs_find_path_where(&graph, v0, |v| v == v1), None, "no path should exist to disconnected vertex" ); @@ -320,9 +319,9 @@ fn bfs_find_path_where_no_match() where G::Edge: Debug, { - let (graph, vertices, _, _) = G::standard(); + let (graph, [v0, ..], _, _) = G::standard(); assert_eq!( - algorithms::bfs_find_path_where(&graph, vertices[0], |_| false), + algorithms::bfs_find_path_where(&graph, v0, |_| false), None, "no path should exist when predicate never matches" ); diff --git a/tests/dfs.rs b/tests/dfs.rs index d49bbcb..25fa58b 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -150,9 +150,9 @@ fn dfs_find_source() { } fn dfs_find_disconnected() { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert!( - !algorithms::dfs_find(&graph, vertices[0], vertices[1]), + !algorithms::dfs_find(&graph, v0, v1), "disconnected target should not be found" ); } @@ -188,12 +188,11 @@ fn dfs_find_where_disconnected() where G::Vertex: Debug, { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), + algorithms::dfs_find_where(&graph, v0, |v| v == v1), None, - "disconnected vertex {:?} should not be found", - vertices[1] + "disconnected vertex {v1:?} should not be found" ); } @@ -201,9 +200,9 @@ fn dfs_find_where_no_match() where G::Vertex: Debug, { - let (graph, vertices, _, _) = G::standard(); + let (graph, [v0, ..], _, _) = G::standard(); assert_eq!( - algorithms::dfs_find_where(&graph, vertices[0], |_| false), + algorithms::dfs_find_where(&graph, v0, |_| false), None, "no vertex should match an always-false predicate" ); @@ -213,10 +212,10 @@ fn dfs_find_where_adjacent() where G::Vertex: Debug, { - let (graph, vertices, _, _) = G::standard(); + let (graph, [v0, v1, ..], _, _) = G::standard(); assert_eq!( - algorithms::dfs_find_where(&graph, vertices[0], |v| v == vertices[1]), - Some(vertices[1]), + algorithms::dfs_find_where(&graph, v0, |v| v == v1), + Some(v1), "expected to find adjacent vertex" ); } @@ -249,9 +248,9 @@ fn dfs_find_path_disconnected() where G::Edge: Debug, { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::dfs_find_path(&graph, vertices[0], vertices[1]), + algorithms::dfs_find_path(&graph, v0, v1), None, "no path should exist to disconnected vertex" ); @@ -261,8 +260,8 @@ fn dfs_find_path_adjacent() where G::Edge: Debug, { - let (graph, vertices, e) = G::single_edge(); - let path = algorithms::dfs_find_path(&graph, vertices[0], vertices[1]) + let (graph, [v0, v1], e) = G::single_edge(); + let path = algorithms::dfs_find_path(&graph, v0, v1) .expect("path should exist between adjacent vertices"); assert_eq!(path, [e], "path should contain only the connecting edge"); } @@ -293,9 +292,9 @@ fn dfs_find_path_where_disconnected() where G::Edge: Debug, { - let (graph, vertices) = G::disconnected(); + let (graph, [v0, v1, _]) = G::disconnected(); assert_eq!( - algorithms::dfs_find_path_where(&graph, vertices[0], |v| v == vertices[1]), + algorithms::dfs_find_path_where(&graph, v0, |v| v == v1), None, "no path should exist to disconnected vertex" ); @@ -305,9 +304,9 @@ fn dfs_find_path_where_no_match() where G::Edge: Debug, { - let (graph, vertices, _, _) = G::standard(); + let (graph, [v0, ..], _, _) = G::standard(); assert_eq!( - algorithms::dfs_find_path_where(&graph, vertices[0], |_| false), + algorithms::dfs_find_path_where(&graph, v0, |_| false), None, "no path should exist when predicate never matches" ); -- 2.43.0 From e593298eba6d80aff80b2c8660c7add0cd1d9a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 19 Sep 2026 17:19:47 +0200 Subject: [PATCH 43/67] Add Clone to graph models, and tests --- src/models/append_graph.rs | 10 +++ src/models/frozen_graph.rs | 8 ++ src/models/graph.rs | 10 +++ src/testing.rs | 2 + .../graph_topology_addition_clone_testing.rs | 56 +++++++++++++ src/testing/graph_topology_clone_testing.rs | 78 +++++++++++++++++++ 6 files changed, 164 insertions(+) create mode 100644 src/testing/graph_topology_addition_clone_testing.rs create mode 100644 src/testing/graph_topology_clone_testing.rs diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index c20ed1d..dae6622 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -43,6 +43,7 @@ pub type AppendGraphEdgeMap = ElementMap; /// An [`Incidence`] for [`AppendGraph`]. pub type AppendGraphIncidence = Incidence; +#[derive(Clone)] struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, @@ -101,11 +102,18 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// Both [`add_vertex`] and [`add_edge`] run in amortised *O(1)* time. [`degree`] runs in *O(1)* /// time since vertex degrees are stored. Space complexity is *O(|V| + |E|)*. /// +/// # Cloning +/// +/// [`Vertex`] and [`Edge`] handles obtained from a clone are valid, and refer to the same vertex or +/// edge in the original, and vice versa. However, once cloned, the two instances no longer affect +/// each other. +/// /// [`add_vertex`]: Self::add_vertex /// [`add_edge`]: Self::add_edge /// [`degree`]: Self::degree /// [`FrozenGraph`]: crate::models::FrozenGraph /// [`Graph`]: crate::models::Graph +#[derive(Clone)] pub struct AppendGraph { vertices: Vec, incidences: Vec, @@ -257,6 +265,8 @@ mod trait_tests { crate::graph_topology_tests!(AppendGraph); crate::graph_topology_addition_tests!(AppendGraph); + crate::graph_topology_clone_tests!(AppendGraph); + crate::graph_topology_addition_clone_tests!(AppendGraph); } #[cfg(test)] diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 5b62182..d090a39 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -87,9 +87,16 @@ impl IncidenceCursor for FrozenGraphIncidenceCursor { /// /// [`degree`] runs in *O(1)* time. Space complexity is *O(|V| + |E|)*. /// +/// # Cloning +/// +/// [`Vertex`] and [`Edge`] handles obtained from a clone are valid, and refer to the same vertex or +/// edge in the original, and vice versa. However, once cloned, the two instances no longer affect +/// each other. +/// /// [`degree`]: Self::degree /// [`AppendGraph`]: crate::models::AppendGraph /// [`Graph`]: crate::models::Graph +#[derive(Clone)] pub struct FrozenGraph { vertices: Vec, incidences: Vec, @@ -255,6 +262,7 @@ mod trait_tests { use super::FrozenGraph; crate::graph_topology_tests!(FrozenGraph); + crate::graph_topology_clone_tests!(FrozenGraph); } #[cfg(test)] diff --git a/src/models/graph.rs b/src/models/graph.rs index 3895d80..ebfdfe1 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -49,6 +49,7 @@ impl IncidenceSlot { /// Referenced by [`Vertex`] as a type parameter of the underlying arena. Not intended for direct /// external use. #[doc(hidden)] +#[derive(Clone)] pub struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, @@ -129,12 +130,19 @@ impl IncidenceCursor for GraphIncidenceCursor { /// planned, performing deletions first allows freed slots to be reused by subsequent additions, /// potentially avoiding reallocation. /// +/// # Cloning +/// +/// [`Vertex`] and [`Edge`] handles obtained from a clone are valid, and refer to the same vertex or +/// edge in the original, and vice versa. However, once cloned, the two instances no longer affect +/// each other. +/// /// [`add_vertex`]: Self::add_vertex /// [`add_edge`]: Self::add_edge /// [`degree`]: Self::degree /// [`delete_vertex`]: Self::delete_vertex /// [`delete_edge`]: Self::delete_edge /// [`AppendGraph`]: crate::models::AppendGraph +#[derive(Clone)] pub struct Graph { // TODO: Arena index and generation types could be externalized to Graph. vertices: Arena, @@ -398,6 +406,8 @@ mod trait_tests { crate::graph_topology_addition_tests!(Graph); crate::graph_topology_deletion_tests!(Graph); crate::graph_topology_addition_deletion_tests!(Graph); + crate::graph_topology_clone_tests!(Graph); + crate::graph_topology_addition_clone_tests!(Graph); } #[cfg(test)] diff --git a/src/testing.rs b/src/testing.rs index 7b6b7d6..1049f84 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,6 +1,8 @@ //! Test fixture and test macros for graph topology and algorithm implementations. pub mod fixtures; +pub mod graph_topology_addition_clone_testing; +pub mod graph_topology_clone_testing; pub mod graph_topology_addition_deletion_testing; pub mod graph_topology_addition_testing; pub mod graph_topology_deletion_testing; diff --git a/src/testing/graph_topology_addition_clone_testing.rs b/src/testing/graph_topology_addition_clone_testing.rs new file mode 100644 index 0000000..b476dfe --- /dev/null +++ b/src/testing/graph_topology_addition_clone_testing.rs @@ -0,0 +1,56 @@ +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::GraphTopologyAddition; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_clone_tests { + ($T:ty) => { + $crate::graph_topology_addition_clone_tests!(@wrap $T, + clone_independent, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_addition_clone_testing::$name::<$T>(); + } + )* + }; +} + +pub fn clone_independent() { + let (mut graph, [u, v], _) = G::single_edge(); + let mut clone = graph.clone(); + + let w = clone.add_vertex(); + clone.add_edge(u, w); + assert_eq!(clone.vertex_count(), 3, "precondition: vertex count of clone"); + assert_eq!(clone.edge_count(), 2, "precondition: edge count of clone"); + assert_eq!( + graph.vertex_count(), + 2, + "mutating the clone should not affect the vertex count of the original" + ); + assert_eq!( + graph.edge_count(), + 1, + "mutating the clone should not affect the edge count of the original" + ); + + let x = graph.add_vertex(); + graph.add_edge(v, x); + assert_eq!(graph.vertex_count(), 3, "precondition: vertex count of original"); + assert_eq!(graph.edge_count(), 2, "precondition: edge count of original"); + assert_eq!( + clone.vertex_count(), + 3, + "mutating the original should not affect the vertex count of the clone" + ); + assert_eq!( + clone.edge_count(), + 2, + "mutating the original should not affect the edge count of the clone" + ); +} diff --git a/src/testing/graph_topology_clone_testing.rs b/src/testing/graph_topology_clone_testing.rs new file mode 100644 index 0000000..12b9606 --- /dev/null +++ b/src/testing/graph_topology_clone_testing.rs @@ -0,0 +1,78 @@ +use std::fmt::Debug; + +use crate::testing::fixtures::MakeTestGraph; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_clone_tests { + ($T:ty) => { + $crate::graph_topology_clone_tests!(@wrap $T, + clone_preserves_vertex_and_edge_count, + clone_handles_interchangeable, + ); + }; + + (@wrap $T:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_clone_testing::$name::<$T>(); + } + )* + }; +} + +pub fn clone_preserves_vertex_and_edge_count() { + let (graph, ..) = G::standard(); + let clone = graph.clone(); + assert_eq!( + clone.vertex_count(), + graph.vertex_count(), + "clone should have the same vertex count as the original" + ); + assert_eq!( + clone.edge_count(), + graph.edge_count(), + "clone should have the same edge count as the original" + ); +} + +/// Queries the clone using vertex and edge handles obtained from the original graph, and asserts +/// that the results are identical to querying the original with those same handles. This is only +/// meaningful because Vertex and Edge handles are not tied to a particular graph instance. +pub fn clone_handles_interchangeable() +where + G::Vertex: Debug, + G::Edge: Debug, +{ + let (graph, vertices, edges, _) = G::standard(); + let clone = graph.clone(); + for &v in vertices.iter() { + assert_eq!( + clone.degree(v), + graph.degree(v), + "vertex {v:?} should have the same degree in the clone as in the original" + ); + let mut expected: Vec<_> = graph.incidences(v).collect(); + for incidence in clone.incidences(v) { + let pos = expected + .iter() + .position(|x| *x == incidence) + .expect(&format!( + "unexpected incidence {incidence:?} of vertex {v:?} in the clone" + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incidences {expected:?} of vertex {v:?} are missing in the clone" + ); + } + for &(e, u, v) in edges.iter() { + assert_eq!( + clone.incident_vertices(e), + (u, v), + "edge {e:?} should have the same incident vertices in the clone as in the original" + ); + } +} -- 2.43.0 From 038e984c97a5467f4a24015e62a862fd70fadfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 19 Sep 2026 17:53:44 +0200 Subject: [PATCH 44/67] Add FromGraph trait, and add implementation for FrozenGraph --- src/lib.rs | 3 +- src/models/frozen_graph.rs | 131 ++++++++++++++++++------------------- src/testing/fixtures.rs | 2 +- src/traits.rs | 33 ++++++++++ 4 files changed, 99 insertions(+), 70 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d2ea1fb..1fadff8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,8 @@ pub mod traits; /// Convenience re-exports of graph topology traits for common use. pub mod prelude { pub use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, IncidenceCursor, + FromGraph, GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, + IncidenceCursor, }; } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index d090a39..f87d737 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -3,7 +3,7 @@ use std::ops::IndexMut; use crate::maps::ElementMap; -use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; +use crate::traits::{FromGraph, GraphTopology, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in a [`FrozenGraph`]. /// @@ -103,67 +103,6 @@ pub struct FrozenGraph { } impl FrozenGraph { - // TODO: Move this to a new FromGraph trait and adapt the MakeTestGraph impl to use helper functions like "fn fixture_empty() -> G { G::from_graph(&AppendGraph::empty()).0 }". - /// Creates a graph instance as a copy of another graph. - /// - /// Returns the copied graph instance and two mappings of the `source` graph vertices and edges - /// to the new ones. Note that the returned mappings cannot support additional growth, i.e. any - /// vertex or edge added to the `source` graph after calling `from_graph` will map to an invalid - /// vertex or edge in the new `FrozenGraph`. - pub fn from_graph( - source: &G, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { - // Builds vertex list with sentinel and vertex map. - let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); - let mut vertices = Vec::with_capacity(source.vertex_count() + 1); - let mut offset = 0; - for (i, v) in source.vertices().enumerate() { - vertex_map[v] = Vertex(i); - vertices.push(offset); - offset += source.degree(v); - } - vertices.push(offset); - - // Builds incidence list and edge map. Uses "usize::MAX" as "unset" here: an edge can never - // be normalized to "usize::MAX", since it must be the smaller index of two incidences. - const UNSET: usize = usize::MAX; - let mut edge_map = source.edge_map(Edge(UNSET)); - let mut incidences = Vec::with_capacity(source.edge_count() * 2); - for v in source.vertices() { - for Incidence { vertex: u, edge: e } in source.incidences(v) { - let em = edge_map.index_mut(e); - if em.0 == UNSET { - // Adds the first incidence for the edge, but we don't know the opposite, yet. - *em = Edge(incidences.len()); - incidences.push(IncidenceEntry { - adjacent: vertex_map[u], - opposite: 0, - }); - } else { - // Adds the second incidence for the edge and fills the opposite for the first. - incidences[em.0].opposite = incidences.len(); - incidences.push(IncidenceEntry { - adjacent: vertex_map[u], - opposite: em.0, - }); - } - } - } - - ( - Self { - vertices, - incidences, - }, - vertex_map, - edge_map, - ) - } - fn raw_incidences(&self, v: Vertex) -> impl Iterator { self.vertices[v.0]..self.vertices[v.0 + 1] } @@ -186,12 +125,6 @@ impl FrozenGraph { } } -impl From<&G> for FrozenGraph { - fn from(graph: &G) -> Self { - Self::from_graph(graph).0 - } -} - impl GraphTopology for FrozenGraph { type Vertex = Vertex; type Edge = Edge; @@ -257,6 +190,68 @@ impl GraphTopology for FrozenGraph { } } +impl From<&G> for FrozenGraph { + fn from(graph: &G) -> Self { + Self::from_graph(graph).0 + } +} + +impl FromGraph for FrozenGraph { + fn from_graph( + source: &G, + ) -> ( + Self, + ElementMap, + ElementMap, + ) { + // Builds vertex list with sentinel and vertex map. + let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); + let mut vertices = Vec::with_capacity(source.vertex_count() + 1); + let mut offset = 0; + for (i, v) in source.vertices().enumerate() { + vertex_map[v] = Vertex(i); + vertices.push(offset); + offset += source.degree(v); + } + vertices.push(offset); + + // Builds incidence list and edge map. Uses "usize::MAX" as "unset" here: an edge can never + // be normalized to "usize::MAX", since it must be the smaller index of two incidences. + const UNSET: usize = usize::MAX; + let mut edge_map = source.edge_map(Edge(UNSET)); + let mut incidences = Vec::with_capacity(source.edge_count() * 2); + for v in source.vertices() { + for Incidence { vertex: u, edge: e } in source.incidences(v) { + let em = edge_map.index_mut(e); + if em.0 == UNSET { + // Adds the first incidence for the edge, but we don't know the opposite, yet. + *em = Edge(incidences.len()); + incidences.push(IncidenceEntry { + adjacent: vertex_map[u], + opposite: 0, + }); + } else { + // Adds the second incidence for the edge and fills the opposite for the first. + incidences[em.0].opposite = incidences.len(); + incidences.push(IncidenceEntry { + adjacent: vertex_map[u], + opposite: em.0, + }); + } + } + } + + ( + Self { + vertices, + incidences, + }, + vertex_map, + edge_map, + ) + } +} + #[cfg(test)] mod trait_tests { use super::FrozenGraph; diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 0701f91..f60dcf3 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use crate::maps::ElementMap; use crate::models::{AppendGraph, FrozenGraph}; -use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence}; +use crate::traits::{FromGraph, GraphTopology, GraphTopologyAddition, Incidence}; pub trait MakeTestGraph: GraphTopology + Sized { fn standard() -> ( diff --git a/src/traits.rs b/src/traits.rs index 9fdfccd..961f40a 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -237,6 +237,39 @@ pub trait GraphTopologyDeletion: GraphTopology { fn delete_edge(&mut self, e: Self::Edge); } +/// A trait for constructing an undirected graph topology by converting another graph +/// implementation to this one. +pub trait FromGraph: GraphTopology { + /// Creates a new graph instance that is isomorphic to `source`. + /// + /// Returns the copied graph instance and two mappings from the vertices and edges of `source` + /// to the corresponding vertices and edges of the returned graph. + /// + /// # Examples + /// + /// ``` + /// use grapherity::prelude::*; + /// use grapherity::models::{AppendGraph, FrozenGraph}; + /// + /// let mut source = AppendGraph::new(); + /// let u = source.add_vertex(); + /// let v = source.add_vertex(); + /// let e = source.add_edge(u, v); + /// let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&source); + /// + /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); + /// ``` + fn from_graph( + source: &G, + ) -> ( + Self, + ElementMap, + ElementMap, + ) + where + Self: Sized; +} + /// A cursor for traversing the incidences of a vertex one step at a time. /// /// Because the cursor is [`Copy`], its state can be saved and restored to replay or branch a -- 2.43.0 From 345dbc0d5d36dad806ed2efae5f4585bc5db2079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 19 Sep 2026 17:59:27 +0200 Subject: [PATCH 45/67] Add ElementMap::map --- src/maps.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/maps.rs b/src/maps.rs index c96701d..debddc8 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -52,6 +52,32 @@ impl ElementMap { pub fn reserve(&mut self, additional: usize) { self.data.reserve(additional); } + + /// Returns a new map over the same elements with values transformed by `f`. + /// + /// The default value of the new map is `f` applied to the original default, so reads beyond the + /// current capacity of the new map behave consistently with the transformation. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::maps::ElementMap; + /// let mut map: ElementMap> = ElementMap::new(None, |e| e, 0); + /// map[1] = Some(42); + /// + /// let bools = map.map(|v| v.is_some()); + /// assert_eq!(bools[0], false); + /// assert_eq!(bools[1], true); + /// // Returns mapped default for reads beyond capacity. + /// assert_eq!(bools[9], false); + /// ``` + pub fn map(&self, f: impl Fn(&T) -> U) -> ElementMap { + ElementMap { + data: self.data.iter().map(|v| f(v)).collect(), + default: f(&self.default), + to_index: self.to_index, + } + } } impl Index for ElementMap { @@ -131,4 +157,19 @@ mod tests { assert_eq!(map[1], 7, "unexpected read value after write"); assert_eq!(map[0], 0, "initial read should be default"); } + + #[test] + fn map() { + let mut source: ElementMap> = ElementMap::new(None, |e| e, 0); + source[1] = Some(42); + source[2] = None; + let mapped = source.map(|v| v.is_some()); + assert_eq!(mapped[0], false, "unset entry should map to false"); + assert_eq!(mapped[1], true, "Some entry should map to true"); + assert_eq!(mapped[2], false, "explicit None entry should map to false"); + assert_eq!( + mapped[9], false, + "read beyond capacity should return mapped default" + ); + } } -- 2.43.0 From 64c48287a74cdb993cdd6a84623df30a473b3661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 19 Sep 2026 18:07:33 +0200 Subject: [PATCH 46/67] Fix ElementMap capacity in FrozenGraph::edge_map --- src/models/frozen_graph.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index f87d737..220d6c1 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -142,8 +142,9 @@ impl GraphTopology for FrozenGraph { self.incidences.len() / 2 } + // TODO: Find a way to reduce the needed capacity for the edge ElementMap. This could be improved by finding a small vertex cover and store those vertices first. See https://en.wikipedia.org/wiki/Vertex_cover#Approximate_evaluation fn edge_map(&self, default: T) -> ElementMap { - ElementMap::new(default, |e| e.0, self.edge_count()) + ElementMap::new(default, |e| e.0, self.incidences.len() - 1) } fn degree(&self, v: Self::Vertex) -> usize { -- 2.43.0 From 0dea90ceffe0ab2634dda843da77a724bccda6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 25 Sep 2026 08:32:56 +0200 Subject: [PATCH 47/67] Update Dijkstra's algorithm to take any weights that satisfy the new Weight trait --- src/algorithms.rs | 61 +++++++++++++++++++++++++++++++++++------------ tests/dijkstra.rs | 8 ++++--- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index f42e4ad..d36653d 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -32,19 +32,46 @@ use priority_queue::PriorityQueue; use std::cmp::Reverse; use std::collections::VecDeque; use std::hash::Hash; +use std::ops::Add; use crate::maps::ElementMap; use crate::traits::{GraphTopology, Incidence, IncidenceCursor}; +/// Trait that edge weights for [`dijkstra`] and its variants need to satisfy. +/// +/// [`Default`] is used as the starting distance for `source`, so it must behave as the additive +/// identity (typically zero) for accumulation via [`Add`] to give correct results. +/// +/// # Examples +/// +/// ``` +/// # use grapherity::prelude::*; +/// # use grapherity::algorithms::dijkstra; +/// # use grapherity::models::Graph; +/// use std::time::Duration; +/// +/// let mut graph = Graph::new(); +/// let source = graph.add_vertex(); +/// let target = graph.add_vertex(); +/// let e = graph.add_edge(source, target); +/// let mut travel_time = graph.edge_map(Duration::ZERO); +/// travel_time[e] = Duration::from_secs(5); +/// +/// let result = dijkstra(&graph, source, |e| travel_time[e]); +/// assert_eq!(result.distances[target], Some(Duration::from_secs(5))); +/// ``` +pub trait Weight: Copy + Ord + Add + Default {} + +impl + Default> Weight for T {} + /// Return data type for [`dijkstra`] and [`dijkstra_unweighted`]. -pub struct DijkstraResult { +pub struct DijkstraResult { /// Vertex map of minimum distances from a given `source` vertex. - pub distances: ElementMap>, + pub distances: ElementMap>, /// Vertex map of predecessors on some shortest path from a given `source` vertex. pub predecessors: ElementMap>, } -// TODO: Generalize the return type of the weight function. // TODO: A Fibonacci heap would lower complexity to O(|E| + |V| log |V|) by making decrease-key O(1) amortized instead of O(log |V|). No standard Rust implementation exists; high constant factors may negate the asymptotic gain in practice. /// [Dijkstra's algorithm] with custom edge weights, returns minimum distances and predecessors. /// @@ -78,11 +105,15 @@ pub struct DijkstraResult { /// ``` /// /// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm -pub fn dijkstra(graph: &G, source: G::Vertex, weights: W) -> DijkstraResult +pub fn dijkstra( + graph: &G, + source: G::Vertex, + weights: impl Fn(G::Edge) -> W, +) -> DijkstraResult where G: GraphTopology, G::Vertex: Hash, - W: Fn(G::Edge) -> u32, + W: Weight, { let mut predecessors = graph.vertex_map(None); let distances = dijkstra_impl(graph, source, weights, |adjacent, predecessor| { @@ -127,12 +158,12 @@ where pub fn dijkstra_distances( graph: &G, source: G::Vertex, - weights: W, -) -> ElementMap> + weights: impl Fn(G::Edge) -> W, +) -> ElementMap> where G: GraphTopology, G::Vertex: Hash, - W: Fn(G::Edge) -> u32, + W: Weight, { dijkstra_impl(graph, source, weights, |_, _| {}) } @@ -168,7 +199,7 @@ where /// ``` /// /// [Dijkstra's algorithm]: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm -pub fn dijkstra_unweighted(graph: &G, source: G::Vertex) -> DijkstraResult +pub fn dijkstra_unweighted(graph: &G, source: G::Vertex) -> DijkstraResult where G: GraphTopology, G::Vertex: Hash, @@ -220,24 +251,24 @@ where fn dijkstra_impl( graph: &G, source: G::Vertex, - weights: W, + weights: impl Fn(G::Edge) -> W, mut on_relax: F, -) -> ElementMap> +) -> ElementMap> where G: GraphTopology, G::Vertex: Hash, - W: Fn(G::Edge) -> u32, + W: Weight, F: FnMut(G::Vertex, G::Vertex), { let mut distances = graph.vertex_map(None); let mut heap = PriorityQueue::new(); - distances[source] = Some(0); - heap.push(source, Reverse(0u32)); + distances[source] = Some(W::default()); + heap.push(source, Reverse(W::default())); while let Some((v, Reverse(v_distance))) = heap.pop() { for Incidence { vertex: u, edge: e } in graph.incidences(v) { - let new_distance = v_distance + weights(e); + let new_distance = weights(e) + v_distance; if match distances[u] { None => true, Some(old_distance) if old_distance > new_distance => true, diff --git a/tests/dijkstra.rs b/tests/dijkstra.rs index 802098a..36a69cc 100644 --- a/tests/dijkstra.rs +++ b/tests/dijkstra.rs @@ -173,7 +173,7 @@ where fixtures::assert_standard_unweighted_distances_v0(|v| distances[v], &vertices); } -fn assert_single_vertex(result: &DijkstraResult, v: G::Vertex) +fn assert_single_vertex(result: &DijkstraResult, v: G::Vertex) where G::Vertex: Debug, { @@ -195,8 +195,10 @@ fn assert_distances_single_vertex( ); } -fn assert_disconnected(result: &DijkstraResult, vertices: &[G::Vertex]) -where +fn assert_disconnected( + result: &DijkstraResult, + vertices: &[G::Vertex], +) where G::Vertex: Debug, { assert_distances_disconnected::(&result.distances, &vertices); -- 2.43.0 From d438a4288aac5c03904874646558a26ba6b706ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 25 Sep 2026 08:36:30 +0200 Subject: [PATCH 48/67] Extract asserts from Clone tests to be reusable --- src/testing/fixtures.rs | 140 ++++++++++++++++++ .../graph_topology_addition_clone_testing.rs | 38 +---- src/testing/graph_topology_clone_testing.rs | 46 +----- 3 files changed, 147 insertions(+), 77 deletions(-) diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index f60dcf3..048d088 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -338,3 +338,143 @@ where } path } + +/// Asserts that the two graphs are isomorphic. +/// +/// `vertices` must contain all vertices of the `original` graph, and `vertex_map` must for any +/// vertex in the `original` graph return the corresponding vertex in the `other` graph. +/// +/// `edges` must contain all edges and their endpoints of the `original` graph, and `edge_map` must +/// for any edge in the `original` graph return the corresponding edge in the `other` graph. +pub fn assert_isomorphic( + original: &G, + other: &H, + vertices: &[G::Vertex], + vertex_map: impl Fn(G::Vertex) -> H::Vertex, + edges: &[(G::Edge, G::Vertex, G::Vertex)], + edge_map: impl Fn(G::Edge) -> H::Edge, +) where + G::Vertex: Debug, + G::Edge: Debug, + H::Vertex: Debug, + H::Edge: Debug, +{ + assert_eq!( + original.vertex_count(), + vertices.len(), + "precondition: all vertices of the original must be provided" + ); + assert_eq!( + original.edge_count(), + edges.len(), + "precondition: all edges of the original must be provided" + ); + assert_eq!( + other.vertex_count(), + original.vertex_count(), + "other graph should have the same vertex count as the original" + ); + assert_eq!( + other.edge_count(), + original.edge_count(), + "other graph should have the same edge count as the original" + ); + for &v in vertices.iter() { + assert_eq!( + other.degree(vertex_map(v)), + original.degree(v), + "vertex {v:?} should have the same degree in the other graph as in the original" + ); + let mut expected: Vec<_> = original.incidences(v).collect(); + for incidence in other.incidences(vertex_map(v)) { + let pos = expected + .iter() + .position(|x| { + vertex_map(x.vertex) == incidence.vertex && edge_map(x.edge) == incidence.edge + }) + .expect(&format!( + "unexpected incidence {incidence:?} of vertex {v:?} in the other graph" + )); + expected.swap_remove(pos); + } + assert!( + expected.is_empty(), + "expected incidences {expected:?} of vertex {v:?} are missing in the other graph" + ); + } + for &(e, u, v) in edges.iter() { + assert_eq!( + other.incident_vertices(edge_map(e)), + (vertex_map(u), vertex_map(v)), + "edge {e:?} should have the same incident vertices in the other graph as in the original" + ); + } +} + +/// Asserts that mutation of the two graphs happens independently. +/// +/// `vertices` must contain all vertices of the `original` graph, which must not be empty, and +/// `vertex_map` must for any vertex in the `original` graph return the corresponding vertex in the +/// `other` graph. +pub fn assert_mutation_independent( + original: &mut G, + other: &mut H, + vertices: &[G::Vertex], + vertex_map: impl Fn(G::Vertex) -> H::Vertex, +) { + let edge_count = original.edge_count(); + assert_eq!( + original.vertex_count(), + vertices.len(), + "precondition: all vertices of the original must be provided" + ); + assert!( + !vertices.is_empty(), + "precondition: at least one vertex must be provided" + ); + let u = other.add_vertex(); + other.add_edge(vertex_map(vertices[0]), u); + assert_eq!( + other.vertex_count(), + vertices.len() + 1, + "precondition: vertex count of other graph" + ); + assert_eq!( + other.edge_count(), + edge_count + 1, + "precondition: edge count of other graph" + ); + assert_eq!( + original.vertex_count(), + vertices.len(), + "mutating the other graph should not affect the vertex count of the original" + ); + assert_eq!( + original.edge_count(), + edge_count, + "mutating the other graph should not affect the edge count of the original" + ); + + let v = original.add_vertex(); + original.add_edge(vertices[0], v); + assert_eq!( + original.vertex_count(), + vertices.len() + 1, + "precondition: vertex count of original" + ); + assert_eq!( + original.edge_count(), + edge_count + 1, + "precondition: edge count of original" + ); + assert_eq!( + other.vertex_count(), + vertices.len() + 1, + "mutating the original should not affect the vertex count of the other graph" + ); + assert_eq!( + other.edge_count(), + edge_count + 1, + "mutating the original should not affect the edge count of the other graph" + ); +} diff --git a/src/testing/graph_topology_addition_clone_testing.rs b/src/testing/graph_topology_addition_clone_testing.rs index b476dfe..2f360eb 100644 --- a/src/testing/graph_topology_addition_clone_testing.rs +++ b/src/testing/graph_topology_addition_clone_testing.rs @@ -1,3 +1,4 @@ +use crate::testing::fixtures; use crate::testing::fixtures::MakeTestGraph; use crate::traits::GraphTopologyAddition; @@ -6,7 +7,7 @@ use crate::traits::GraphTopologyAddition; macro_rules! graph_topology_addition_clone_tests { ($T:ty) => { $crate::graph_topology_addition_clone_tests!(@wrap $T, - clone_independent, + clone_mutation_independent, ); }; @@ -20,37 +21,8 @@ macro_rules! graph_topology_addition_clone_tests { }; } -pub fn clone_independent() { - let (mut graph, [u, v], _) = G::single_edge(); +pub fn clone_mutation_independent() { + let (mut graph, vertices, _) = G::single_edge(); let mut clone = graph.clone(); - - let w = clone.add_vertex(); - clone.add_edge(u, w); - assert_eq!(clone.vertex_count(), 3, "precondition: vertex count of clone"); - assert_eq!(clone.edge_count(), 2, "precondition: edge count of clone"); - assert_eq!( - graph.vertex_count(), - 2, - "mutating the clone should not affect the vertex count of the original" - ); - assert_eq!( - graph.edge_count(), - 1, - "mutating the clone should not affect the edge count of the original" - ); - - let x = graph.add_vertex(); - graph.add_edge(v, x); - assert_eq!(graph.vertex_count(), 3, "precondition: vertex count of original"); - assert_eq!(graph.edge_count(), 2, "precondition: edge count of original"); - assert_eq!( - clone.vertex_count(), - 3, - "mutating the original should not affect the vertex count of the clone" - ); - assert_eq!( - clone.edge_count(), - 2, - "mutating the original should not affect the edge count of the clone" - ); + fixtures::assert_mutation_independent(&mut graph, &mut clone, &vertices, |v| v); } diff --git a/src/testing/graph_topology_clone_testing.rs b/src/testing/graph_topology_clone_testing.rs index 12b9606..b4ffd1e 100644 --- a/src/testing/graph_topology_clone_testing.rs +++ b/src/testing/graph_topology_clone_testing.rs @@ -1,5 +1,6 @@ use std::fmt::Debug; +use crate::testing::fixtures; use crate::testing::fixtures::MakeTestGraph; #[doc(hidden)] @@ -7,7 +8,6 @@ use crate::testing::fixtures::MakeTestGraph; macro_rules! graph_topology_clone_tests { ($T:ty) => { $crate::graph_topology_clone_tests!(@wrap $T, - clone_preserves_vertex_and_edge_count, clone_handles_interchangeable, ); }; @@ -22,21 +22,6 @@ macro_rules! graph_topology_clone_tests { }; } -pub fn clone_preserves_vertex_and_edge_count() { - let (graph, ..) = G::standard(); - let clone = graph.clone(); - assert_eq!( - clone.vertex_count(), - graph.vertex_count(), - "clone should have the same vertex count as the original" - ); - assert_eq!( - clone.edge_count(), - graph.edge_count(), - "clone should have the same edge count as the original" - ); -} - /// Queries the clone using vertex and edge handles obtained from the original graph, and asserts /// that the results are identical to querying the original with those same handles. This is only /// meaningful because Vertex and Edge handles are not tied to a particular graph instance. @@ -47,32 +32,5 @@ where { let (graph, vertices, edges, _) = G::standard(); let clone = graph.clone(); - for &v in vertices.iter() { - assert_eq!( - clone.degree(v), - graph.degree(v), - "vertex {v:?} should have the same degree in the clone as in the original" - ); - let mut expected: Vec<_> = graph.incidences(v).collect(); - for incidence in clone.incidences(v) { - let pos = expected - .iter() - .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {v:?} in the clone" - )); - expected.swap_remove(pos); - } - assert!( - expected.is_empty(), - "expected incidences {expected:?} of vertex {v:?} are missing in the clone" - ); - } - for &(e, u, v) in edges.iter() { - assert_eq!( - clone.incident_vertices(e), - (u, v), - "edge {e:?} should have the same incident vertices in the clone as in the original" - ); - } + fixtures::assert_isomorphic(&graph, &clone, &vertices, |v| v, &edges, |e| e); } -- 2.43.0 From a9190f88dbe364f421ade65ac9dfda5929998fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 25 Sep 2026 08:41:10 +0200 Subject: [PATCH 49/67] Add From and FromGraph tests for FrozenGraph and for GraphTopologyAddition The new tests are analogous to the existing Clone tests. Currently, only FrozenGraph implements From and FromGraph, but not GraphTopologyAddition, so the tests relying on GraphTopologyAddition are currently unused. --- src/models/frozen_graph.rs | 2 + src/testing.rs | 4 +- ...ph_topology_addition_from_graph_testing.rs | 32 ++++++++++ .../graph_topology_from_graph_testing.rs | 61 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/testing/graph_topology_addition_from_graph_testing.rs create mode 100644 src/testing/graph_topology_from_graph_testing.rs diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 220d6c1..9e86039 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -255,10 +255,12 @@ impl FromGraph for FrozenGraph { #[cfg(test)] mod trait_tests { + use crate::models::AppendGraph; use super::FrozenGraph; crate::graph_topology_tests!(FrozenGraph); crate::graph_topology_clone_tests!(FrozenGraph); + crate::graph_topology_from_graph_tests!(FrozenGraph, AppendGraph); } #[cfg(test)] diff --git a/src/testing.rs b/src/testing.rs index 1049f84..8a841dd 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -2,8 +2,10 @@ pub mod fixtures; pub mod graph_topology_addition_clone_testing; -pub mod graph_topology_clone_testing; pub mod graph_topology_addition_deletion_testing; +pub mod graph_topology_addition_from_graph_testing; pub mod graph_topology_addition_testing; +pub mod graph_topology_clone_testing; pub mod graph_topology_deletion_testing; +pub mod graph_topology_from_graph_testing; pub mod graph_topology_testing; diff --git a/src/testing/graph_topology_addition_from_graph_testing.rs b/src/testing/graph_topology_addition_from_graph_testing.rs new file mode 100644 index 0000000..9b5a5b4 --- /dev/null +++ b/src/testing/graph_topology_addition_from_graph_testing.rs @@ -0,0 +1,32 @@ +use crate::testing::fixtures; +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{FromGraph, GraphTopologyAddition}; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_from_graph_tests { + ($T:ty, $U:ty) => { + $crate::graph_topology_addition_from_graph_tests!(@wrap $T, $U, + from_graph_mutation_independent, + ); + }; + + (@wrap $T:ty, $U:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_addition_from_graph_testing::$name::<$T, $U>(); + } + )* + }; +} + +pub fn from_graph_mutation_independent() +where + G: FromGraph + GraphTopologyAddition, + H: MakeTestGraph + GraphTopologyAddition, +{ + let (mut source, vertices, _) = H::single_edge(); + let (mut graph, vertex_map, _) = G::from_graph(&source); + fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| vertex_map[v]); +} diff --git a/src/testing/graph_topology_from_graph_testing.rs b/src/testing/graph_topology_from_graph_testing.rs new file mode 100644 index 0000000..82ef3e0 --- /dev/null +++ b/src/testing/graph_topology_from_graph_testing.rs @@ -0,0 +1,61 @@ +use std::fmt::Debug; + +use crate::testing::fixtures; +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{FromGraph, GraphTopology}; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_from_graph_tests { + ($T:ty, $U:ty) => { + $crate::graph_topology_from_graph_tests!(@wrap $T, $U, + from, + from_graph, + ); + }; + + (@wrap $T:ty, $U:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_from_graph_testing::$name::<$T, $U>(); + } + )* + }; +} + +pub fn from() +where + G: for<'a> From<&'a H> + GraphTopology, + H: MakeTestGraph, +{ + let (source, _, _, _) = H::standard(); + let graph = G::from(&source); + assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); + assert_eq!(graph.edge_count(), 18, "unexpected edge count"); + + let mut expected: Vec<_> = source.vertices().map(|v| source.degree(v)).collect(); + let mut actual: Vec<_> = graph.vertices().map(|v| graph.degree(v)).collect(); + expected.sort(); + actual.sort(); + assert_eq!(actual, expected, "degree sequence should match"); +} + +pub fn from_graph() +where + G::Vertex: Debug, + G::Edge: Debug, + H::Vertex: Debug, + H::Edge: Debug, +{ + let (source, vertices, edges, _) = H::standard(); + let (graph, vertex_map, edge_map) = G::from_graph(&source); + fixtures::assert_isomorphic( + &source, + &graph, + &vertices, + |v| vertex_map[v], + &edges, + |e| edge_map[e], + ); +} -- 2.43.0 From 34782749594198f3144d0ac89a5012917e7ad246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 28 Sep 2026 23:58:30 +0200 Subject: [PATCH 50/67] Add From and FromGraph implementations for Graph and AppendGraph --- src/models.rs | 35 +++++++++++++++++++++++++++++++++++ src/models/append_graph.rs | 27 ++++++++++++++++++++++++++- src/models/graph.rs | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/models.rs b/src/models.rs index 0834a4a..3c55fbd 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,8 @@ //! Concrete graph topology models. +use crate::maps::ElementMap; +use crate::traits::{GraphTopology, GraphTopologyAddition}; + pub mod append_graph; pub mod frozen_graph; pub mod graph; @@ -11,3 +14,35 @@ pub use frozen_graph::{ FrozenGraph, FrozenGraphEdgeMap, FrozenGraphIncidence, FrozenGraphVertexMap, }; pub use graph::{Graph, GraphEdgeMap, GraphIncidence, GraphVertexMap}; + +/// Builds a new graph isomorphic to `source`. +/// +/// The resulting graph type `G` must implement [`GraphTopologyAddition`] which is used to add +/// vertices and edges. `on_edge` is called for each edge in `source` together with the +/// corresponding new edge of the new graph, which callers can use to construct an edge mapping. +/// +/// Used by the [`GraphTopologyAddition`] models' `From<&H>` and [`FromGraph`] implementations. +/// +/// [`FromGraph`]: crate::traits::FromGraph +pub(crate) fn build_from_topology( + source: &H, + vertex_default: G::Vertex, + mut on_edge: impl FnMut(H::Edge, G::Edge), +) -> (G, ElementMap) { + let mut result = G::default(); + result.reserve_vertices(source.vertex_count()); + result.reserve_edges(source.edge_count()); + + let mut vertex_map = source.vertex_map(vertex_default); + for v in source.vertices() { + vertex_map[v] = result.add_vertex(); + } + + for e in source.edges() { + let (u, v) = source.incident_vertices(e); + let new_edge = result.add_edge(vertex_map[u], vertex_map[v]); + on_edge(e, new_edge); + } + + (result, vertex_map) +} diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index dae6622..58b7c2f 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -3,7 +3,8 @@ use std::num::NonZeroUsize; use crate::maps::ElementMap; -use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; +use crate::models; +use crate::traits::{FromGraph, GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in an [`AppendGraph`]. /// @@ -259,14 +260,38 @@ impl GraphTopologyAddition for AppendGraph { } } +impl From<&H> for AppendGraph { + fn from(source: &H) -> Self { + models::build_from_topology::(source, Vertex(usize::MAX), |_, _| {}).0 + } +} + +impl FromGraph for AppendGraph { + fn from_graph( + source: &H, + ) -> ( + Self, + ElementMap, + ElementMap, + ) { + let mut edge_map = source.edge_map(Edge(NonZeroUsize::MAX)); + let (result, vertex_map) = + models::build_from_topology(source, Vertex(usize::MAX), |e, new_e| edge_map[e] = new_e); + (result, vertex_map, edge_map) + } +} + #[cfg(test)] mod trait_tests { use super::AppendGraph; + use crate::models::Graph; crate::graph_topology_tests!(AppendGraph); crate::graph_topology_addition_tests!(AppendGraph); crate::graph_topology_clone_tests!(AppendGraph); crate::graph_topology_addition_clone_tests!(AppendGraph); + crate::graph_topology_from_graph_tests!(AppendGraph, Graph); + crate::graph_topology_addition_from_graph_tests!(AppendGraph, Graph); } #[cfg(test)] diff --git a/src/models/graph.rs b/src/models/graph.rs index ebfdfe1..eb96813 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -4,8 +4,10 @@ use std::num::NonZeroUsize; use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; +use crate::models; use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, IncidenceCursor, + FromGraph, GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, + IncidenceCursor, }; /// An opaque handle identifying a vertex in a [`Graph`]. @@ -398,9 +400,38 @@ impl GraphTopologyDeletion for Graph { } } +impl From<&H> for Graph { + fn from(source: &H) -> Self { + models::build_from_topology::( + source, + Vertex::new(usize::MAX, usize::MAX), + |_, _| {}, + ) + .0 + } +} + +impl FromGraph for Graph { + fn from_graph( + source: &H, + ) -> ( + Self, + ElementMap, + ElementMap, + ) { + let mut edge_map = source.edge_map(Edge::new(usize::MAX, usize::MAX)); + let (result, vertex_map) = + models::build_from_topology(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| { + edge_map[e] = new_e + }); + (result, vertex_map, edge_map) + } +} + #[cfg(test)] mod trait_tests { use super::Graph; + use crate::models::AppendGraph; crate::graph_topology_tests!(Graph); crate::graph_topology_addition_tests!(Graph); @@ -408,6 +439,8 @@ mod trait_tests { crate::graph_topology_addition_deletion_tests!(Graph); crate::graph_topology_clone_tests!(Graph); crate::graph_topology_addition_clone_tests!(Graph); + crate::graph_topology_from_graph_tests!(Graph, AppendGraph); + crate::graph_topology_addition_from_graph_tests!(Graph, AppendGraph); } #[cfg(test)] -- 2.43.0 From 8bf196d1adbe74fa8e36e1261c25787561e28dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 28 Sep 2026 23:59:30 +0200 Subject: [PATCH 51/67] Fix code format --- src/models/frozen_graph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 9e86039..a7ca9b1 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -255,8 +255,8 @@ impl FromGraph for FrozenGraph { #[cfg(test)] mod trait_tests { - use crate::models::AppendGraph; use super::FrozenGraph; + use crate::models::AppendGraph; crate::graph_topology_tests!(FrozenGraph); crate::graph_topology_clone_tests!(FrozenGraph); -- 2.43.0 From 9d43f0cb8de9861be817024c98cf9ac6113dc8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 29 Sep 2026 00:00:13 +0200 Subject: [PATCH 52/67] Move FromGrath trait within traits module --- src/traits.rs | 67 ++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/traits.rs b/src/traits.rs index 961f40a..45fce15 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -237,39 +237,6 @@ pub trait GraphTopologyDeletion: GraphTopology { fn delete_edge(&mut self, e: Self::Edge); } -/// A trait for constructing an undirected graph topology by converting another graph -/// implementation to this one. -pub trait FromGraph: GraphTopology { - /// Creates a new graph instance that is isomorphic to `source`. - /// - /// Returns the copied graph instance and two mappings from the vertices and edges of `source` - /// to the corresponding vertices and edges of the returned graph. - /// - /// # Examples - /// - /// ``` - /// use grapherity::prelude::*; - /// use grapherity::models::{AppendGraph, FrozenGraph}; - /// - /// let mut source = AppendGraph::new(); - /// let u = source.add_vertex(); - /// let v = source.add_vertex(); - /// let e = source.add_edge(u, v); - /// let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&source); - /// - /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); - /// ``` - fn from_graph( - source: &G, - ) -> ( - Self, - ElementMap, - ElementMap, - ) - where - Self: Sized; -} - /// A cursor for traversing the incidences of a vertex one step at a time. /// /// Because the cursor is [`Copy`], its state can be saved and restored to replay or branch a @@ -320,3 +287,37 @@ pub struct Incidence { /// The edge between *v* and [`vertex`](Self::vertex). pub edge: E, } + +// TODO-0.3.0: Provide and document a way to transform an ElementMap for source to an ElementMap for the copy. +/// A trait for constructing an undirected graph topology by converting another graph +/// implementation to this one. +pub trait FromGraph: GraphTopology { + /// Creates a new graph instance that is isomorphic to `source`. + /// + /// Returns the copied graph instance and two mappings from the vertices and edges of `source` + /// to the corresponding vertices and edges of the returned graph. + /// + /// # Examples + /// + /// ``` + /// use grapherity::prelude::*; + /// use grapherity::models::{AppendGraph, FrozenGraph}; + /// + /// let mut source = AppendGraph::new(); + /// let u = source.add_vertex(); + /// let v = source.add_vertex(); + /// let e = source.add_edge(u, v); + /// let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&source); + /// + /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); + /// ``` + fn from_graph( + source: &G, + ) -> ( + Self, + ElementMap, + ElementMap, + ) + where + Self: Sized; +} -- 2.43.0 From c8855daddfdac9d585a2ff6dde23be73288bea86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 29 Sep 2026 23:16:57 +0200 Subject: [PATCH 53/67] Renamed FromGraph to GraphTopologyConversion to align with existing traits and separate from From trait --- src/lib.rs | 4 +- src/models.rs | 9 ++-- src/models/append_graph.rs | 27 ++++++------ src/models/frozen_graph.rs | 23 +++++----- src/models/graph.rs | 37 ++++++++-------- src/testing.rs | 5 ++- src/testing/fixtures.rs | 18 ++++---- ..._from_graph_testing.rs => from_testing.rs} | 31 ++----------- ...h_topology_addition_conversion_testing.rs} | 16 +++---- .../graph_topology_conversion_testing.rs | 43 +++++++++++++++++++ src/traits.rs | 10 ++--- 11 files changed, 125 insertions(+), 98 deletions(-) rename src/testing/{graph_topology_from_graph_testing.rs => from_testing.rs} (53%) rename src/testing/{graph_topology_addition_from_graph_testing.rs => graph_topology_addition_conversion_testing.rs} (51%) create mode 100644 src/testing/graph_topology_conversion_testing.rs diff --git a/src/lib.rs b/src/lib.rs index 1fadff8..2a0a884 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,8 +145,8 @@ pub mod traits; /// Convenience re-exports of graph topology traits for common use. pub mod prelude { pub use crate::traits::{ - FromGraph, GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, - IncidenceCursor, + GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, + Incidence, IncidenceCursor, }; } diff --git a/src/models.rs b/src/models.rs index 3c55fbd..5274bbc 100644 --- a/src/models.rs +++ b/src/models.rs @@ -17,14 +17,15 @@ pub use graph::{Graph, GraphEdgeMap, GraphIncidence, GraphVertexMap}; /// Builds a new graph isomorphic to `source`. /// -/// The resulting graph type `G` must implement [`GraphTopologyAddition`] which is used to add +/// The resulting graph type `G` implements [`GraphTopologyAddition`] which is used to add /// vertices and edges. `on_edge` is called for each edge in `source` together with the /// corresponding new edge of the new graph, which callers can use to construct an edge mapping. /// -/// Used by the [`GraphTopologyAddition`] models' `From<&H>` and [`FromGraph`] implementations. +/// Used by the [`GraphTopologyAddition`] models' `From<&H>` and [`GraphTopologyConversion`] +/// implementations. /// -/// [`FromGraph`]: crate::traits::FromGraph -pub(crate) fn build_from_topology( +/// [`GraphTopologyConversion`]: crate::traits::GraphTopologyConversion +pub(crate) fn convert_to_addition( source: &H, vertex_default: G::Vertex, mut on_edge: impl FnMut(H::Edge, G::Edge), diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 58b7c2f..1dd03db 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -4,7 +4,9 @@ use std::num::NonZeroUsize; use crate::maps::ElementMap; use crate::models; -use crate::traits::{FromGraph, GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; +use crate::traits::{ + GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence, IncidenceCursor, +}; /// An opaque handle identifying a vertex in an [`AppendGraph`]. /// @@ -260,14 +262,8 @@ impl GraphTopologyAddition for AppendGraph { } } -impl From<&H> for AppendGraph { - fn from(source: &H) -> Self { - models::build_from_topology::(source, Vertex(usize::MAX), |_, _| {}).0 - } -} - -impl FromGraph for AppendGraph { - fn from_graph( +impl GraphTopologyConversion for AppendGraph { + fn convert( source: &H, ) -> ( Self, @@ -276,11 +272,17 @@ impl FromGraph for AppendGraph { ) { let mut edge_map = source.edge_map(Edge(NonZeroUsize::MAX)); let (result, vertex_map) = - models::build_from_topology(source, Vertex(usize::MAX), |e, new_e| edge_map[e] = new_e); + models::convert_to_addition(source, Vertex(usize::MAX), |e, new_e| edge_map[e] = new_e); (result, vertex_map, edge_map) } } +impl From<&H> for AppendGraph { + fn from(source: &H) -> Self { + models::convert_to_addition::(source, Vertex(usize::MAX), |_, _| {}).0 + } +} + #[cfg(test)] mod trait_tests { use super::AppendGraph; @@ -290,8 +292,9 @@ mod trait_tests { crate::graph_topology_addition_tests!(AppendGraph); crate::graph_topology_clone_tests!(AppendGraph); crate::graph_topology_addition_clone_tests!(AppendGraph); - crate::graph_topology_from_graph_tests!(AppendGraph, Graph); - crate::graph_topology_addition_from_graph_tests!(AppendGraph, Graph); + crate::graph_topology_conversion_tests!(AppendGraph, Graph); + crate::graph_topology_addition_conversion_tests!(AppendGraph, Graph); + crate::from_tests!(AppendGraph, Graph); } #[cfg(test)] diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index a7ca9b1..eeaa08e 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -3,7 +3,7 @@ use std::ops::IndexMut; use crate::maps::ElementMap; -use crate::traits::{FromGraph, GraphTopology, Incidence, IncidenceCursor}; +use crate::traits::{GraphTopology, GraphTopologyConversion, Incidence, IncidenceCursor}; /// An opaque handle identifying a vertex in a [`FrozenGraph`]. /// @@ -77,7 +77,7 @@ impl IncidenceCursor for FrozenGraphIncidenceCursor { /// let u = graph.add_vertex(); /// let v = graph.add_vertex(); /// let e = graph.add_edge(u, v); -/// let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&graph); +/// let (graph, vertex_map, edge_map) = FrozenGraph::convert(&graph); /// /// // Queries the FrozenGraph instance via the returned map. /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); @@ -191,14 +191,8 @@ impl GraphTopology for FrozenGraph { } } -impl From<&G> for FrozenGraph { - fn from(graph: &G) -> Self { - Self::from_graph(graph).0 - } -} - -impl FromGraph for FrozenGraph { - fn from_graph( +impl GraphTopologyConversion for FrozenGraph { + fn convert( source: &G, ) -> ( Self, @@ -253,6 +247,12 @@ impl FromGraph for FrozenGraph { } } +impl From<&G> for FrozenGraph { + fn from(graph: &G) -> Self { + Self::convert(graph).0 + } +} + #[cfg(test)] mod trait_tests { use super::FrozenGraph; @@ -260,7 +260,8 @@ mod trait_tests { crate::graph_topology_tests!(FrozenGraph); crate::graph_topology_clone_tests!(FrozenGraph); - crate::graph_topology_from_graph_tests!(FrozenGraph, AppendGraph); + crate::graph_topology_conversion_tests!(FrozenGraph, AppendGraph); + crate::from_tests!(FrozenGraph, AppendGraph); } #[cfg(test)] diff --git a/src/models/graph.rs b/src/models/graph.rs index eb96813..4b85360 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -6,8 +6,8 @@ use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; use crate::models; use crate::traits::{ - FromGraph, GraphTopology, GraphTopologyAddition, GraphTopologyDeletion, Incidence, - IncidenceCursor, + GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, + Incidence, IncidenceCursor, }; /// An opaque handle identifying a vertex in a [`Graph`]. @@ -400,19 +400,8 @@ impl GraphTopologyDeletion for Graph { } } -impl From<&H> for Graph { - fn from(source: &H) -> Self { - models::build_from_topology::( - source, - Vertex::new(usize::MAX, usize::MAX), - |_, _| {}, - ) - .0 - } -} - -impl FromGraph for Graph { - fn from_graph( +impl GraphTopologyConversion for Graph { + fn convert( source: &H, ) -> ( Self, @@ -421,13 +410,24 @@ impl FromGraph for Graph { ) { let mut edge_map = source.edge_map(Edge::new(usize::MAX, usize::MAX)); let (result, vertex_map) = - models::build_from_topology(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| { + models::convert_to_addition(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| { edge_map[e] = new_e }); (result, vertex_map, edge_map) } } +impl From<&H> for Graph { + fn from(source: &H) -> Self { + models::convert_to_addition::( + source, + Vertex::new(usize::MAX, usize::MAX), + |_, _| {}, + ) + .0 + } +} + #[cfg(test)] mod trait_tests { use super::Graph; @@ -439,8 +439,9 @@ mod trait_tests { crate::graph_topology_addition_deletion_tests!(Graph); crate::graph_topology_clone_tests!(Graph); crate::graph_topology_addition_clone_tests!(Graph); - crate::graph_topology_from_graph_tests!(Graph, AppendGraph); - crate::graph_topology_addition_from_graph_tests!(Graph, AppendGraph); + crate::graph_topology_conversion_tests!(Graph, AppendGraph); + crate::graph_topology_addition_conversion_tests!(Graph, AppendGraph); + crate::from_tests!(Graph, AppendGraph); } #[cfg(test)] diff --git a/src/testing.rs b/src/testing.rs index 8a841dd..5d54232 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,11 +1,12 @@ //! Test fixture and test macros for graph topology and algorithm implementations. pub mod fixtures; +pub mod from_testing; pub mod graph_topology_addition_clone_testing; +pub mod graph_topology_addition_conversion_testing; pub mod graph_topology_addition_deletion_testing; -pub mod graph_topology_addition_from_graph_testing; pub mod graph_topology_addition_testing; pub mod graph_topology_clone_testing; +pub mod graph_topology_conversion_testing; pub mod graph_topology_deletion_testing; -pub mod graph_topology_from_graph_testing; pub mod graph_topology_testing; diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 048d088..6f49f82 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use crate::maps::ElementMap; use crate::models::{AppendGraph, FrozenGraph}; -use crate::traits::{FromGraph, GraphTopology, GraphTopologyAddition, Incidence}; +use crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence}; pub trait MakeTestGraph: GraphTopology + Sized { fn standard() -> ( @@ -175,7 +175,7 @@ impl MakeTestGraph for FrozenGraph { [Vec>; 10], ) { let (build, vertices, edges, incidences) = AppendGraph::standard(); - let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); ( graph, vertices.map(|v| vertex_map[v]), @@ -197,31 +197,31 @@ impl MakeTestGraph for FrozenGraph { fn single_vertex() -> (Self, Self::Vertex) { let (build, v) = AppendGraph::single_vertex(); - let (graph, vertex_map, _) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, _) = FrozenGraph::convert(&build); (graph, vertex_map[v]) } fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge) { let (build, vertices, e) = AppendGraph::single_edge(); - let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); (graph, vertices.map(|v| vertex_map[v]), edge_map[e]) } fn disconnected() -> (Self, [Self::Vertex; 3]) { let (build, vertices) = AppendGraph::disconnected(); - let (graph, vertex_map, _) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, _) = FrozenGraph::convert(&build); (graph, vertices.map(|v| vertex_map[v])) } fn loop_edge() -> (Self, Self::Vertex, Self::Edge) { let (build, v, e) = AppendGraph::loop_edge(); - let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); (graph, vertex_map[v], edge_map[e]) } fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]) { let (build, vertices, edges) = AppendGraph::multiple_edges(); - let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); ( graph, vertices.map(|v| vertex_map[v]), @@ -231,7 +231,7 @@ impl MakeTestGraph for FrozenGraph { fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]) { let (build, vertices, edges) = AppendGraph::two_edge_path(); - let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); ( graph, vertices.map(|v| vertex_map[v]), @@ -241,7 +241,7 @@ impl MakeTestGraph for FrozenGraph { fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]) { let (build, vertices, edges) = AppendGraph::two_edge_path_with_loops(); - let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&build); + let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); ( graph, vertices.map(|v| vertex_map[v]), diff --git a/src/testing/graph_topology_from_graph_testing.rs b/src/testing/from_testing.rs similarity index 53% rename from src/testing/graph_topology_from_graph_testing.rs rename to src/testing/from_testing.rs index 82ef3e0..2253fd7 100644 --- a/src/testing/graph_topology_from_graph_testing.rs +++ b/src/testing/from_testing.rs @@ -1,16 +1,12 @@ -use std::fmt::Debug; - -use crate::testing::fixtures; use crate::testing::fixtures::MakeTestGraph; -use crate::traits::{FromGraph, GraphTopology}; +use crate::traits::GraphTopology; #[doc(hidden)] #[macro_export] -macro_rules! graph_topology_from_graph_tests { +macro_rules! from_tests { ($T:ty, $U:ty) => { - $crate::graph_topology_from_graph_tests!(@wrap $T, $U, + $crate::from_tests!(@wrap $T, $U, from, - from_graph, ); }; @@ -18,7 +14,7 @@ macro_rules! graph_topology_from_graph_tests { $( #[test] fn $name() { - $crate::testing::graph_topology_from_graph_testing::$name::<$T, $U>(); + $crate::testing::from_testing::$name::<$T, $U>(); } )* }; @@ -40,22 +36,3 @@ where actual.sort(); assert_eq!(actual, expected, "degree sequence should match"); } - -pub fn from_graph() -where - G::Vertex: Debug, - G::Edge: Debug, - H::Vertex: Debug, - H::Edge: Debug, -{ - let (source, vertices, edges, _) = H::standard(); - let (graph, vertex_map, edge_map) = G::from_graph(&source); - fixtures::assert_isomorphic( - &source, - &graph, - &vertices, - |v| vertex_map[v], - &edges, - |e| edge_map[e], - ); -} diff --git a/src/testing/graph_topology_addition_from_graph_testing.rs b/src/testing/graph_topology_addition_conversion_testing.rs similarity index 51% rename from src/testing/graph_topology_addition_from_graph_testing.rs rename to src/testing/graph_topology_addition_conversion_testing.rs index 9b5a5b4..171573d 100644 --- a/src/testing/graph_topology_addition_from_graph_testing.rs +++ b/src/testing/graph_topology_addition_conversion_testing.rs @@ -1,13 +1,13 @@ use crate::testing::fixtures; use crate::testing::fixtures::MakeTestGraph; -use crate::traits::{FromGraph, GraphTopologyAddition}; +use crate::traits::{GraphTopologyAddition, GraphTopologyConversion}; #[doc(hidden)] #[macro_export] -macro_rules! graph_topology_addition_from_graph_tests { +macro_rules! graph_topology_addition_conversion_tests { ($T:ty, $U:ty) => { - $crate::graph_topology_addition_from_graph_tests!(@wrap $T, $U, - from_graph_mutation_independent, + $crate::graph_topology_addition_conversion_tests!(@wrap $T, $U, + convert_mutation_independent, ); }; @@ -15,18 +15,18 @@ macro_rules! graph_topology_addition_from_graph_tests { $( #[test] fn $name() { - $crate::testing::graph_topology_addition_from_graph_testing::$name::<$T, $U>(); + $crate::testing::graph_topology_addition_conversion_testing::$name::<$T, $U>(); } )* }; } -pub fn from_graph_mutation_independent() +pub fn convert_mutation_independent() where - G: FromGraph + GraphTopologyAddition, + G: GraphTopologyConversion + GraphTopologyAddition, H: MakeTestGraph + GraphTopologyAddition, { let (mut source, vertices, _) = H::single_edge(); - let (mut graph, vertex_map, _) = G::from_graph(&source); + let (mut graph, vertex_map, _) = G::convert(&source); fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| vertex_map[v]); } diff --git a/src/testing/graph_topology_conversion_testing.rs b/src/testing/graph_topology_conversion_testing.rs new file mode 100644 index 0000000..41f8de8 --- /dev/null +++ b/src/testing/graph_topology_conversion_testing.rs @@ -0,0 +1,43 @@ +use std::fmt::Debug; + +use crate::testing::fixtures; +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::GraphTopologyConversion; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_conversion_tests { + ($T:ty, $U:ty) => { + $crate::graph_topology_conversion_tests!(@wrap $T, $U, + convert, + ); + }; + + (@wrap $T:ty, $U:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_conversion_testing::$name::<$T, $U>(); + } + )* + }; +} + +pub fn convert() +where + G::Vertex: Debug, + G::Edge: Debug, + H::Vertex: Debug, + H::Edge: Debug, +{ + let (source, vertices, edges, _) = H::standard(); + let (graph, vertex_map, edge_map) = G::convert(&source); + fixtures::assert_isomorphic( + &source, + &graph, + &vertices, + |v| vertex_map[v], + &edges, + |e| edge_map[e], + ); +} diff --git a/src/traits.rs b/src/traits.rs index 45fce15..d491668 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -291,11 +291,11 @@ pub struct Incidence { // TODO-0.3.0: Provide and document a way to transform an ElementMap for source to an ElementMap for the copy. /// A trait for constructing an undirected graph topology by converting another graph /// implementation to this one. -pub trait FromGraph: GraphTopology { +pub trait GraphTopologyConversion: GraphTopology { /// Creates a new graph instance that is isomorphic to `source`. /// - /// Returns the copied graph instance and two mappings from the vertices and edges of `source` - /// to the corresponding vertices and edges of the returned graph. + /// Returns the converted graph instance and two mappings from the vertices and edges of + /// `source` to the corresponding vertices and edges of the returned graph. /// /// # Examples /// @@ -307,11 +307,11 @@ pub trait FromGraph: GraphTopology { /// let u = source.add_vertex(); /// let v = source.add_vertex(); /// let e = source.add_edge(u, v); - /// let (graph, vertex_map, edge_map) = FrozenGraph::from_graph(&source); + /// let (graph, vertex_map, edge_map) = FrozenGraph::convert(&source); /// /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); /// ``` - fn from_graph( + fn convert( source: &G, ) -> ( Self, -- 2.43.0 From b943dddf1b0b9760a27e8aecb271d1d2ad7fa146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 29 Sep 2026 23:19:08 +0200 Subject: [PATCH 54/67] Add docs for generators and generators::petersen --- src/generators.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/generators.rs b/src/generators.rs index 55f47a1..68fe60b 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1,5 +1,13 @@ +//! Generators for common graphs. + use crate::traits::{GraphTopology, GraphTopologyAddition}; +/// Generates a Petersen graph. +/// +/// Generates a [Petersen graph] in `graph` as a new connected component and returns the new +/// vertices and edges. This graph is also the Kneser graph *KG(5, 2)*. +/// +/// [Petersen graph]: https://en.wikipedia.org/wiki/Petersen_graph pub fn petersen( graph: &mut G, ) -> ( -- 2.43.0 From 4e20c4948683f8b17528267fd53ad2fa12e7642c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 29 Sep 2026 23:22:10 +0200 Subject: [PATCH 55/67] Update generators::petersen to use graph reserve methods --- src/generators.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/generators.rs b/src/generators.rs index 68fe60b..4457fea 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -20,6 +20,8 @@ where const N: usize = 5; const K: usize = 2; + graph.reserve_vertices(10); + graph.reserve_edges(15); let vertices = core::array::from_fn(|_| graph.add_vertex()); let edges = core::array::from_fn(|j| { let i = j / 3; -- 2.43.0 From 3d7dda6f7b194cf621f95efce35612f20aa14c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sun, 11 Oct 2026 22:24:00 +0200 Subject: [PATCH 56/67] Refactor ElementMap, removing its copy-on-write feature --- README.md | 6 +- src/lib.rs | 7 +- src/maps.rs | 189 +++++++++++------- src/models/append_graph.rs | 2 - src/models/frozen_graph.rs | 3 +- src/models/graph.rs | 2 - .../graph_topology_addition_testing.rs | 55 ++++- src/testing/graph_topology_testing.rs | 44 ++-- src/traits.rs | 86 +++++++- 9 files changed, 281 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index b471e97..a47f8b8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This crate provides data structures to model graphs and to perform algorithms on Currently supported are: * Undirected graph types `Graph`, `AppendGraph`, and `FrozenGraph`, built on a flat, index-based adjacency list, -* Maps to freely associate custom data with vertices and edges, +* `ElementMap` to freely associate custom data with vertices and edges, * Connectivity and pathing algorithms: Dijkstra's algorithm, DFS, and BFS in different variants, * Exposed traits to implement custom graph data structures or algorithms. @@ -22,14 +22,14 @@ use grapherity::models::Graph; use grapherity::algorithms; let mut graph = Graph::new(); -// Creates a map for edge weights with a default weight. -let mut weights = graph.edge_map(1); // Adds three vertices and two edges. let v1 = graph.add_vertex(); let v2 = graph.add_vertex(); let v3 = graph.add_vertex(); graph.add_edge(v1, v2); let e = graph.add_edge(v2, v3); +// Creates a map for edge weights with a default weight, after creating the edge. +let mut weights = graph.edge_map(1); // Sets a non-default weight for the second edge. weights[e] = 5; diff --git a/src/lib.rs b/src/lib.rs index 2a0a884..8a0b27d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,14 +23,14 @@ //! use grapherity::algorithms; //! //! let mut graph = Graph::new(); -//! // Creates a map for edge weights with a default weight. -//! let mut weights = graph.edge_map(1); //! // Adds three vertices and two edges. //! let v1 = graph.add_vertex(); //! let v2 = graph.add_vertex(); //! let v3 = graph.add_vertex(); //! graph.add_edge(v1, v2); //! let e = graph.add_edge(v2, v3); +//! // Creates a map for edge weights with a default weight, after creating the edges. +//! let mut weights = graph.edge_map(1); //! // Sets a non-default weight for the second edge. //! weights[e] = 5; //! @@ -97,7 +97,8 @@ //! graph.degree(v); //! ``` //! -//! [`ElementMap`] is not required to panic if provided with an invalid vertex or edge. +//! [`ElementMap`] does not check validity of vertices or edges and might or might not panic in case +//! an invalid handle is provided. //! //! Vertices and edges are provided by the graph and not intended to be generated by users. //! diff --git a/src/maps.rs b/src/maps.rs index debddc8..82a693c 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -1,7 +1,7 @@ //! Provides maps to associate custom data to graph vertices and edges. //! -//! [`ElementMap`] provides a copy-on-write, [`Vec`]-backed map to associate data to either all -//! vertices or all edges in a graph. +//! [`ElementMap`] is a dense, [`Vec`]-backed map to associate data to either all vertices or all +//! edges in a graph. use std::ops::{Index, IndexMut}; @@ -12,15 +12,19 @@ use std::ops::{Index, IndexMut}; /// which means that the provided index conversion function should map elements to contiguous /// indices, or indices with relatively few gaps. /// -/// Data allocation happens as copy-on-write, i.e. the backing [`Vec`] is only resized if a value is -/// written beyond current capacity, but the `default` value can transparently be read. No bound or -/// validity checks are performed by the map on the provided element handles. -/// /// Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] to obtain an `ElementMap` for /// vertices or edges, respectively. /// +/// The map does not grow on its own when the associated graph grows. Use the graph's +/// [`GraphTopologyAddition::sync_vertex_map`] or [`GraphTopologyAddition::sync_edge_map`] to extend +/// the map explicitly after adding vertices or edges to it. +/// +/// No bound or validity checks are performed by the map on the provided element handles. +/// /// [`GraphTopology::edge_map`]: crate::traits::GraphTopology::edge_map /// [`GraphTopology::vertex_map`]: crate::traits::GraphTopology::vertex_map +/// [`GraphTopologyAddition::sync_vertex_map`]: crate::traits::GraphTopologyAddition::sync_vertex_map +/// [`GraphTopologyAddition::sync_edge_map`]: crate::traits::GraphTopologyAddition::sync_edge_map pub struct ElementMap { data: Vec, default: T, @@ -29,47 +33,59 @@ pub struct ElementMap { impl ElementMap { /// Creates a new map with the given `default` value, an index conversion function `to_index`, - /// and an initial `capacity`. - pub fn new(default: T, to_index: fn(E) -> usize, capacity: usize) -> Self { + /// and an initial `len`, i.e. the number of slots that can be read from and written to. + /// + /// Since `to_index` is usually an implementation detail of the graph model, this is not meant + /// to be called directly. Use [`GraphTopology::vertex_map`] or [`GraphTopology::edge_map`] + /// instead. + /// + /// [`GraphTopology::edge_map`]: crate::traits::GraphTopology::edge_map + /// [`GraphTopology::vertex_map`]: crate::traits::GraphTopology::vertex_map + pub fn new(default: T, to_index: fn(E) -> usize, len: usize) -> Self { Self { - data: vec![default.clone(); capacity], + data: vec![default.clone(); len], default, to_index, } } - /// Returns the total number of data entries the map can write without reallocating. Reads - /// beyond `capacity` are valid and return the default value. - pub fn capacity(&self) -> usize { - self.data.capacity() - } - - /// Reserves capacity for at least `additional` more data entries. Does nothing if capacity is - /// already sufficient. + /// Grows the map so that it contains at least `new_len` slots for elements, initialising any + /// newly added slots to the `default` value provided to [`new`], without modifying existing + /// data. Does nothing if the map already has at least `new_len` slots. /// - /// Use this before writing data for new graph elements to avoid incremental growth on the first - /// write to each new element. - pub fn reserve(&mut self, additional: usize) { - self.data.reserve(additional); + /// Since the number of elements the map can actually store depend on the index conversion + /// function, which is usually an implementation detail of the graph model, this is not meant to + /// be called directly. Use [`GraphTopologyAddition::sync_vertex_map`] or + /// [`GraphTopologyAddition::sync_edge_map`] instead. + /// + /// [`new`]: ElementMap::new + /// [`GraphTopologyAddition::sync_vertex_map`]: crate::traits::GraphTopologyAddition::sync_vertex_map + /// [`GraphTopologyAddition::sync_edge_map`]: crate::traits::GraphTopologyAddition::sync_edge_map + pub fn grow(&mut self, new_len: usize) { + if new_len > self.data.len() { + self.data.resize(new_len, self.default.clone()); + } } /// Returns a new map over the same elements with values transformed by `f`. /// - /// The default value of the new map is `f` applied to the original default, so reads beyond the - /// current capacity of the new map behave consistently with the transformation. + /// The default value of the new map is `f` applied to the original default, so slots later added + /// to the new map by [`GraphTopologyAddition::sync_vertex_map`] or + /// [`GraphTopologyAddition::sync_edge_map`] are filled consistently with the transformation. + /// + /// [`GraphTopologyAddition::sync_vertex_map`]: crate::traits::GraphTopologyAddition::sync_vertex_map + /// [`GraphTopologyAddition::sync_edge_map`]: crate::traits::GraphTopologyAddition::sync_edge_map /// /// # Examples /// /// ``` /// # use grapherity::maps::ElementMap; - /// let mut map: ElementMap> = ElementMap::new(None, |e| e, 0); + /// let mut map: ElementMap> = ElementMap::new(None, |e| e, 2); /// map[1] = Some(42); /// /// let bools = map.map(|v| v.is_some()); /// assert_eq!(bools[0], false); /// assert_eq!(bools[1], true); - /// // Returns mapped default for reads beyond capacity. - /// assert_eq!(bools[9], false); /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> ElementMap { ElementMap { @@ -84,22 +100,13 @@ impl Index for ElementMap { type Output = T; fn index(&self, e: E) -> &T { - let i = (self.to_index)(e); - if i < self.data.len() { - &self.data[i] - } else { - &self.default - } + &self.data[(self.to_index)(e)] } } impl IndexMut for ElementMap { fn index_mut(&mut self, e: E) -> &mut T { - let i = (self.to_index)(e); - if i >= self.data.len() { - self.data.resize(i + 1, self.default.clone()); - } - &mut self.data[i] + &mut self.data[(self.to_index)(e)] } } @@ -109,67 +116,97 @@ mod tests { #[test] fn initial_values_are_default() { - let map = ElementMap::new(42, |e| e, 0); + let map = ElementMap::new(42, |e| e, 6); assert_eq!(map[0], 42, "initial read at 0 should be default"); assert_eq!(map[5], 42, "initial read at 5 should be default"); } #[test] - fn reserve_increases_capacity_without_initializing() { - let mut map = ElementMap::new(0, |e| e, 0); - let capacity_before = map.capacity(); - map.reserve(capacity_before + 10); - assert!( - map.capacity() > capacity_before, - "expected sufficient capacity increase after reserve" - ); - assert_eq!(map[5], 0, "expected read beyond length to return default"); + #[should_panic] + fn read_out_of_bounds() { + let map = ElementMap::new(42, |e| e, 3); + let _ = map[3]; } #[test] - fn reserve_prevents_reallocation_on_write() { - let mut map = ElementMap::new(0, |e| e, 0); - map.reserve(10); - let capacity_before = map.capacity(); - for i in 0..10 { - map[i] = i as i32; - } - assert_eq!( - map.capacity(), - capacity_before, - "writes within reserved capacity should not reallocate" - ); - } - - #[test] - fn reserve_does_not_overwrite_existing_values() { - let mut map = ElementMap::new(0, |e| e, 0); - map[0] = 5; - let capacity_before = map.capacity(); - map.reserve(capacity_before + 10); - assert_eq!(map[0], 5, "reserve should not change existing data"); + #[should_panic] + fn write_out_of_bounds() { + let mut map = ElementMap::new(0, |e| e, 3); + map[3] = 1; } #[test] fn write_and_read() { - let mut map = ElementMap::new(0, |e| e, 0); + let mut map = ElementMap::new(0, |e| e, 2); map[1] = 7; assert_eq!(map[1], 7, "unexpected read value after write"); assert_eq!(map[0], 0, "initial read should be default"); } + #[test] + fn grow() { + let mut map = ElementMap::new(0, |e| e, 2); + assert_eq!( + map.data.len(), + 2, + "precondition: unexpected data length before grow" + ); + map.grow(10); + assert_eq!(map.data.len(), 10, "unexpected data length after grow"); + } + + #[test] + fn grow_extends_with_default() { + let mut map = ElementMap::new(17, |e| e, 3); + map.grow(6); + assert_eq!(map[5], 17, "read of newly grown slot should be default"); + map[5] = 9; + assert_eq!( + map[5], 9, + "unexpected value after write in newly grown slot" + ); + } + + #[test] + fn grow_preserves_existing_values() { + let mut map = ElementMap::new(22, |e| e, 3); + map[0] = 5; + map.grow(10); + assert_eq!(map[0], 5, "unexpected data after grow"); + } + + #[test] + fn grow_to_smaller_len() { + let mut map = ElementMap::new(23, |e| e, 6); + map[5] = 9; + map.grow(3); + assert_eq!( + map[5], 9, + "grow to a smaller length should not discard data" + ); + assert_eq!(map.data.len(), 6, "unexpected data length"); + } + #[test] fn map() { - let mut source: ElementMap> = ElementMap::new(None, |e| e, 0); + let mut source: ElementMap> = ElementMap::new(None, |e| e, 3); source[1] = Some(42); source[2] = None; let mapped = source.map(|v| v.is_some()); - assert_eq!(mapped[0], false, "unset entry should map to false"); - assert_eq!(mapped[1], true, "Some entry should map to true"); - assert_eq!(mapped[2], false, "explicit None entry should map to false"); - assert_eq!( - mapped[9], false, - "read beyond capacity should return mapped default" + assert!(!mapped[0], "unset entry should map to false"); + assert!(mapped[1], "Some entry should map to true"); + assert!(!mapped[2], "explicit None entry should map to false"); + } + + #[test] + fn map_grow() { + let mut source: ElementMap> = ElementMap::new(None, |e| e, 2); + source[1] = Some(42); + let mut mapped = source.map(|v| v.is_some()); + mapped.grow(4); + assert!( + !mapped[3], + "slot added by grow should hold the mapped default" ); } } diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 1dd03db..96d36d1 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -170,7 +170,6 @@ impl GraphTopology for AppendGraph { self.vertices.len() } - // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn vertex_map(&self, default: T) -> ElementMap { ElementMap::new(default, |v| v.0, self.vertex_capacity()) } @@ -179,7 +178,6 @@ impl GraphTopology for AppendGraph { self.incidences.len() / 2 } - // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn edge_map(&self, default: T) -> ElementMap { ElementMap::new(default, |e| e.index() / 2, self.edge_capacity()) } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index eeaa08e..4db2439 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -144,7 +144,8 @@ impl GraphTopology for FrozenGraph { // TODO: Find a way to reduce the needed capacity for the edge ElementMap. This could be improved by finding a small vertex cover and store those vertices first. See https://en.wikipedia.org/wiki/Vertex_cover#Approximate_evaluation fn edge_map(&self, default: T) -> ElementMap { - ElementMap::new(default, |e| e.0, self.incidences.len() - 1) + // "incidences.len() - 1" would underflow for an edge-free graph. + ElementMap::new(default, |e| e.0, self.incidences.len().saturating_sub(1)) } fn degree(&self, v: Self::Vertex) -> usize { diff --git a/src/models/graph.rs b/src/models/graph.rs index 4b85360..a95e1c9 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -262,7 +262,6 @@ impl GraphTopology for Graph { self.vertices.len() } - // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn vertex_map(&self, default: T) -> ElementMap { ElementMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) } @@ -271,7 +270,6 @@ impl GraphTopology for Graph { self.incidences.len() / 2 } - // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn edge_map(&self, default: T) -> ElementMap { ElementMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) } diff --git a/src/testing/graph_topology_addition_testing.rs b/src/testing/graph_topology_addition_testing.rs index 77f68b1..935d715 100644 --- a/src/testing/graph_topology_addition_testing.rs +++ b/src/testing/graph_topology_addition_testing.rs @@ -15,8 +15,10 @@ macro_rules! graph_topology_addition_tests { reserve_edges_does_not_affect_vertex_capacity, add_vertex, add_edge, - vertex_map_new_vertex, - edge_map_new_edge, + sync_vertex_map_after_add, + sync_edge_map_after_add, + sync_vertex_map_preserves_values, + sync_edge_map_preserves_values, ); }; @@ -122,31 +124,68 @@ where assert_ne!(graph.add_edge(u, v), e, "unexpected duplicate edge"); } -pub fn vertex_map_new_vertex() { +pub fn sync_vertex_map_after_add() { let mut graph = G::default(); let mut map = graph.vertex_map(27); + let u = graph.add_vertex(); let v = graph.add_vertex(); + graph.sync_vertex_map(&mut map); assert_eq!( - map[v], 27, + map[u], 27, "unexpected value from default map read for new vertex" ); - map[v] = 9; + map[u] = 9; assert_eq!( - map[v], 9, + map[u], 9, "unexpected value from map read after write for new vertex" ); + assert_eq!( + map[v], 27, + "unexpected value from default map read for second new vertex" + ); } -pub fn edge_map_new_edge() { +pub fn sync_edge_map_after_add() { let mut graph = G::default(); let u = graph.add_vertex(); let v = graph.add_vertex(); let mut map = graph.edge_map(28); let e = graph.add_edge(u, v); - assert_eq!(map[e], 28); + let f = graph.add_edge(u, v); + graph.sync_edge_map(&mut map); + assert_eq!( + map[e], 28, + "unexpected value from default map read for new edge" + ); map[e] = 8; assert_eq!( map[e], 8, "unexpected value from map read after write for new edge" ); + assert_eq!( + map[f], 28, + "unexpected value from default map read for second new edge" + ); +} + +pub fn sync_vertex_map_preserves_values() { + let mut graph = G::default(); + let v = graph.add_vertex(); + let mut map = graph.vertex_map(0); + map[v] = 5; + graph.reserve_vertices(20); + graph.sync_vertex_map(&mut map); + assert_eq!(map[v], 5, "sync should not change existing map values"); +} + +pub fn sync_edge_map_preserves_values() { + let mut graph = G::default(); + let u = graph.add_vertex(); + let v = graph.add_vertex(); + let e = graph.add_edge(u, v); + let mut map = graph.edge_map(0); + map[e] = 6; + graph.reserve_edges(20); + graph.sync_edge_map(&mut map); + assert_eq!(map[e], 6, "sync should not change existing map values"); } diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 415fc52..d595c2d 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -72,12 +72,22 @@ pub fn vertex_count() { assert_eq!(graph.vertex_count(), 10, "unexpected vertex count"); } -pub fn vertex_map() { - let (graph, v) = G::single_vertex(); - let mut map = graph.vertex_map(27); - assert_eq!(map[v], 27, "unexpected value from default map read"); - map[v] = 9; - assert_eq!(map[v], 9, "unexpected value from map after write"); +pub fn vertex_map() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + let mut map = graph.vertex_map(88); + for (i, &v) in vertices.iter().enumerate() { + assert_eq!( + map[v], 88, + "unexpected value from default map read for vertex {v:?}" + ); + map[v] = i; + } + for (i, &v) in vertices.iter().enumerate() { + assert_eq!(map[v], i, "unexpected value after write for vertex {v:?}"); + } } pub fn edge_count_empty() { @@ -90,12 +100,22 @@ pub fn edge_count() { assert_eq!(graph.edge_count(), 18, "unexpected edge count"); } -pub fn edge_map() { - let (graph, _, e) = G::single_edge(); - let mut map = graph.edge_map(28); - assert_eq!(map[e], 28, "unexpected value from default map read"); - map[e] = 8; - assert_eq!(map[e], 8, "unexpected value from map after write"); +pub fn edge_map() +where + G::Edge: Debug, +{ + let (graph, _, edges, _) = G::standard(); + let mut map = graph.edge_map(99); + for (i, &(e, _, _)) in edges.iter().enumerate() { + assert_eq!( + map[e], 99, + "unexpected value from default map read for edge {e:?}" + ); + map[e] = i; + } + for (i, &(e, _, _)) in edges.iter().enumerate() { + assert_eq!(map[e], i, "unexpected value after write for edge {e:?}"); + } } pub fn degree_zero() { diff --git a/src/traits.rs b/src/traits.rs index d491668..06ca661 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -47,6 +47,10 @@ pub trait GraphTopology { /// Creates and returns an [`ElementMap`] for the vertices with every slot initialised to /// `default`. /// + /// The returned map only covers the vertices that exist in the graph at the time of the call. + /// Use [`GraphTopologyAddition::sync_vertex_map`] to extend the map before accessing new + /// vertices. + /// /// # Examples /// /// ``` @@ -54,13 +58,11 @@ pub trait GraphTopology { /// # use grapherity::models::Graph; /// let mut graph = Graph::new(); /// let v1 = graph.add_vertex(); + /// let v2 = graph.add_vertex(); /// let mut labels = graph.vertex_map("z"); /// assert_eq!(labels[v1], "z"); /// labels[v1] = "a"; /// assert_eq!(labels[v1], "a"); - /// - /// // A new vertex is immediately available for read and write. - /// let v2 = graph.add_vertex(); /// assert_eq!(labels[v2], "z"); /// labels[v2] = "b"; /// assert_eq!(labels[v2], "b"); @@ -72,6 +74,9 @@ pub trait GraphTopology { /// Creates and returns an [`ElementMap`] for the edges with every slot initialised to `default`. /// + /// The returned map only covers the edges that exist in the graph at the time of the call. Use + /// [`GraphTopologyAddition::sync_edge_map`] to extend the map before accessing new edges. + /// /// # Examples /// /// ``` @@ -81,13 +86,11 @@ pub trait GraphTopology { /// let v1 = graph.add_vertex(); /// let v2 = graph.add_vertex(); /// let e1 = graph.add_edge(v1, v2); + /// let e2 = graph.add_edge(v1, v2); /// let mut weights = graph.edge_map(5); /// assert_eq!(weights[e1], 5); /// weights[e1] = 1; /// assert_eq!(weights[e1], 1); - /// - /// // A new edge is immediately available for read and write. - /// let e2 = graph.add_edge(v1, v2); /// assert_eq!(weights[e2], 5); /// weights[e2] = 2; /// assert_eq!(weights[e2], 2); @@ -180,9 +183,21 @@ pub trait GraphTopology { /// undirected graph. pub trait GraphTopologyAddition: GraphTopology + Default { /// Returns the total number of vertices the graph can hold without reallocating. + /// + /// For every valid [`Vertex`] handle, the index produced by the conversion function used in + /// [`GraphTopology::vertex_map`] must be strictly less than this capacity. Implementors of + /// [`GraphTopology::vertex_map`] must size the returned vertex [`ElementMap`] to this capacity. + /// + /// [`Vertex`]: GraphTopology::Vertex fn vertex_capacity(&self) -> usize; /// Returns the total number of edges the graph can hold without reallocating. + /// + /// For every valid [`Edge`] handle, the index produced by the conversion function used in + /// [`GraphTopology::edge_map`] must be strictly less than this capacity. Implementors of + /// [`GraphTopology::edge_map`] must size the returned edge [`ElementMap`] to this capacity. + /// + /// [`Edge`]: GraphTopology::Edge fn edge_capacity(&self) -> usize; /// Reserves capacity for at least `additional` more vertices. Does nothing if capacity is @@ -212,6 +227,65 @@ pub trait GraphTopologyAddition: GraphTopology + Default { /// /// Panics if `u` or `v` is not a valid vertex of this graph. fn add_edge(&mut self, u: Self::Vertex, v: Self::Vertex) -> Self::Edge; + + /// Grows `map` so that it covers a number of vertices up to at least [`vertex_capacity`]. + /// + /// The data for any newly covered vertices is initialised to the map's original default value. + /// Does nothing if `map` already covers enough vertices. + /// + /// An [`ElementMap`] does not grow on its own, so call this after [`reserve_vertices`] (or + /// after vertices have already been added) and before accessing `map` for any newly added + /// vertices. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::Graph; + /// let mut graph = Graph::new(); + /// let mut labels = graph.vertex_map("z"); + /// graph.reserve_vertices(1); + /// graph.sync_vertex_map(&mut labels); + /// let v = graph.add_vertex(); + /// labels[v] = "a"; + /// assert_eq!(labels[v], "a"); + /// ``` + /// + /// [`reserve_vertices`]: Self::reserve_vertices + /// [`vertex_capacity`]: Self::vertex_capacity + fn sync_vertex_map(&self, map: &mut ElementMap) { + map.grow(self.vertex_capacity()); + } + + /// Grows `map` so that it covers a number of edges up to at least [`edge_capacity`]. + /// + /// The data for any newly covered edges is initialised to the map's original default value. + /// Does nothing if `map` already covers enough edges. + /// + /// An [`ElementMap`] does not grow on its own, so call this after [`reserve_edges`] (or after + /// edges have already been added) and before accessing `map` for any newly added edges. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::Graph; + /// let mut graph = Graph::new(); + /// let mut weights = graph.edge_map(1); + /// let v1 = graph.add_vertex(); + /// let v2 = graph.add_vertex(); + /// graph.reserve_edges(1); + /// graph.sync_edge_map(&mut weights); + /// let e = graph.add_edge(v1, v2); + /// weights[e] = 5; + /// assert_eq!(weights[e], 5); + /// ``` + /// + /// [`reserve_edges`]: Self::reserve_edges + /// [`edge_capacity`]: Self::edge_capacity + fn sync_edge_map(&self, map: &mut ElementMap) { + map.grow(self.edge_capacity()); + } } /// A trait that adds deletion operations to an undirected graph topology. -- 2.43.0 From 1979d336493b2a1e7083ed89ebbf8c311860bdba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 12 Oct 2026 20:23:27 +0200 Subject: [PATCH 57/67] Add reserve calls to the test fixtures --- src/testing/fixtures.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 6f49f82..b5dc20c 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -37,6 +37,8 @@ impl MakeTestGraph for G { [Vec>; 10], ) { let mut graph = Self::default(); + graph.reserve_vertices(10); + graph.reserve_edges(18); let vertices = core::array::from_fn(|_| graph.add_vertex()); let edges = [ (vertices[0], vertices[1]), @@ -121,6 +123,7 @@ impl MakeTestGraph for G { fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge) { let mut graph = Self::default(); + graph.reserve_vertices(2); let vertices = core::array::from_fn(|_| graph.add_vertex()); let e = graph.add_edge(vertices[0], vertices[1]); (graph, vertices, e) @@ -128,6 +131,7 @@ impl MakeTestGraph for G { fn disconnected() -> (Self, [Self::Vertex; 3]) { let mut graph = Self::default(); + graph.reserve_vertices(3); let vertices = core::array::from_fn(|_| graph.add_vertex()); graph.add_edge(vertices[1], vertices[2]); (graph, vertices) @@ -142,6 +146,8 @@ impl MakeTestGraph for G { fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]) { let mut graph = Self::default(); + graph.reserve_vertices(2); + graph.reserve_edges(K); let vertices = core::array::from_fn(|_| graph.add_vertex()); let edges = core::array::from_fn(|_| graph.add_edge(vertices[0], vertices[1])); (graph, vertices, edges) @@ -149,6 +155,8 @@ impl MakeTestGraph for G { fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]) { let mut graph = Self::default(); + graph.reserve_vertices(3); + graph.reserve_edges(2); let vertices = core::array::from_fn(|_| graph.add_vertex()); let edges = core::array::from_fn(|i| graph.add_edge(vertices[i], vertices[i + 1])); (graph, vertices, edges) @@ -156,6 +164,8 @@ impl MakeTestGraph for G { fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]) { let mut graph = Self::default(); + graph.reserve_vertices(3); + graph.reserve_edges(4); let vertices = core::array::from_fn(|_| graph.add_vertex()); // Path edges. let e0 = graph.add_edge(vertices[0], vertices[1]); -- 2.43.0 From b124e58b380fc57fa252d6cf8d1c9a3d5d234f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 12 Oct 2026 21:33:49 +0200 Subject: [PATCH 58/67] Change GraphTopologyConversion::convert return value to include the new GraphIsomorphism This allows to convert maps for the converted graph. --- src/lib.rs | 4 +- src/maps.rs | 29 ++++ src/models/append_graph.rs | 14 +- src/models/frozen_graph.rs | 30 ++-- src/models/graph.rs | 15 +- src/testing/fixtures.rs | 44 ++--- ...ph_topology_addition_conversion_testing.rs | 4 +- .../graph_topology_conversion_testing.rs | 6 +- src/traits.rs | 161 ++++++++++++++++-- 9 files changed, 228 insertions(+), 79 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8a0b27d..843225d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,8 +146,8 @@ pub mod traits; /// Convenience re-exports of graph topology traits for common use. pub mod prelude { pub use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, - Incidence, IncidenceCursor, + GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, + GraphTopologyDeletion, Incidence, IncidenceCursor, }; } diff --git a/src/maps.rs b/src/maps.rs index 82a693c..bb45b09 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -94,6 +94,35 @@ impl ElementMap { to_index: self.to_index, } } + + /// Creates a new `ElementMap` indexed by elements of type `F`, such that for each element *e* + /// of this map that `mapping` maps to some *f*, the new map contains at *f* a clone of the + /// value this map contains at *e*. + /// + /// A slot of `mapping` counts as unmapped if it holds the default value of `mapping`, which + /// must therefore never be a valid element of `F`. Unmapped slots are skipped. Slots of the new + /// map that no element is mapped to, and slots of this map beyond the length of `mapping` or + /// vice versa, are left at this map's default value. The `template` is only used to determine + /// the length and the index conversion function of the new map. + /// + /// Iterates the raw slots of both maps, so it neither needs the graph nor the element handles. + pub(crate) fn remap( + &self, + mapping: &ElementMap, + template: &ElementMap, + ) -> ElementMap { + let mut result = ElementMap { + data: vec![self.default.clone(); template.data.len()], + default: self.default.clone(), + to_index: template.to_index, + }; + for (value, &f) in self.data.iter().zip(&mapping.data) { + if f != mapping.default { + result.data[(template.to_index)(f)] = value.clone(); + } + } + result + } } impl Index for ElementMap { diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 96d36d1..27c8742 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -5,7 +5,8 @@ use std::num::NonZeroUsize; use crate::maps::ElementMap; use crate::models; use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence, IncidenceCursor, + GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence, + IncidenceCursor, }; /// An opaque handle identifying a vertex in an [`AppendGraph`]. @@ -261,17 +262,12 @@ impl GraphTopologyAddition for AppendGraph { } impl GraphTopologyConversion for AppendGraph { - fn convert( - source: &H, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { + fn convert(source: &G) -> (Self, GraphIsomorphism) { let mut edge_map = source.edge_map(Edge(NonZeroUsize::MAX)); let (result, vertex_map) = models::convert_to_addition(source, Vertex(usize::MAX), |e, new_e| edge_map[e] = new_e); - (result, vertex_map, edge_map) + let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); + (result, iso) } } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 4db2439..9167f69 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -3,7 +3,9 @@ use std::ops::IndexMut; use crate::maps::ElementMap; -use crate::traits::{GraphTopology, GraphTopologyConversion, Incidence, IncidenceCursor}; +use crate::traits::{ + GraphIsomorphism, GraphTopology, GraphTopologyConversion, Incidence, IncidenceCursor, +}; /// An opaque handle identifying a vertex in a [`FrozenGraph`]. /// @@ -77,10 +79,10 @@ impl IncidenceCursor for FrozenGraphIncidenceCursor { /// let u = graph.add_vertex(); /// let v = graph.add_vertex(); /// let e = graph.add_edge(u, v); -/// let (graph, vertex_map, edge_map) = FrozenGraph::convert(&graph); +/// let (graph, isomorphism) = FrozenGraph::convert(&graph); /// /// // Queries the FrozenGraph instance via the returned map. -/// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); +/// assert!(graph.are_adjacent(isomorphism.vertex(u), isomorphism.vertex(v))); /// ``` /// /// # Time and space complexity @@ -193,13 +195,7 @@ impl GraphTopology for FrozenGraph { } impl GraphTopologyConversion for FrozenGraph { - fn convert( - source: &G, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { + fn convert(source: &G) -> (Self, GraphIsomorphism) { // Builds vertex list with sentinel and vertex map. let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); let mut vertices = Vec::with_capacity(source.vertex_count() + 1); @@ -237,14 +233,12 @@ impl GraphTopologyConversion for FrozenGraph { } } - ( - Self { - vertices, - incidences, - }, - vertex_map, - edge_map, - ) + let result = Self { + vertices, + incidences, + }; + let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); + (result, iso) } } diff --git a/src/models/graph.rs b/src/models/graph.rs index a95e1c9..6677da0 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -6,8 +6,8 @@ use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; use crate::models; use crate::traits::{ - GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, - Incidence, IncidenceCursor, + GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, + GraphTopologyDeletion, Incidence, IncidenceCursor, }; /// An opaque handle identifying a vertex in a [`Graph`]. @@ -399,19 +399,14 @@ impl GraphTopologyDeletion for Graph { } impl GraphTopologyConversion for Graph { - fn convert( - source: &H, - ) -> ( - Self, - ElementMap, - ElementMap, - ) { + fn convert(source: &G) -> (Self, GraphIsomorphism) { let mut edge_map = source.edge_map(Edge::new(usize::MAX, usize::MAX)); let (result, vertex_map) = models::convert_to_addition(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| { edge_map[e] = new_e }); - (result, vertex_map, edge_map) + let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); + (result, iso) } } diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index b5dc20c..5689c28 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -185,16 +185,16 @@ impl MakeTestGraph for FrozenGraph { [Vec>; 10], ) { let (build, vertices, edges, incidences) = AppendGraph::standard(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|(e, u, v)| (edge_map[e], vertex_map[u], vertex_map[v])), + vertices.map(|v| iso.vertex(v)), + edges.map(|(e, u, v)| (iso.edge(e), iso.vertex(u), iso.vertex(v))), incidences.map(|vec| { vec.iter() .map(|i| Incidence { - vertex: vertex_map[i.vertex], - edge: edge_map[i.edge], + vertex: iso.vertex(i.vertex), + edge: iso.edge(i.edge), }) .collect() }), @@ -207,55 +207,55 @@ impl MakeTestGraph for FrozenGraph { fn single_vertex() -> (Self, Self::Vertex) { let (build, v) = AppendGraph::single_vertex(); - let (graph, vertex_map, _) = FrozenGraph::convert(&build); - (graph, vertex_map[v]) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, iso.vertex(v)) } fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge) { let (build, vertices, e) = AppendGraph::single_edge(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); - (graph, vertices.map(|v| vertex_map[v]), edge_map[e]) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, vertices.map(|v| iso.vertex(v)), iso.edge(e)) } fn disconnected() -> (Self, [Self::Vertex; 3]) { let (build, vertices) = AppendGraph::disconnected(); - let (graph, vertex_map, _) = FrozenGraph::convert(&build); - (graph, vertices.map(|v| vertex_map[v])) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, vertices.map(|v| iso.vertex(v))) } fn loop_edge() -> (Self, Self::Vertex, Self::Edge) { let (build, v, e) = AppendGraph::loop_edge(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); - (graph, vertex_map[v], edge_map[e]) + let (graph, iso) = FrozenGraph::convert(&build); + (graph, iso.vertex(v), iso.edge(e)) } fn multiple_edges() -> (Self, [Self::Vertex; 2], [Self::Edge; K]) { let (build, vertices, edges) = AppendGraph::multiple_edges(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|e| edge_map[e]), + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), ) } fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]) { let (build, vertices, edges) = AppendGraph::two_edge_path(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|e| edge_map[e]), + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), ) } fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]) { let (build, vertices, edges) = AppendGraph::two_edge_path_with_loops(); - let (graph, vertex_map, edge_map) = FrozenGraph::convert(&build); + let (graph, iso) = FrozenGraph::convert(&build); ( graph, - vertices.map(|v| vertex_map[v]), - edges.map(|e| edge_map[e]), + vertices.map(|v| iso.vertex(v)), + edges.map(|e| iso.edge(e)), ) } } diff --git a/src/testing/graph_topology_addition_conversion_testing.rs b/src/testing/graph_topology_addition_conversion_testing.rs index 171573d..f3a423b 100644 --- a/src/testing/graph_topology_addition_conversion_testing.rs +++ b/src/testing/graph_topology_addition_conversion_testing.rs @@ -27,6 +27,6 @@ where H: MakeTestGraph + GraphTopologyAddition, { let (mut source, vertices, _) = H::single_edge(); - let (mut graph, vertex_map, _) = G::convert(&source); - fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| vertex_map[v]); + let (mut graph, iso) = G::convert(&source); + fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| iso.vertex(v)); } diff --git a/src/testing/graph_topology_conversion_testing.rs b/src/testing/graph_topology_conversion_testing.rs index 41f8de8..3c5e22c 100644 --- a/src/testing/graph_topology_conversion_testing.rs +++ b/src/testing/graph_topology_conversion_testing.rs @@ -31,13 +31,13 @@ where H::Edge: Debug, { let (source, vertices, edges, _) = H::standard(); - let (graph, vertex_map, edge_map) = G::convert(&source); + let (graph, iso) = G::convert(&source); fixtures::assert_isomorphic( &source, &graph, &vertices, - |v| vertex_map[v], + |v| iso.vertex(v), &edges, - |e| edge_map[e], + |e| iso.edge(e), ); } diff --git a/src/traits.rs b/src/traits.rs index 06ca661..0d5414b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -362,14 +362,14 @@ pub struct Incidence { pub edge: E, } -// TODO-0.3.0: Provide and document a way to transform an ElementMap for source to an ElementMap for the copy. /// A trait for constructing an undirected graph topology by converting another graph /// implementation to this one. pub trait GraphTopologyConversion: GraphTopology { /// Creates a new graph instance that is isomorphic to `source`. /// - /// Returns the converted graph instance and two mappings from the vertices and edges of - /// `source` to the corresponding vertices and edges of the returned graph. + /// Returns the converted graph instance and a [`GraphIsomorphism`] instance that maps the + /// vertices and edges of `source` to the corresponding vertices and edges of the returned + /// graph. /// /// # Examples /// @@ -380,18 +380,153 @@ pub trait GraphTopologyConversion: GraphTopology { /// let mut source = AppendGraph::new(); /// let u = source.add_vertex(); /// let v = source.add_vertex(); - /// let e = source.add_edge(u, v); - /// let (graph, vertex_map, edge_map) = FrozenGraph::convert(&source); + /// source.add_edge(u, v); + /// let (graph, iso) = FrozenGraph::convert(&source); /// - /// assert!(graph.are_adjacent(vertex_map[u], vertex_map[v])); + /// assert!(graph.are_adjacent(iso.vertex(u), iso.vertex(v))); /// ``` - fn convert( - source: &G, - ) -> ( - Self, - ElementMap, - ElementMap, - ) + /// + /// The isomorphism can also convert [`ElementMap`] instances defined on `source` so that they + /// can be indexed by vertex or edge handles of the new graph. + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::{AppendGraph, FrozenGraph}; + /// + /// # let mut source = AppendGraph::new(); + /// # let u = source.add_vertex(); + /// # let v = source.add_vertex(); + /// # let (graph, isomorphism) = FrozenGraph::convert(&source); + /// + /// let mut source_map = source.vertex_map(0); + /// source_map[u] = 7; + /// let map = isomorphism.convert_vertex_map(&source_map); + /// + /// assert_eq!(map[isomorphism.vertex(u)], 7); + /// ``` + fn convert(source: &G) -> (Self, GraphIsomorphism) where Self: Sized; } + +/// An explicit isomorphism between a source graph of type `G` and a graph of type `H` converted +/// from it. +/// +/// Returned by [`GraphTopologyConversion::convert`]. It maps each vertex and edge handle of the +/// source graph to the corresponding handle of the converted graph, and can transfer +/// [`ElementMap`] instances defined on the source over to the converted graph. +/// +/// The isomorphism describes the source as it was when it was converted. Handle lookups stay +/// correct for handles that were valid then. If the source graph type reuses storage after +/// deletions (see [`GraphTopologyDeletion`]), do not delete vertices or edges from the source +/// before converting maps, since a new element could take the place of a deleted one and be paired +/// with it. +pub struct GraphIsomorphism { + vertices: ElementMap, + edges: ElementMap, + vertex_map_template: ElementMap, + edge_map_template: ElementMap, +} + +impl GraphIsomorphism { + /// Creates an isomorphism from the given handle mappings, for use by implementations of + /// [`GraphTopologyConversion::convert`]. + /// + /// `vertices` and `edges` must map every vertex and edge of the source to the corresponding + /// vertex and edge of `target`, respectively. Their default values mark unmapped slots, so they + /// must be handles that are never valid in `target`, and must not have been assigned to any + /// mapped slot. Passing mappings that are not an isomorphism results in unspecified, but + /// memory-safe, behaviour. + pub fn new( + target: &H, + vertices: ElementMap, + edges: ElementMap, + ) -> Self { + Self { + vertices, + edges, + vertex_map_template: target.vertex_map(()), + edge_map_template: target.edge_map(()), + } + } + + /// Returns the vertex of the converted graph that corresponds to the source vertex `v`. + /// + /// `v` must have been a vertex of the source graph when it was converted. Otherwise, the + /// returned handle may be invalid, or this method may panic. + pub fn vertex(&self, v: G::Vertex) -> H::Vertex { + self.vertices[v] + } + + /// Returns the edge of the converted graph that corresponds to the source edge `e`. + /// + /// `e` must have been an edge of the source graph when it was converted. Otherwise, the + /// returned handle may be invalid, or this method may panic. + pub fn edge(&self, e: G::Edge) -> H::Edge { + self.edges[e] + } + + /// Converts a vertex map of the source into an equivalent vertex map of the converted graph. + /// + /// For each source vertex *v*, the returned map holds a clone of `map[v]` at `vertex(v)`. It + /// has the same default value as `map`, and it covers the vertices of the converted graph at + /// the time of conversion. Use [`GraphTopologyAddition::sync_vertex_map`] before accessing + /// vertices added to the converted graph afterwards. Missing slots of `map`, for instance + /// because `map` was created before the source grew, are treated as holding the default value. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::{AppendGraph, FrozenGraph}; + /// let mut source = AppendGraph::new(); + /// let u = source.add_vertex(); + /// let v = source.add_vertex(); + /// let (graph, iso) = FrozenGraph::convert(&source); + /// + /// let mut labels = source.vertex_map("none"); + /// labels[u] = "start"; + /// let converted = iso.convert_vertex_map(&labels); + /// assert_eq!(converted[iso.vertex(u)], "start"); + /// assert_eq!(converted[iso.vertex(v)], "none"); + /// ``` + pub fn convert_vertex_map( + &self, + map: &ElementMap, + ) -> ElementMap { + map.remap(&self.vertices, &self.vertex_map_template) + } + + /// Converts an edge map of the source into an equivalent edge map of the converted graph. + /// + /// For each source edge *e*, the returned map holds a clone of `map[e]` at `edge(e)`. It has + /// the same default value as `map`, and it covers the edges of the converted graph at the time + /// of conversion. Use [`GraphTopologyAddition::sync_edge_map`] before accessing edges added to + /// the converted graph afterwards. Missing slots of `map`, for instance because `map` was + /// created before the source grew, are treated as holding the default value. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::{AppendGraph, FrozenGraph}; + /// let mut source = AppendGraph::new(); + /// let u = source.add_vertex(); + /// let v = source.add_vertex(); + /// let e = source.add_edge(u, v); + /// let f = source.add_edge(u, v); + /// let (graph, iso) = FrozenGraph::convert(&source); + /// + /// let mut weights = source.edge_map(1); + /// weights[e] = 5; + /// let converted = iso.convert_edge_map(&weights); + /// assert_eq!(converted[iso.edge(e)], 5); + /// assert_eq!(converted[iso.edge(f)], 1); + /// ``` + pub fn convert_edge_map( + &self, + map: &ElementMap, + ) -> ElementMap { + map.remap(&self.edges, &self.edge_map_template) + } +} -- 2.43.0 From 7cfee9cce5dd688307acff6076afa441fc121b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 12 Oct 2026 21:52:51 +0200 Subject: [PATCH 59/67] Remove function calls inside of 'expect' --- src/testing/fixtures.rs | 16 ++-- .../graph_topology_deletion_testing.rs | 30 +++++--- src/testing/graph_topology_testing.rs | 73 +++++++++++-------- 3 files changed, 69 insertions(+), 50 deletions(-) diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 5689c28..52e8953 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -318,10 +318,12 @@ where G::Vertex: Debug, G::Edge: Debug, { - let path = path_option.expect(&format!( - "path should exist between source vertex {:?} and connected targets {:?}", - source, targets - )); + let path = path_option.unwrap_or_else(|| { + panic!( + "path should exist between source vertex {:?} and connected targets {:?}", + source, targets + ) + }); if targets.contains(&source) { assert!( @@ -402,9 +404,9 @@ pub fn assert_isomorphic( .position(|x| { vertex_map(x.vertex) == incidence.vertex && edge_map(x.edge) == incidence.edge }) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {v:?} in the other graph" - )); + .unwrap_or_else(|| { + panic!("unexpected incidence {incidence:?} of vertex {v:?} in the other graph") + }); expected.swap_remove(pos); } assert!( diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs index d1d82e7..1ef5217 100644 --- a/src/testing/graph_topology_deletion_testing.rs +++ b/src/testing/graph_topology_deletion_testing.rs @@ -452,10 +452,12 @@ fn assert_vertex_incidences( let pos = expected .iter() .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from iterator after delete", - v - )); + .unwrap_or_else(|| { + panic!( + "unexpected incidence {incidence:?} of vertex {:?} from iterator after delete", + v + ) + }); expected.swap_remove(pos); } assert!( @@ -479,10 +481,12 @@ fn assert_vertex_incidence_cursor( let pos = expected .iter() .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from cursor after delete", - v - )); + .unwrap_or_else(|| { + panic!( + "unexpected incidence {incidence:?} of vertex {:?} from cursor after delete", + v + ) + }); expected.swap_remove(pos); } assert!( @@ -508,10 +512,12 @@ fn assert_incident_edges_after_delete( v ); for e in graph.incident_edges(v) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} after delete", - v - )); + let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { + panic!( + "unexpected incident edge {e:?} of vertex {:?} after delete", + v + ) + }); expected.swap_remove(pos); } assert!( diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index d595c2d..df58962 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -278,10 +278,12 @@ where let i = expected_adjacency .iter() .position(|w| *w == v) - .expect(&format!( - "unexpected adjacent vertex {v:?} of {:?} from iterator", - vertices[4] - )); + .unwrap_or_else(|| { + panic!( + "unexpected adjacent vertex {v:?} of {:?} from iterator", + vertices[4] + ) + }); expected_adjacency.swap_remove(i); } assert_eq!( @@ -443,10 +445,12 @@ where ); let mut expected: Vec<_> = incidences[i].iter().map(|x| x.edge).collect(); for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?} from iterator", - vertices[i] - )); + let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { + panic!( + "unexpected incident edge {e:?} of vertex {:?} from iterator", + vertices[i] + ) + }); expected.swap_remove(pos); } assert!( @@ -487,10 +491,9 @@ where for i in 0..2 { let mut expected = edges.to_vec(); for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "unexpected incident edge {e:?} of vertex {:?}", - vertices[i] - )); + let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { + panic!("unexpected incident edge {e:?} of vertex {:?}", vertices[i]) + }); expected.swap_remove(pos); } assert!( @@ -529,10 +532,12 @@ where let pos = expected .iter() .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from iterator", - vertices[i] - )); + .unwrap_or_else(|| { + panic!( + "unexpected incidence {incidence:?} of vertex {:?} from iterator", + vertices[i] + ) + }); expected.swap_remove(pos); } assert!( @@ -611,10 +616,12 @@ where for i in 0..2 { let mut iter = graph.incidences(vertices[i]); for j in 0..K { - let current = iter.next().expect(&format!( - "incidence {j} missing, expected {K} incidences for vertex {:?}", - vertices[i] - )); + let current = iter.next().unwrap_or_else(|| { + panic!( + "incidence {j} missing, expected {K} incidences for vertex {:?}", + vertices[i] + ) + }); assert_eq!( current.vertex, vertices[1 - i], @@ -665,10 +672,12 @@ where let pos = expected .iter() .position(|x| *x == incidence) - .expect(&format!( - "unexpected incidence {incidence:?} of vertex {:?} from cursor", - vertices[i] - )); + .unwrap_or_else(|| { + panic!( + "unexpected incidence {incidence:?} of vertex {:?} from cursor", + vertices[i] + ) + }); expected.swap_remove(pos); } assert!( @@ -710,10 +719,12 @@ where for i in 0..2 { let mut cursor = graph.incidence_cursor(vertices[i]); for j in 0..K { - let current = cursor.next(&graph).expect(&format!( - "incidence {j} missing, expected {K} incidences for vertex {:?}", - vertices[i] - )); + let current = cursor.next(&graph).unwrap_or_else(|| { + panic!( + "incidence {j} missing, expected {K} incidences for vertex {:?}", + vertices[i] + ) + }); assert_eq!( current.vertex, vertices[1 - i], @@ -789,9 +800,9 @@ where for u in graph.vertices() { let mut expected: Vec<_> = graph.incidences(u).map(|x| x.edge).collect(); for e in graph.incident_edges(u) { - let pos = expected.iter().position(|f| *f == e).expect(&format!( - "incident edge {e:?} of vertex {u:?} missing from incidences" - )); + let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { + panic!("incident edge {e:?} of vertex {u:?} missing from incidences") + }); expected.swap_remove(pos); } assert!( -- 2.43.0 From 3d1e8246a8138222e50c7311d9aa30edb3d8568f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 13 Oct 2026 21:25:50 +0200 Subject: [PATCH 60/67] Fix clippy findings and related issues --- src/algorithms.rs | 3 +- src/lib.rs | 4 +- src/maps.rs | 2 +- src/models/frozen_graph.rs | 4 +- src/testing/fixtures.rs | 15 +- .../graph_topology_addition_testing.rs | 10 +- .../graph_topology_deletion_testing.rs | 134 ++++++------- src/testing/graph_topology_testing.rs | 183 +++++++----------- src/traits.rs | 1 + tests/dfs.rs | 20 +- tests/dijkstra.rs | 57 +++--- 11 files changed, 179 insertions(+), 254 deletions(-) diff --git a/src/algorithms.rs b/src/algorithms.rs index d36653d..19ff46d 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -558,8 +558,7 @@ where let mut visited = graph.vertex_map(false); visited[source] = true; - let mut predecessor: ElementMap> = - graph.vertex_map(None); + let mut predecessor = graph.vertex_map(None); let mut queue = VecDeque::from([source]); while let Some(v) = queue.pop_front() { diff --git a/src/lib.rs b/src/lib.rs index 843225d..51f83a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,10 @@ //! Currently supported are: //! //! * Undirected graph types [`Graph`], [`AppendGraph`], and [`FrozenGraph`] built on a flat, -//! index-based adjacency list, +//! index-based adjacency list, //! * [`ElementMap`] to freely associate custom data with vertices and edges, //! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different -//! variants, +//! variants, //! * Exposed [`traits`] to implement custom graph data structures or algorithms. //! //! # Usage example diff --git a/src/maps.rs b/src/maps.rs index bb45b09..7c01d8a 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -89,7 +89,7 @@ impl ElementMap { /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> ElementMap { ElementMap { - data: self.data.iter().map(|v| f(v)).collect(), + data: self.data.iter().map(&f).collect(), default: f(&self.default), to_index: self.to_index, } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 9167f69..4c1f8c5 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -308,8 +308,8 @@ mod tests { #[test] fn opposite_symmetry() { let (graph, ..) = FrozenGraph::standard(); - for i in 0..graph.incidences.len() { - let opp = graph.incidences[i].opposite; + for (i, inc) in graph.incidences.iter().enumerate() { + let opp = inc.opposite; assert_ne!(opp, i, "incidence should not equal its own opposite"); assert_eq!( graph.incidences[opp].opposite, i, diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 52e8953..721bf1f 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -5,6 +5,8 @@ use crate::models::{AppendGraph, FrozenGraph}; use crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence}; pub trait MakeTestGraph: GraphTopology + Sized { + // Since this is only a test fixture, the complex return type here is deemed acceptable. + #[allow(clippy::type_complexity)] fn standard() -> ( Self, [Self::Vertex; 10], @@ -293,12 +295,12 @@ pub fn assert_standard_unweighted_predecessors_v0( vec![Some(vertices[4])], vec![Some(vertices[5]), Some(vertices[6]), Some(vertices[7])], ]; - for i in 0..10 { + assert_eq!(vertices.len(), expected.len()); + for (&v, exp) in vertices.iter().zip(&expected) { assert!( - expected[i].contains(&actual[vertices[i]]), - "unexpected predecessor {:?} of vertex {:?}", - actual[vertices[i]], - vertices[i] + exp.contains(&actual[v]), + "unexpected predecessor {:?} of vertex {v:?}", + actual[v] ); } } @@ -320,8 +322,7 @@ where { let path = path_option.unwrap_or_else(|| { panic!( - "path should exist between source vertex {:?} and connected targets {:?}", - source, targets + "path should exist between source vertex {source:?} and connected targets {targets:?}" ) }); diff --git a/src/testing/graph_topology_addition_testing.rs b/src/testing/graph_topology_addition_testing.rs index 935d715..c15d0e6 100644 --- a/src/testing/graph_topology_addition_testing.rs +++ b/src/testing/graph_topology_addition_testing.rs @@ -43,10 +43,11 @@ pub fn reserve_vertices_increases_capacity() { } pub fn reserve_vertices_prevents_reallocation_on_add() { + const N: usize = 10; let mut graph = G::default(); - graph.reserve_vertices(10); + graph.reserve_vertices(N); let capacity_before = graph.vertex_capacity(); - for _ in 0..10 { + for _ in 0..N { graph.add_vertex(); } assert_eq!( @@ -78,12 +79,13 @@ pub fn reserve_edges_increases_capacity() { } pub fn reserve_edges_prevents_reallocation_on_add() { + const N: usize = 10; let mut graph = G::default(); let u = graph.add_vertex(); let v = graph.add_vertex(); - graph.reserve_edges(10); + graph.reserve_edges(N); let capacity_before = graph.edge_capacity(); - for _ in 0..10 { + for _ in 0..N { graph.add_edge(u, v); } assert_eq!( diff --git a/src/testing/graph_topology_deletion_testing.rs b/src/testing/graph_topology_deletion_testing.rs index 1ef5217..7ad8edf 100644 --- a/src/testing/graph_topology_deletion_testing.rs +++ b/src/testing/graph_topology_deletion_testing.rs @@ -262,7 +262,8 @@ where G::Vertex: Debug, { let (mut graph, vertices, _, _) = G::standard(); - graph.delete_vertex(vertices[2]); + let del = vertices[2]; + graph.delete_vertex(del); assert_eq!( graph.vertex_count(), 9, @@ -274,17 +275,12 @@ where "unexpected vertex iterator count after delete" ); for v in graph.vertices() { - assert_ne!( - v, vertices[2], - "deleted vertex {:?} appeared in iterator", - vertices[2] - ); + assert_ne!(v, del, "deleted vertex {del:?} appeared in iterator"); } - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { + for &v in vertices.iter().filter(|&&u| u != del) { assert!( - graph.vertices().any(|v| v == vertices[i]), - "expected vertex {:?} missing from iterator after delete", - vertices[i] + graph.vertices().any(|u| u == v), + "expected vertex {v:?} missing from iterator after delete" ); } } @@ -295,9 +291,9 @@ where G::Edge: Debug, { let (mut graph, _, edges, _) = G::standard(); - let e = edges[2].0; - graph.delete_edge(e); - for &(f, v0, v1) in edges.iter().filter(|&&(f, _, _)| f != e) { + let del = edges[2].0; + graph.delete_edge(del); + for &(f, v0, v1) in edges.iter().filter(|&&(f, _, _)| f != del) { let (u0, u1) = graph.incident_vertices(f); assert!( (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), @@ -340,13 +336,15 @@ where G::Edge: Debug, { let (mut graph, vertices, edges, incidences) = G::standard(); - graph.delete_edge(edges[2].0); - for i in 0..10 { - let mut expected: Vec<_> = incidences[i] + let del = edges[2].0; + graph.delete_edge(del); + assert_eq!(vertices.len(), incidences.len()); + for (&v, incs) in vertices.iter().zip(&incidences) { + let mut expected = incs .iter() - .filter_map(|x| (x.edge != edges[2].0).then_some(x.edge)) + .filter_map(|x| (x.edge != del).then_some(x.edge)) .collect(); - assert_incident_edges_after_delete(&graph, vertices[i], &mut expected); + assert_incident_edges_after_delete(&graph, v, &mut expected); } } @@ -356,13 +354,14 @@ where G::Edge: Debug, { let (mut graph, vertices, _, incidences) = G::standard(); - graph.delete_vertex(vertices[2]); - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - let mut expected: Vec<_> = incidences[i] + let del = vertices[2]; + graph.delete_vertex(del); + for (&v, incs) in vertices.iter().zip(&incidences).filter(|&(&u, _)| u != del) { + let mut expected = incs .iter() - .filter_map(|x| (x.vertex != vertices[2]).then_some(x.edge)) + .filter_map(|x| (x.vertex != del).then_some(x.edge)) .collect(); - assert_incident_edges_after_delete(&graph, vertices[i], &mut expected); + assert_incident_edges_after_delete(&graph, v, &mut expected); } } @@ -372,14 +371,11 @@ where G::Edge: Debug, { let (mut graph, vertices, _, incidences) = G::standard(); - graph.delete_vertex(vertices[2]); - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - let remaining = incidences[i] - .iter() - .filter(|x| x.vertex != vertices[2]) - .cloned() - .collect(); - assert_vertex_incidences(&graph, vertices[i], remaining); + let del = vertices[2]; + graph.delete_vertex(del); + for (&v, incs) in vertices.iter().zip(&incidences).filter(|&(&u, _)| u != del) { + let remaining = incs.iter().filter(|x| x.vertex != del).cloned().collect(); + assert_vertex_incidences(&graph, v, remaining); } } @@ -389,14 +385,12 @@ where G::Edge: Debug, { let (mut graph, vertices, edges, incidences) = G::standard(); - graph.delete_edge(edges[2].0); - for i in 0..10 { - let remaining = incidences[i] - .iter() - .filter(|x| x.edge != edges[2].0) - .cloned() - .collect(); - assert_vertex_incidences(&graph, vertices[i], remaining); + let del = edges[2].0; + graph.delete_edge(del); + assert_eq!(vertices.len(), incidences.len()); + for (&v, incs) in vertices.iter().zip(&incidences) { + let remaining = incs.iter().filter(|x| x.edge != del).cloned().collect(); + assert_vertex_incidences(&graph, v, remaining); } } @@ -406,14 +400,11 @@ where G::Edge: Debug, { let (mut graph, vertices, _, incidences) = G::standard(); - graph.delete_vertex(vertices[2]); - for i in [0, 1, 3, 4, 5, 6, 7, 8, 9] { - let remaining = incidences[i] - .iter() - .filter(|x| x.vertex != vertices[2]) - .cloned() - .collect(); - assert_vertex_incidence_cursor(&graph, vertices[i], remaining); + let del = vertices[2]; + graph.delete_vertex(del); + for (&v, incs) in vertices.iter().zip(&incidences).filter(|&(&u, _)| u != del) { + let remaining = incs.iter().filter(|x| x.vertex != del).cloned().collect(); + assert_vertex_incidence_cursor(&graph, v, remaining); } } @@ -423,14 +414,12 @@ where G::Edge: Debug, { let (mut graph, vertices, edges, incidences) = G::standard(); - graph.delete_edge(edges[2].0); - for i in 0..10 { - let remaining = incidences[i] - .iter() - .filter(|x| x.edge != edges[2].0) - .cloned() - .collect(); - assert_vertex_incidence_cursor(&graph, vertices[i], remaining); + let del = edges[2].0; + graph.delete_edge(del); + assert_eq!(vertices.len(), incidences.len()); + for (&v, incs) in vertices.iter().zip(&incidences) { + let remaining = incs.iter().filter(|x| x.edge != del).cloned().collect(); + assert_vertex_incidence_cursor(&graph, v, remaining); } } @@ -445,26 +434,22 @@ fn assert_vertex_incidences( assert_eq!( graph.incidences(v).count(), expected.len(), - "unexpected incidence count for vertex {:?} after delete", - v + "unexpected incidence count for vertex {v:?} after delete" ); for incidence in graph.incidences(v) { let pos = expected .iter() - .position(|x| *x == incidence) + .position(|&x| x == incidence) .unwrap_or_else(|| { panic!( - "unexpected incidence {incidence:?} of vertex {:?} from iterator after delete", - v + "unexpected incidence {incidence:?} of vertex {v:?} from iterator after delete" ) }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incidences {:?} of vertex {:?} not matched by iterator after delete", - expected, - v + "expected incidences {expected:?} of vertex {v:?} not matched by iterator after delete" ); } @@ -480,20 +465,17 @@ fn assert_vertex_incidence_cursor( while let Some(incidence) = cursor.next(graph) { let pos = expected .iter() - .position(|x| *x == incidence) + .position(|&x| x == incidence) .unwrap_or_else(|| { panic!( - "unexpected incidence {incidence:?} of vertex {:?} from cursor after delete", - v + "unexpected incidence {incidence:?} of vertex {v:?} from cursor after delete" ) }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incidences {:?} of vertex {:?} not matched by cursor after delete", - expected, - v + "expected incidences {expected:?} of vertex {v:?} not matched by cursor after delete" ); } @@ -508,22 +490,16 @@ fn assert_incident_edges_after_delete( assert_eq!( graph.incident_edges(v).count(), expected.len(), - "unexpected incident edge count for vertex {:?} after delete", - v + "unexpected incident edge count for vertex {v:?} after delete" ); for e in graph.incident_edges(v) { - let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { - panic!( - "unexpected incident edge {e:?} of vertex {:?} after delete", - v - ) + let pos = expected.iter().position(|&f| f == e).unwrap_or_else(|| { + panic!("unexpected incident edge {e:?} of vertex {v:?} after delete") }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incident edges {:?} of vertex {:?} not matched after delete", - expected, - v + "expected incident edges {expected:?} of vertex {v:?} not matched after delete" ); } diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index df58962..2c4c394 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -128,14 +128,10 @@ where G::Vertex: Debug, { let (graph, vertices, _, _) = G::standard(); - let expected_degrees = [2, 5, 7, 2, 7, 2, 3, 3, 2, 3]; - for i in 0..graph.vertex_count() { - assert_eq!( - graph.degree(vertices[i]), - expected_degrees[i], - "unexpected degree of {:?}", - vertices[i] - ); + let expected = [2, 5, 7, 2, 7, 2, 3, 3, 2, 3]; + assert_eq!(vertices.len(), expected.len()); + for (&v, &exp) in vertices.iter().zip(&expected) { + assert_eq!(graph.degree(v), exp, "unexpected degree of {v:?}"); } } @@ -200,23 +196,18 @@ where vertices[9], vertices[3] ); - for i in 0..graph.vertex_count() { - let exp = match i { - 1 | 2 | 4 | 5 | 6 => true, - _ => false, - }; + for (i, &v) in vertices.iter().enumerate() { + let exp = matches!(i, 1 | 2 | 4 | 5 | 6); assert_eq!( - graph.are_adjacent(vertices[2], vertices[i]), + graph.are_adjacent(vertices[2], v), exp, - "unexpected adjacency of {:?} and {:?}", - vertices[2], - vertices[i] + "unexpected adjacency of {:?} and {v:?}", + vertices[2] ); assert_eq!( - graph.are_adjacent(vertices[i], vertices[2]), + graph.are_adjacent(v, vertices[2]), exp, - "unexpected adjacency of {:?} and {:?}", - vertices[i], + "unexpected adjacency of {v:?} and {:?}", vertices[2] ); } @@ -289,8 +280,7 @@ where assert_eq!( expected_adjacency.len(), 0, - "expected adjacent vertices {:?} of {:?} were not matched by iterator", - expected_adjacency, + "expected adjacent vertices {expected_adjacency:?} of {:?} were not matched by iterator", vertices[4] ); } @@ -320,21 +310,19 @@ where { const K: usize = 3; let (graph, vertices, _) = G::multiple_edges::(); - for i in 0..2 { - let mut iter = graph.adjacent_vertices(vertices[i]); + for (i, &v) in vertices.iter().enumerate() { + let mut iter = graph.adjacent_vertices(v); for j in 0..K { assert_eq!( iter.next(), Some(vertices[1 - i]), - "unexpected adjacent vertex {j} of vertex {:?}", - vertices[i] + "unexpected adjacent vertex {j} of vertex {v:?}" ); } assert_eq!( iter.next(), None, - "too many adjacent vertices of vertex {:?} from iterator", - vertices[i] + "too many adjacent vertices of vertex {v:?} from iterator" ); } } @@ -372,12 +360,11 @@ where { let (graph, [v0, v1], edges) = G::multiple_edges::<2>(); assert_ne!(edges[0], edges[1], "edges should be distinct"); - for i in 0..2 { - let (u0, u1) = graph.incident_vertices(edges[i]); + for (i, &e) in edges.iter().enumerate() { + let (u0, u1) = graph.incident_vertices(e); assert!( (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), - "unexpected incident vertices {u0:?} and {u1:?} for multi-edge {:?} (index {i})", - edges[i] + "unexpected incident vertices {u0:?} and {u1:?} for multi-edge {e:?} (index {i})" ); } } @@ -436,28 +423,23 @@ where G::Edge: Debug, { let (graph, vertices, _, incidences) = G::standard(); - for i in 0..10 { + assert_eq!(vertices.len(), incidences.len()); + for (&v, incs) in vertices.iter().zip(&incidences) { assert_eq!( - graph.incident_edges(vertices[i]).count(), - incidences[i].len(), - "unexpected incident edge count for vertex {:?}", - vertices[i] + graph.incident_edges(v).count(), + incs.len(), + "unexpected incident edge count for vertex {v:?}" ); - let mut expected: Vec<_> = incidences[i].iter().map(|x| x.edge).collect(); - for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { - panic!( - "unexpected incident edge {e:?} of vertex {:?} from iterator", - vertices[i] - ) + let mut expected: Vec<_> = incs.iter().map(|x| x.edge).collect(); + for e in graph.incident_edges(v) { + let pos = expected.iter().position(|&f| f == e).unwrap_or_else(|| { + panic!("unexpected incident edge {e:?} of vertex {v:?} from iterator") }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incident edges {:?} of vertex {:?} were not matched by iterator", - expected, - vertices[i] + "expected incident edges {expected:?} of vertex {v:?} were not matched by iterator" ); } } @@ -488,19 +470,18 @@ where { const K: usize = 3; let (graph, vertices, edges) = G::multiple_edges::(); - for i in 0..2 { + for &v in &vertices { let mut expected = edges.to_vec(); - for e in graph.incident_edges(vertices[i]) { - let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { - panic!("unexpected incident edge {e:?} of vertex {:?}", vertices[i]) - }); + for e in graph.incident_edges(v) { + let pos = expected + .iter() + .position(|&f| f == e) + .unwrap_or_else(|| panic!("unexpected incident edge {e:?} of vertex {v:?}")); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incident edges {:?} of vertex {:?} were not matched by iterator", - expected, - vertices[i] + "expected incident edges {expected:?} of vertex {v:?} were not matched by iterator" ); } } @@ -520,31 +501,26 @@ where G::Edge: Debug, { let (graph, vertices, _, incidences) = G::standard(); - for i in 0..10 { + assert_eq!(vertices.len(), incidences.len()); + for (&v, incs) in vertices.iter().zip(&incidences) { assert_eq!( - graph.incidences(vertices[i]).count(), - incidences[i].len(), - "unexpected incidence count for vertex {:?}", - vertices[i] + graph.incidences(v).count(), + incs.len(), + "unexpected incidence count for vertex {v:?}" ); - let mut expected = incidences[i].clone(); - for incidence in graph.incidences(vertices[i]) { + let mut expected = incs.clone(); + for incidence in graph.incidences(v) { let pos = expected .iter() - .position(|x| *x == incidence) + .position(|&x| x == incidence) .unwrap_or_else(|| { - panic!( - "unexpected incidence {incidence:?} of vertex {:?} from iterator", - vertices[i] - ) + panic!("unexpected incidence {incidence:?} of vertex {v:?} from iterator") }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incidences {:?} of vertex {:?} were not matched by iterator", - expected, - vertices[i] + "expected incidences {expected:?} of vertex {v:?} were not matched by iterator" ); } } @@ -613,34 +589,28 @@ where { const K: usize = 3; let (graph, vertices, edges) = G::multiple_edges::(); - for i in 0..2 { - let mut iter = graph.incidences(vertices[i]); + for (i, &v) in vertices.iter().enumerate() { + let mut iter = graph.incidences(v); for j in 0..K { let current = iter.next().unwrap_or_else(|| { - panic!( - "incidence {j} missing, expected {K} incidences for vertex {:?}", - vertices[i] - ) + panic!("incidence {j} missing, expected {K} incidences for vertex {v:?}") }); assert_eq!( current.vertex, vertices[1 - i], - "unexpected adjacent vertex of vertex {:?} in incidence {j}", - vertices[i] + "unexpected adjacent vertex of vertex {v:?} in incidence {j}" ); assert_eq!( - edges.iter().filter(|e| **e == current.edge).count(), + edges.iter().filter(|&&e| e == current.edge).count(), 1, - "unexpected incident edge {:?} of vertex {:?}", - current.edge, - vertices[i], + "unexpected incident edge {:?} of vertex {v:?}", + current.edge ); } assert_eq!( iter.next(), None, - "too many adjacent vertices of {:?} from iterator", - vertices[i] + "too many adjacent vertices of {v:?} from iterator" ); } } @@ -665,26 +635,22 @@ where G::Edge: Debug, { let (graph, vertices, _, incidences) = G::standard(); - for i in 0..10 { - let mut expected = incidences[i].clone(); - let mut cursor = graph.incidence_cursor(vertices[i]); + assert_eq!(vertices.len(), incidences.len()); + for (&v, incs) in vertices.iter().zip(&incidences) { + let mut expected = incs.clone(); + let mut cursor = graph.incidence_cursor(v); while let Some(incidence) = cursor.next(&graph) { let pos = expected .iter() - .position(|x| *x == incidence) + .position(|&x| x == incidence) .unwrap_or_else(|| { - panic!( - "unexpected incidence {incidence:?} of vertex {:?} from cursor", - vertices[i] - ) + panic!("unexpected incidence {incidence:?} of vertex {v:?} from cursor") }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "expected incidences {:?} of vertex {:?} were not matched by cursor", - expected, - vertices[i] + "expected incidences {expected:?} of vertex {v:?} were not matched by cursor" ); } } @@ -716,34 +682,28 @@ where { const K: usize = 3; let (graph, vertices, edges) = G::multiple_edges::(); - for i in 0..2 { - let mut cursor = graph.incidence_cursor(vertices[i]); + for (i, &v) in vertices.iter().enumerate() { + let mut cursor = graph.incidence_cursor(v); for j in 0..K { let current = cursor.next(&graph).unwrap_or_else(|| { - panic!( - "incidence {j} missing, expected {K} incidences for vertex {:?}", - vertices[i] - ) + panic!("incidence {j} missing, expected {K} incidences for vertex {v:?}") }); assert_eq!( current.vertex, vertices[1 - i], - "unexpected adjacent vertex of vertex {:?} in incidence {j}", - vertices[i] + "unexpected adjacent vertex of vertex {v:?} in incidence {j}" ); assert_eq!( - edges.iter().filter(|e| **e == current.edge).count(), + edges.iter().filter(|&&e| e == current.edge).count(), 1, - "unexpected incident edge {:?} of vertex {:?}", - current.edge, - vertices[i], + "unexpected incident edge {:?} of vertex {v:?}", + current.edge ); } assert_eq!( cursor.next(&graph), None, - "too many incidences of {:?} from cursor", - vertices[i] + "too many incidences of {v:?} from cursor" ); } } @@ -800,15 +760,14 @@ where for u in graph.vertices() { let mut expected: Vec<_> = graph.incidences(u).map(|x| x.edge).collect(); for e in graph.incident_edges(u) { - let pos = expected.iter().position(|f| *f == e).unwrap_or_else(|| { + let pos = expected.iter().position(|&f| f == e).unwrap_or_else(|| { panic!("incident edge {e:?} of vertex {u:?} missing from incidences") }); expected.swap_remove(pos); } assert!( expected.is_empty(), - "incidence edges {:?} of vertex {u:?} not matched by incident_edges", - expected + "incidence edges {expected:?} of vertex {u:?} not matched by incident_edges" ); } } diff --git a/src/traits.rs b/src/traits.rs index 0d5414b..7f27523 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -347,6 +347,7 @@ pub trait IncidenceCursor: Copy { /// assert!(c2.next(&graph).is_some()); /// assert!(c2.next(&graph).is_none()); /// ``` + // TODO: Replace the above example graph with a path generator function. fn next(&mut self, graph: &G) -> Option>; } diff --git a/tests/dfs.rs b/tests/dfs.rs index 25fa58b..39afd23 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -162,11 +162,10 @@ where G::Vertex: Debug, { let (graph, vertices, _, _) = G::standard(); - for i in 0..10 { + for v in vertices { assert!( - algorithms::dfs_find(&graph, vertices[0], vertices[i]), - "vertex {:?} should be reachable from {:?}", - vertices[i], + algorithms::dfs_find(&graph, vertices[0], v), + "vertex {v:?} should be reachable from {:?}", vertices[0] ); } @@ -328,12 +327,8 @@ fn assert_dfs_visited( ) where G::Vertex: Debug, { - for i in 0..10 { - assert!( - visited[vertices[i]], - "vertex {:?} should be visited", - vertices[i] - ); + for &v in vertices { + assert!(visited[v], "vertex {v:?} should be visited"); } } @@ -349,9 +344,8 @@ fn assert_dfs_predecessors( predecessors[vertices[0]], None, "source should have no predecessor" ); - for i in 1..10 { - let v = vertices[i]; - let p = predecessors[v].expect(&format!("vertex {v:?} should have a predecessor")); + for &v in vertices.iter().skip(1) { + let p = predecessors[v].unwrap_or_else(|| panic!("vertex {v:?} should have a predecessor")); assert!( visited[p], "predecessor {p:?} of vertex {v:?} should be visited" diff --git a/tests/dijkstra.rs b/tests/dijkstra.rs index 36a69cc..a2f5559 100644 --- a/tests/dijkstra.rs +++ b/tests/dijkstra.rs @@ -6,7 +6,7 @@ use grapherity::algorithms::DijkstraResult; use grapherity::maps::ElementMap; use grapherity::testing::fixtures; use grapherity::testing::fixtures::MakeTestGraph; -use grapherity::traits::{GraphTopology, Incidence}; +use grapherity::traits::GraphTopology; macro_rules! dijkstra_tests { ($T:ty) => { @@ -78,21 +78,19 @@ fn dijkstra_zero_weight_loop() where G::Vertex: Debug + Hash, { - let (graph, vertices, _, weights) = make_two_edge_path_with_zero_weight_loops::(); + let (graph, vertices, weights) = make_two_edge_path_with_zero_weight_loops::(); let result = algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); assert_single_vertex::(&result, vertices[0]); - for i in 1..3 { + for (i, &v) in vertices.iter().enumerate().skip(1) { assert_eq!( - result.predecessors[vertices[i]], + result.predecessors[v], Some(vertices[i - 1]), - "unexpected predecessor of vertex {:?}", - vertices[i] + "unexpected predecessor of vertex {v:?}" ); assert_eq!( - result.distances[vertices[i]], + result.distances[v], Some(i.try_into().unwrap()), - "unexpected distance of vertex {:?}", - vertices[i] + "unexpected distance of vertex {v:?}" ); } } @@ -101,7 +99,7 @@ fn dijkstra() where G::Vertex: Debug + Hash, { - let (graph, vertices, _, _, weights) = make_standard_weighted::(); + let (graph, vertices, weights) = make_standard_weighted::(); let result = algorithms::dijkstra(&graph, vertices[0], |e| weights[e]); assert_standard_weighted_distances_v0::(&result.distances, &vertices); assert_standard_weighted_predecessors_v0::(&result.predecessors, &vertices); @@ -111,7 +109,7 @@ fn dijkstra_distances() where G::Vertex: Debug + Hash, { - let (graph, vertices, _, _, weights) = make_standard_weighted::(); + let (graph, vertices, weights) = make_standard_weighted::(); let distances = algorithms::dijkstra_distances(&graph, vertices[0], |e| weights[e]); assert_standard_weighted_distances_v0::(&distances, &vertices); } @@ -201,7 +199,7 @@ fn assert_disconnected( ) where G::Vertex: Debug, { - assert_distances_disconnected::(&result.distances, &vertices); + assert_distances_disconnected::(&result.distances, vertices); for &v in vertices { assert_eq!( result.predecessors[v], None, @@ -242,12 +240,12 @@ fn assert_standard_weighted_predecessors_v0( vec![Some(vertices[7])], vec![Some(vertices[6])], ]; - for i in 0..10 { + assert_eq!(vertices.len(), expected.len()); + for (&v, exp) in vertices.iter().zip(&expected) { assert!( - expected[i].contains(&actual[vertices[i]]), - "unexpected predecessor {:?} of {:?}", - actual[vertices[i]], - vertices[i] + exp.contains(&actual[v]), + "unexpected predecessor {:?} of {v:?}", + actual[v] ); } } @@ -270,35 +268,30 @@ fn assert_standard_weighted_distances_v0( Some(145), Some(29), ]; - for i in 0..10 { + assert_eq!(vertices.len(), expected.len()); + for (&v, &exp) in vertices.iter().zip(&expected) { assert_eq!( - actual[vertices[i]], expected[i], - "unexpected distance from {:?} to {:?}", - vertices[0], vertices[i] + actual[v], exp, + "unexpected distance from {:?} to {v:?}", + vertices[0] ); } } -fn make_standard_weighted() -> ( - G, - [G::Vertex; 10], - [(G::Edge, G::Vertex, G::Vertex); 18], - [Vec>; 10], - ElementMap, -) { - let (graph, vertices, edges, incidences) = G::standard(); +fn make_standard_weighted() -> (G, [G::Vertex; 10], ElementMap) { + let (graph, vertices, edges, _) = G::standard(); let mut weights = graph.edge_map(99); for i in [1, 3, 7, 10, 12, 14, 15, 17] { weights[edges[i].0] = i.try_into().unwrap(); } - (graph, vertices, edges, incidences, weights) + (graph, vertices, weights) } fn make_two_edge_path_with_zero_weight_loops() --> (G, [G::Vertex; 3], [G::Edge; 4], ElementMap) { +-> (G, [G::Vertex; 3], ElementMap) { let (graph, vertices, edges) = G::two_edge_path_with_loops(); let mut weights = graph.edge_map(1); weights[edges[2]] = 0; weights[edges[3]] = 0; - (graph, vertices, edges, weights) + (graph, vertices, weights) } -- 2.43.0 From 653cd392791f37ebf43fb45221194c842694546d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Tue, 13 Oct 2026 21:38:44 +0200 Subject: [PATCH 61/67] Fix doc links --- src/models/append_graph.rs | 4 ++-- src/models/graph.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 27c8742..c653627 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -115,8 +115,8 @@ impl IncidenceCursor for AppendGraphIncidenceCursor { /// [`add_vertex`]: Self::add_vertex /// [`add_edge`]: Self::add_edge /// [`degree`]: Self::degree -/// [`FrozenGraph`]: crate::models::FrozenGraph -/// [`Graph`]: crate::models::Graph +/// [`FrozenGraph`]: models::FrozenGraph +/// [`Graph`]: models::Graph #[derive(Clone)] pub struct AppendGraph { vertices: Vec, diff --git a/src/models/graph.rs b/src/models/graph.rs index 6677da0..628cf3a 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -143,7 +143,7 @@ impl IncidenceCursor for GraphIncidenceCursor { /// [`degree`]: Self::degree /// [`delete_vertex`]: Self::delete_vertex /// [`delete_edge`]: Self::delete_edge -/// [`AppendGraph`]: crate::models::AppendGraph +/// [`AppendGraph`]: models::AppendGraph #[derive(Clone)] pub struct Graph { // TODO: Arena index and generation types could be externalized to Graph. -- 2.43.0 From eab5a63e6c7fe6776ff7bc7a4431d1c1f9b28cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 08:45:18 +0200 Subject: [PATCH 62/67] Fix loop patterns --- src/testing/fixtures.rs | 4 ++-- tests/dfs.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/testing/fixtures.rs b/src/testing/fixtures.rs index 721bf1f..b118fe7 100644 --- a/src/testing/fixtures.rs +++ b/src/testing/fixtures.rs @@ -392,7 +392,7 @@ pub fn assert_isomorphic( original.edge_count(), "other graph should have the same edge count as the original" ); - for &v in vertices.iter() { + for &v in vertices { assert_eq!( other.degree(vertex_map(v)), original.degree(v), @@ -415,7 +415,7 @@ pub fn assert_isomorphic( "expected incidences {expected:?} of vertex {v:?} are missing in the other graph" ); } - for &(e, u, v) in edges.iter() { + for &(e, u, v) in edges { assert_eq!( other.incident_vertices(edge_map(e)), (vertex_map(u), vertex_map(v)), diff --git a/tests/dfs.rs b/tests/dfs.rs index 39afd23..0875463 100644 --- a/tests/dfs.rs +++ b/tests/dfs.rs @@ -162,7 +162,7 @@ where G::Vertex: Debug, { let (graph, vertices, _, _) = G::standard(); - for v in vertices { + for &v in &vertices { assert!( algorithms::dfs_find(&graph, vertices[0], v), "vertex {v:?} should be reachable from {:?}", -- 2.43.0 From 9ae841288b22bb416f1fd4ea54efbc43b34c18c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 08:46:47 +0200 Subject: [PATCH 63/67] Add conversion test for empty graph --- .../graph_topology_conversion_testing.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/testing/graph_topology_conversion_testing.rs b/src/testing/graph_topology_conversion_testing.rs index 3c5e22c..b85c0c7 100644 --- a/src/testing/graph_topology_conversion_testing.rs +++ b/src/testing/graph_topology_conversion_testing.rs @@ -10,6 +10,7 @@ macro_rules! graph_topology_conversion_tests { ($T:ty, $U:ty) => { $crate::graph_topology_conversion_tests!(@wrap $T, $U, convert, + convert_empty, ); }; @@ -41,3 +42,22 @@ where |e| iso.edge(e), ); } + +pub fn convert_empty() +where + G::Vertex: Debug, + G::Edge: Debug, + H::Vertex: Debug, + H::Edge: Debug, +{ + let source = H::empty(); + let (graph, iso) = G::convert(&source); + fixtures::assert_isomorphic( + &source, + &graph, + &[], + |v| iso.vertex(v), + &[], + |e| iso.edge(e), + ); +} -- 2.43.0 From dd0ae64984d226e382b0282aa0281a1bb7511a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 09:06:21 +0200 Subject: [PATCH 64/67] Add unit tests for ElementMap::remap --- src/maps.rs | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/src/maps.rs b/src/maps.rs index 7c01d8a..136b815 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -142,6 +142,7 @@ impl IndexMut for ElementMap { #[cfg(test)] mod tests { use super::*; + use std::rc::Rc; #[test] fn initial_values_are_default() { @@ -238,4 +239,143 @@ mod tests { "slot added by grow should hold the mapped default" ); } + + #[test] + fn remap() { + let mut source = ElementMap::new('-', |e| e, 3); + source[0] = 'a'; + source[1] = 'b'; + source[2] = 'c'; + let mut mapping = ElementMap::new(usize::MAX, |e| e, 3); + mapping[0] = 2; + mapping[1] = 0; + mapping[2] = 1; + let template = ElementMap::new((), |f| f, 3); + let result = source.remap(&mapping, &template); + assert_eq!(result[0], 'b', "unexpected value at slot 0"); + assert_eq!(result[1], 'c', "unexpected value at slot 1"); + assert_eq!(result[2], 'a', "unexpected value at slot 2"); + assert_eq!( + result.default, '-', + "default should be taken from the source map" + ); + } + + #[test] + fn remap_skips_default() { + let mut source = ElementMap::new(0, |e| e, 3); + source[0] = 1; + source[1] = 2; + source[2] = 3; + // Slot 1 stays unmapped, which would be out of bounds for the template if it was used. + let mut mapping = ElementMap::new(usize::MAX, |e| e, 3); + mapping[0] = 1; + mapping[2] = 0; + let template = ElementMap::new((), |f| f, 2); + let result = source.remap(&mapping, &template); + assert_eq!(result[0], 3, "unexpected value at slot 0"); + assert_eq!(result[1], 1, "unexpected value at slot 1"); + } + + #[test] + fn remap_unmapped_slots() { + let mut source = ElementMap::new(9, |e| e, 2); + source[0] = 1; + source[1] = 2; + let mut mapping = ElementMap::new(usize::MAX, |e| e, 2); + mapping[0] = 0; + mapping[1] = 2; + let template = ElementMap::new((), |f| f, 4); + let result = source.remap(&mapping, &template); + assert_eq!( + result.data, + vec![1, 9, 2, 9], + "should have the template's length, with unmapped slots holding the source's default" + ); + } + + #[test] + fn remap_index_function() { + let mut source = ElementMap::::new(0, |e| e, 3); + source[0] = 10; + source[1] = 20; + source[2] = 30; + let mut mapping = ElementMap::::new(u32::MAX, |e| e, 3); + mapping[0] = 0; + mapping[1] = 1; + mapping[2] = 2; + // Reverses the order of the slots. + let template = ElementMap::::new((), |f| 2 - f as usize, 3); + let result = source.remap(&mapping, &template); + assert_eq!( + result.data, + vec![30, 20, 10], + "values should be written at the slots given by the template's index function" + ); + assert_eq!( + (result.to_index)(0), + 2, + "index function should be taken from the template" + ); + } + + #[test] + fn remap_shorter_source() { + let mut source = ElementMap::new(9, |e| e, 2); + source[0] = 5; + source[1] = 6; + let mut mapping = ElementMap::new(usize::MAX, |e| e, 4); + for i in 0..4 { + mapping[i] = i; + } + let template = ElementMap::new((), |f| f, 4); + let result = source.remap(&mapping, &template); + assert_eq!(result.data, vec![5, 6, 9, 9], "unexpected data"); + } + + #[test] + fn remap_longer_source() { + let mut source = ElementMap::new(0, |e| e, 4); + for i in 0..4 { + source[i] = i + 1; + } + let mut mapping = ElementMap::new(usize::MAX, |e| e, 2); + mapping[0] = 1; + mapping[1] = 0; + let template = ElementMap::new((), |f| f, 2); + let result = source.remap(&mapping, &template); + assert_eq!(result.data, vec![2, 1], "unexpected data"); + } + + #[test] + fn remap_empty() { + let source = ElementMap::new(0, |e| e, 0); + let mapping = ElementMap::new(usize::MAX, |e| e, 0); + let template = ElementMap::new((), |f| f, 0); + let result = source.remap(&mapping, &template); + assert_eq!(result.data, Vec::::new(), "should be empty"); + } + + #[test] + fn remap_clones_values() { + let value = Rc::new(1); + let mut source = ElementMap::new(Rc::new(0), |e| e, 1); + source[0] = Rc::clone(&value); + let mut mapping = ElementMap::new(usize::MAX, |e| e, 1); + mapping[0] = 0; + let template = ElementMap::new((), |f| f, 1); + let _result = source.remap(&mapping, &template); + // The local variable, the source slot and the result slot each hold one reference. + assert_eq!( + Rc::strong_count(&value), + 3, + "result should hold its own clone of the value" + ); + drop(source); + assert_eq!( + Rc::strong_count(&value), + 2, + "result should keep the value alive on its own" + ); + } } -- 2.43.0 From 7196871a966a23c6028ba62a9ebe8b45b8f2e6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 09:32:15 +0200 Subject: [PATCH 65/67] Add ElementMap conversion tests via new GraphIsomorphism --- ...ph_topology_addition_conversion_testing.rs | 33 +++++++ .../graph_topology_conversion_testing.rs | 91 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/src/testing/graph_topology_addition_conversion_testing.rs b/src/testing/graph_topology_addition_conversion_testing.rs index f3a423b..6d2310c 100644 --- a/src/testing/graph_topology_addition_conversion_testing.rs +++ b/src/testing/graph_topology_addition_conversion_testing.rs @@ -8,6 +8,7 @@ macro_rules! graph_topology_addition_conversion_tests { ($T:ty, $U:ty) => { $crate::graph_topology_addition_conversion_tests!(@wrap $T, $U, convert_mutation_independent, + convert_map_stale_source_map, ); }; @@ -30,3 +31,35 @@ where let (mut graph, iso) = G::convert(&source); fixtures::assert_mutation_independent(&mut source, &mut graph, &vertices, |v| iso.vertex(v)); } + +pub fn convert_map_stale_source_map() +where + G: GraphTopologyConversion + GraphTopologyAddition, + H: MakeTestGraph + GraphTopologyAddition, +{ + let (mut source, [u, v], _) = H::single_edge(); + let mut source_map = source.vertex_map(9); + source_map[u] = 1; + source_map[v] = 2; + let added = add_vertices_until_capacity_grows(&mut source); + let (_, iso) = G::convert(&source); + let map = iso.convert_vertex_map(&source_map); + assert_eq!(map[iso.vertex(u)], 1, "unexpected value for first vertex"); + assert_eq!(map[iso.vertex(v)], 2, "unexpected value for second vertex"); + for w in added { + assert_eq!( + map[iso.vertex(w)], + 9, + "vertex not covered by the source map should hold the default" + ); + } +} + +fn add_vertices_until_capacity_grows(graph: &mut G) -> Vec { + let capacity = graph.vertex_capacity(); + let mut added = Vec::new(); + while graph.vertex_capacity() == capacity { + added.push(graph.add_vertex()); + } + added +} diff --git a/src/testing/graph_topology_conversion_testing.rs b/src/testing/graph_topology_conversion_testing.rs index b85c0c7..6478ed9 100644 --- a/src/testing/graph_topology_conversion_testing.rs +++ b/src/testing/graph_topology_conversion_testing.rs @@ -11,6 +11,11 @@ macro_rules! graph_topology_conversion_tests { $crate::graph_topology_conversion_tests!(@wrap $T, $U, convert, convert_empty, + convert_vertex_map, + convert_edge_map, + convert_vertex_map_defaults, + convert_edge_map_defaults, + convert_map_empty, ); }; @@ -61,3 +66,89 @@ where |e| iso.edge(e), ); } + +pub fn convert_vertex_map() +where + H::Vertex: Debug, +{ + let (source, vertices, _, _) = H::standard(); + let (_, iso) = G::convert(&source); + let mut source_map = source.vertex_map(None); + for (i, &v) in vertices.iter().enumerate() { + source_map[v] = Some(i); + } + let map = iso.convert_vertex_map(&source_map); + for (i, &v) in vertices.iter().enumerate() { + assert_eq!( + map[iso.vertex(v)], + Some(i), + "unexpected converted value for vertex {v:?}" + ); + } +} + +pub fn convert_edge_map() +where + H::Edge: Debug, +{ + let (source, _, edges, _) = H::standard(); + let (_, iso) = G::convert(&source); + let mut source_map = source.edge_map(None); + for (i, &(e, _, _)) in edges.iter().enumerate() { + source_map[e] = Some(i); + } + let map = iso.convert_edge_map(&source_map); + for (i, &(e, _, _)) in edges.iter().enumerate() { + assert_eq!( + map[iso.edge(e)], + Some(i), + "unexpected converted value for edge {e:?}" + ); + } +} + +pub fn convert_vertex_map_defaults() +where + G::Vertex: Debug, +{ + let (source, vertices, _, _) = H::standard(); + let (graph, iso) = G::convert(&source); + + for &v in &vertices { + let mut source_map = source.vertex_map(8); + source_map[v] = 1; + let map = iso.convert_vertex_map(&source_map); + let mapped = iso.vertex(v); + assert_eq!(map[mapped], 1, "unexpected value for set vertex {mapped:?}"); + for u in graph.vertices().filter(|&x| x != mapped) { + assert_eq!(map[u], 8, "unset vertex {u:?} should hold the default"); + } + } +} + +pub fn convert_edge_map_defaults() +where + G::Edge: Debug, +{ + let (source, _, edges, _) = H::standard(); + let (graph, iso) = G::convert(&source); + + for &(e, _, _) in &edges { + let mut source_map = source.edge_map(8); + source_map[e] = 1; + let map = iso.convert_edge_map(&source_map); + let mapped = iso.edge(e); + assert_eq!(map[mapped], 1, "unexpected value for set edge {mapped:?}"); + for f in graph.edges().filter(|&x| x != mapped) { + assert_eq!(map[f], 8, "unset edge {f:?} should hold the default"); + } + } +} + +pub fn convert_map_empty() { + // Checks that converting maps of a graph without vertices and edges does not panic. + let source = H::empty(); + let (_, iso) = G::convert(&source); + iso.convert_vertex_map(&source.vertex_map(0)); + iso.convert_edge_map(&source.edge_map(0)); +} -- 2.43.0 From 90a2bd9e9e2d0e86d512ce65c151c02330423613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 10:01:54 +0200 Subject: [PATCH 66/67] Add GraphTopologyConversion tests related to addition and deletion --- src/models/append_graph.rs | 2 + src/models/frozen_graph.rs | 4 +- src/models/graph.rs | 2 + src/testing.rs | 2 + ...gy_addition_deletion_conversion_testing.rs | 56 +++++++++++++++++++ ...ph_topology_deletion_conversion_testing.rs | 55 ++++++++++++++++++ 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/testing/graph_topology_addition_deletion_conversion_testing.rs create mode 100644 src/testing/graph_topology_deletion_conversion_testing.rs diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index c653627..a872e6a 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -288,6 +288,8 @@ mod trait_tests { crate::graph_topology_addition_clone_tests!(AppendGraph); crate::graph_topology_conversion_tests!(AppendGraph, Graph); crate::graph_topology_addition_conversion_tests!(AppendGraph, Graph); + crate::graph_topology_deletion_conversion_tests!(AppendGraph, Graph); + crate::graph_topology_addition_deletion_conversion_tests!(AppendGraph, Graph); crate::from_tests!(AppendGraph, Graph); } diff --git a/src/models/frozen_graph.rs b/src/models/frozen_graph.rs index 4c1f8c5..3215a67 100644 --- a/src/models/frozen_graph.rs +++ b/src/models/frozen_graph.rs @@ -251,11 +251,13 @@ impl From<&G> for FrozenGraph { #[cfg(test)] mod trait_tests { use super::FrozenGraph; - use crate::models::AppendGraph; + use crate::models::{AppendGraph, Graph}; crate::graph_topology_tests!(FrozenGraph); crate::graph_topology_clone_tests!(FrozenGraph); crate::graph_topology_conversion_tests!(FrozenGraph, AppendGraph); + crate::graph_topology_deletion_conversion_tests!(FrozenGraph, Graph); + crate::graph_topology_addition_deletion_conversion_tests!(FrozenGraph, Graph); crate::from_tests!(FrozenGraph, AppendGraph); } diff --git a/src/models/graph.rs b/src/models/graph.rs index 628cf3a..ae225ed 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -434,6 +434,8 @@ mod trait_tests { crate::graph_topology_addition_clone_tests!(Graph); crate::graph_topology_conversion_tests!(Graph, AppendGraph); crate::graph_topology_addition_conversion_tests!(Graph, AppendGraph); + crate::graph_topology_deletion_conversion_tests!(Graph, Graph); + crate::graph_topology_addition_deletion_conversion_tests!(Graph, Graph); crate::from_tests!(Graph, AppendGraph); } diff --git a/src/testing.rs b/src/testing.rs index 5d54232..0c39578 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -4,9 +4,11 @@ pub mod fixtures; pub mod from_testing; pub mod graph_topology_addition_clone_testing; pub mod graph_topology_addition_conversion_testing; +pub mod graph_topology_addition_deletion_conversion_testing; pub mod graph_topology_addition_deletion_testing; pub mod graph_topology_addition_testing; pub mod graph_topology_clone_testing; pub mod graph_topology_conversion_testing; +pub mod graph_topology_deletion_conversion_testing; pub mod graph_topology_deletion_testing; pub mod graph_topology_testing; diff --git a/src/testing/graph_topology_addition_deletion_conversion_testing.rs b/src/testing/graph_topology_addition_deletion_conversion_testing.rs new file mode 100644 index 0000000..aa8e2d4 --- /dev/null +++ b/src/testing/graph_topology_addition_deletion_conversion_testing.rs @@ -0,0 +1,56 @@ +use std::fmt::Debug; + +use crate::testing::fixtures; +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion}; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_addition_deletion_conversion_tests { + ($T:ty, $U:ty) => { + $crate::graph_topology_addition_deletion_conversion_tests!(@wrap $T, $U, + convert_after_delete_add_vertex, + ); + }; + + (@wrap $T:ty, $U:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_addition_deletion_conversion_testing::$name::<$T, $U>(); + } + )* + }; +} + +pub fn convert_after_delete_add_vertex() +where + G: GraphTopologyConversion, + H: MakeTestGraph + GraphTopologyDeletion + GraphTopologyAddition, + G::Vertex: Debug, + G::Edge: Debug, + H::Vertex: Debug, + H::Edge: Debug, +{ + let (mut source, vertices, edges, _) = H::standard(); + let del = vertices[2]; + source.delete_vertex(del); + // Depending on the graph type, the new vertex may reuse the slot of the deleted one. + let added = source.add_vertex(); + let mut vertices: Vec<_> = vertices.into_iter().filter(|&v| v != del).collect(); + vertices.push(added); + let edges: Vec<_> = edges + .into_iter() + .filter(|&(_, u, v)| u != del && v != del) + .collect(); + + let (graph, iso) = G::convert(&source); + fixtures::assert_isomorphic( + &source, + &graph, + &vertices, + |v| iso.vertex(v), + &edges, + |e| iso.edge(e), + ); +} diff --git a/src/testing/graph_topology_deletion_conversion_testing.rs b/src/testing/graph_topology_deletion_conversion_testing.rs new file mode 100644 index 0000000..31aea56 --- /dev/null +++ b/src/testing/graph_topology_deletion_conversion_testing.rs @@ -0,0 +1,55 @@ +use std::fmt::Debug; + +use crate::testing::fixtures; +use crate::testing::fixtures::MakeTestGraph; +use crate::traits::{GraphTopologyConversion, GraphTopologyDeletion}; + +#[doc(hidden)] +#[macro_export] +macro_rules! graph_topology_deletion_conversion_tests { + ($T:ty, $U:ty) => { + $crate::graph_topology_deletion_conversion_tests!(@wrap $T, $U, + convert_after_deletion, + ); + }; + + (@wrap $T:ty, $U:ty, $($name:ident),* $(,)?) => { + $( + #[test] + fn $name() { + $crate::testing::graph_topology_deletion_conversion_testing::$name::<$T, $U>(); + } + )* + }; +} + +pub fn convert_after_deletion() +where + G: GraphTopologyConversion, + H: MakeTestGraph + GraphTopologyDeletion, + G::Vertex: Debug, + G::Edge: Debug, + H::Vertex: Debug, + H::Edge: Debug, +{ + let (mut source, vertices, edges, _) = H::standard(); + + // Deletes a vertex together with its incident edges, which leaves gaps in the source's slots. + let del = vertices[2]; + source.delete_vertex(del); + let vertices: Vec<_> = vertices.into_iter().filter(|&v| v != del).collect(); + let edges: Vec<_> = edges + .into_iter() + .filter(|&(_, u, v)| u != del && v != del) + .collect(); + + let (graph, iso) = G::convert(&source); + fixtures::assert_isomorphic( + &source, + &graph, + &vertices, + |v| iso.vertex(v), + &edges, + |e| iso.edge(e), + ); +} -- 2.43.0 From 1d1ca3bd6aa9e97c431861ee5e203b31a33b6e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 15 Oct 2026 10:03:18 +0200 Subject: [PATCH 67/67] Update package version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4129fd2..15834cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grapherity" -version = "0.2.4" +version = "0.3.0" authors = ["Stefan Müller"] edition = "2024" rust-version = "1.85.0" -- 2.43.0