More tests, more adjacency cleanup.

This commit is contained in:
Pat Garrity 2026-06-06 10:37:00 -05:00
parent 44a3bb6a22
commit 57ca573717
Signed by: pfm
GPG key ID: 0DC16BCA24B270C4
4 changed files with 171 additions and 35 deletions

View file

@ -79,7 +79,7 @@ sealed trait Adjacency:
val notContained = vertices.filterNot(contains) val notContained = vertices.filterNot(contains)
if notContained.nonEmpty then if notContained.nonEmpty then
throw GraphException.AdjacencyDoesNotContainVertices( throw GraphException.AdjacencyDoesNotContainVertices(
vertices, notContained,
numberOfVertices numberOfVertices
) )
else else
@ -109,16 +109,6 @@ object Adjacency:
given CanEqual[Adjacency, Adjacency] = CanEqual.derived given CanEqual[Adjacency, Adjacency] = CanEqual.derived
/** Instantiate a new [[Adjacency]] list from a raw representation.
*
* @param adj
* The underlying vector of values.
* @return
* The new [[Adjacency]].
*/
def apply(adj: Vector[Vector[Vertex]]): Adjacency =
DefaultAdjacency.apply(adj)
/** Create an empty adjacency list for some number of vertices. No vertex has /** Create an empty adjacency list for some number of vertices. No vertex has
* any connections to another vertex. * any connections to another vertex.
* *
@ -269,19 +259,6 @@ final class DefaultAdjacency private (
object DefaultAdjacency: object DefaultAdjacency:
/** Instantiate a new [[Adjacency]] list from a raw representation.
*
* @param adj
* The underlying vector of values.
* @return
* The new [[Adjacency]].
*/
def apply(adj: Vector[Vector[Vertex]]): Adjacency =
new DefaultAdjacency(
numberOfVertices = Size.fromVector(adj),
neighbors = adj
)
/** Create an empty adjacency list for some number of vertices. No vertex has /** Create an empty adjacency list for some number of vertices. No vertex has
* any connections to another vertex. * any connections to another vertex.
* *
@ -462,7 +439,10 @@ object MappedAdjacency:
): Adjacency = ): Adjacency =
val acc = scala.collection.mutable.Map.empty[Vertex, ListBuffer[Vertex]] val acc = scala.collection.mutable.Map.empty[Vertex, ListBuffer[Vertex]]
edges.foreach { edge => edges.foreach { edge =>
acc.getOrElseUpdate(edge.from, ListBuffer.empty).addOne(edge.to) val _ = acc.getOrElseUpdate(edge.from, ListBuffer.empty).addOne(edge.to)
// Accounts for vertices with no outgoing edges.
// They should still be present as vertices in the graph.
val _ = acc.getOrElseUpdate(edge.to, ListBuffer.empty)
} }
val neighbors = acc.map { case (v, ns) => v -> ns.toVector }.toMap val neighbors = acc.map { case (v, ns) => v -> ns.toVector }.toMap
new MappedAdjacency(Size.of(neighbors), neighbors) new MappedAdjacency(Size.of(neighbors), neighbors)

View file

@ -1,9 +1,19 @@
package gs.graph.v0 package gs.graph.v0
/** Root for all exceptions in the `gs-graph` library.
*/
sealed abstract class GraphException extends Throwable sealed abstract class GraphException extends Throwable
object GraphException: object GraphException:
/** Thrown when attempting to leverage a vertex with a bounded adjacency, and
* the vertex exceeds the bounds of that graph (adjacency).
*
* @param vertex
* The vertex in question.
* @param bound
* The bound (maximum vertex value) of the graph.
*/
case class VertexExceedsGraphBounds( case class VertexExceedsGraphBounds(
vertex: Vertex, vertex: Vertex,
bound: Size bound: Size
@ -12,6 +22,14 @@ object GraphException:
override def getMessage(): String = override def getMessage(): String =
s"Vertex '$vertex' exceeds graph bound of '$bound' vertices." s"Vertex '$vertex' exceeds graph bound of '$bound' vertices."
/** Thrown when instantiating a data graph, but the wrong amount of data is
* provided.
*
* @param numberOfVertices
* The number of vertices in the graph.
* @param quantityOfData
* The number of data entries provided.
*/
case class DataGraphMismatch( case class DataGraphMismatch(
numberOfVertices: Size, numberOfVertices: Size,
quantityOfData: Int quantityOfData: Int
@ -21,16 +39,14 @@ object GraphException:
s"""Attempted to initialize a data graph with '$numberOfVertices' s"""Attempted to initialize a data graph with '$numberOfVertices'
vertices. '$quantityOfData' data values were provided.""".stripMargin vertices. '$quantityOfData' data values were provided.""".stripMargin
case class RootOutOfBounds( /** Thrown when some collection of vertices are expected within an adjacency,
root: Vertex, * but not found.
numberOfVertices: Size *
) extends GraphException: * @param vertices
* The collection of expected vertices.
override def getMessage(): String = * @param numberOfVertices
s"""Attempted to instantiate a single-root digraph with root '$root', * The size of the underlying adjacency.
but that root exceeds the graph bound of '$numberOfVertices' */
vertices.""".stripMargin
case class AdjacencyDoesNotContainVertices( case class AdjacencyDoesNotContainVertices(
vertices: Iterable[Vertex], vertices: Iterable[Vertex],
numberOfVertices: Size numberOfVertices: Size
@ -40,6 +56,14 @@ object GraphException:
s"""The following vertices were not found within an Adjacency of size s"""The following vertices were not found within an Adjacency of size
'$numberOfVertices': [${vertices.mkString(", ")}]""".stripMargin '$numberOfVertices': [${vertices.mkString(", ")}]""".stripMargin
/** Thrown when some candidate neighbor is defined, but not contained within
* the adjacency as a vertex. This indicates a malformed structure.
*
* @param candidate
* The candidate neighbor vertex.
* @param vertex
* The vertex that uses the neighbor.
*/
case class NeighborNotContainedByAdjacency( case class NeighborNotContainedByAdjacency(
candidate: Vertex, candidate: Vertex,
vertex: Vertex vertex: Vertex

View file

@ -19,6 +19,9 @@ class AdjacencyTests extends munit.FunSuite:
4 -> 6 4 -> 6
) )
private val vertices =
edges.flatMap(edge => List(edge.from, edge.to)).distinct
private val adj = Adjacency.fromEdges(size, edges) private val adj = Adjacency.fromEdges(size, edges)
test("should provide equality") { test("should provide equality") {
@ -102,3 +105,75 @@ class AdjacencyTests extends munit.FunSuite:
assertEquals(a.numberOfVertices, expectedSize) assertEquals(a.numberOfVertices, expectedSize)
assertEquals(a.numberOfEdges, Size.of(es)) assertEquals(a.numberOfEdges, Size.of(es))
} }
test(
"should fail to take a subset of vertices not contained within the source"
) {
val subset = Set(V(0), V(1), V(5))
val es = Edge.list(0 -> 1, 1 -> 2, 2 -> 0)
val a = Adjacency.fromEdges(es)
Try(a.subset(subset)) match
case Failure(ex: GraphException.AdjacencyDoesNotContainVertices) =>
assertEquals(ex.vertices, Set(V(5)))
assertEquals(ex.numberOfVertices, a.numberOfVertices)
case _ => fail("Expected the subset operation to fail.")
}
test("should represent a single arbitrary vertex") {
val v = V(9)
val a = Adjacency.single(v)
assertEquals(a.numberOfVertices, Size.One)
assertEquals(a.numberOfEdges, Size.Zero)
assertEquals(a.contains(v), true)
assertEquals(a.contains(V(0)), false)
}
test("should instantiate an adjacency from mapped edges") {
val es = Edge.list(7 -> 8, 8 -> 9, 9 -> 5)
val vs = es.flatMap(edge => List(edge.v1, edge.v2))
val a = Adjacency.fromEdgesMapped(es)
assertEquals(a.numberOfVertices, Size(4))
assertEquals(a.numberOfEdges, Size.of(es))
es.foreach { edge =>
assertEquals(a.contains(edge.v1), true)
assertEquals(a.contains(edge.v2), true)
}
a.vertices.foreach(vertex => assertEquals(vs.contains(vertex), true))
assertEquals(a.toEdges(), es.toVector)
assertEquals(a.incoming(V(7)), Vector.empty)
assertEquals(a.incoming(V(8)), Vector(V(7)))
assertEquals(a.incoming(V(9)), Vector(V(8)))
assertEquals(a.incoming(V(5)), Vector(V(9)))
}
test(
"should return an empty vector when finding a vertex that is not present"
) {
val es = Edge.list(0 -> 1, 1 -> 2, 2 -> 3)
val a1 = Adjacency.fromEdges(es)
val a2 = Adjacency.fromEdgesMapped(es)
assertEquals(a1.at(V(9)), Vector.empty)
assertEquals(a2.at(V(9)), Vector.empty)
}
test("should instantiate an adjacency from edges (size-based)") {
val a = Adjacency.fromEdges(edges)
a.vertices.foreach(v => assertEquals(vertices.contains(v), true))
assertEquals(a.numberOfVertices, Size.of(vertices))
assertEquals(a.numberOfEdges, Size.of(edges))
}
test(
"should fail to validate a mapped adjacency when a neighbor is not contained in the keyset"
) {
val input = Map(
V(0) -> Vector(V(1), V(2)),
V(1) -> Vector.empty
)
Try(MappedAdjacency.validate(input)) match
case Failure(ex: GraphException.NeighborNotContainedByAdjacency) =>
assertEquals(ex.candidate, V(2))
assertEquals(ex.vertex, V(0))
case _ => fail("Expected adjacency validation to fail.")
}

View file

@ -0,0 +1,57 @@
package gs.graph.v0
import gs.graph.v0.syntax.V
import munit.*
class GraphExceptionTests extends FunSuite:
test("should VertexExceedsGraphBounds") {
val v = V(2)
val b = Size(1)
val ex = GraphException.VertexExceedsGraphBounds(v, b)
assertEquals(ex.vertex, v)
assertEquals(ex.bound, b)
assertEquals(
ex.getMessage(),
s"Vertex '$v' exceeds graph bound of '$b' vertices."
)
}
test("should DataGraphMismatch") {
val n = Size(2)
val q = 1
val ex = GraphException.DataGraphMismatch(n, q)
assertEquals(ex.numberOfVertices, n)
assertEquals(ex.quantityOfData, q)
assertEquals(
ex.getMessage(),
s"""Attempted to initialize a data graph with '$n'
vertices. '$q' data values were provided.""".stripMargin
)
}
test("should AdjacencyDoesNotContainVertices") {
val vs = List(V(8), V(9))
val n = Size(4)
val ex = GraphException.AdjacencyDoesNotContainVertices(vs, n)
assertEquals(ex.vertices, vs)
assertEquals(ex.numberOfVertices, n)
assertEquals(
ex.getMessage(),
s"""The following vertices were not found within an Adjacency of size
'$n': [${vs.mkString(", ")}]""".stripMargin
)
}
test("should NeighborNotContainedByAdjacency") {
val c = V(9)
val v = V(0)
val ex = GraphException.NeighborNotContainedByAdjacency(c, v)
assertEquals(ex.candidate, c)
assertEquals(ex.vertex, v)
assertEquals(
ex.getMessage(),
s"""Attempted to reference neighbor '$c' of vertex '$v', but
'$c' is not a valid vertex in the adjacency.""".stripMargin
)
}