235 lines
6.1 KiB
Go
235 lines
6.1 KiB
Go
package tests
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
|
|
"datastructures/graphs"
|
|
)
|
|
|
|
// DFS marks a vertex visited when it is pushed, so the neighbours of a vertex
|
|
// come off the stack in reverse adjacency order: the last neighbour listed is
|
|
// explored first. The exact-order expectations below follow that rule.
|
|
|
|
func TestDFSUnknownStartReturnsNil(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
|
|
if order := graph.DFS("zz"); order != nil {
|
|
t.Errorf("DFS from unknown vertex = %v, want nil", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSEmptyGraphReturnsNil(t *testing.T) {
|
|
graph := graphs.NewGraph[int]()
|
|
|
|
if order := graph.DFS(1); order != nil {
|
|
t.Errorf("DFS on empty graph = %v, want nil", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSIsolatedVertexVisitsOnlyItself(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddIsolatedVertex("lonely")
|
|
|
|
order := graph.DFS("lonely")
|
|
if !slices.Equal(order, []string{"lonely"}) {
|
|
t.Errorf("DFS = %v, want [lonely]", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSPathGraphVisitsInOrder(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("b", "c")
|
|
graph.AddVertex("c", "d")
|
|
|
|
order := graph.DFS("a")
|
|
if !slices.Equal(order, []string{"a", "b", "c", "d"}) {
|
|
t.Errorf("DFS = %v, want [a b c d]", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSGoesDeepBeforeWide(t *testing.T) {
|
|
// a
|
|
// / \
|
|
// b c
|
|
// | |
|
|
// d e
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("a", "c")
|
|
graph.AddVertex("b", "d")
|
|
graph.AddVertex("c", "e")
|
|
|
|
// c is pushed last, so its branch is exhausted (c, e) before b's
|
|
order := graph.DFS("a")
|
|
if !slices.Equal(order, []string{"a", "c", "e", "b", "d"}) {
|
|
t.Errorf("DFS = %v, want [a c e b d]", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSFromNonRootStart(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("a", "c")
|
|
graph.AddVertex("b", "d")
|
|
|
|
// b's adjacency is [a d], so d is popped first, then a and its branch
|
|
order := graph.DFS("b")
|
|
if !slices.Equal(order, []string{"b", "d", "a", "c"}) {
|
|
t.Errorf("DFS from b = %v, want [b d a c]", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSCycleTerminatesWithoutRepeats(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("b", "c")
|
|
graph.AddVertex("c", "a")
|
|
|
|
order := graph.DFS("a")
|
|
if len(order) != 3 {
|
|
t.Fatalf("DFS on triangle = %v, want 3 vertices", order)
|
|
}
|
|
assertNoDuplicates(t, order)
|
|
}
|
|
|
|
func TestDFSDenseGraphVisitsEachVertexOnce(t *testing.T) {
|
|
// K4: every vertex connected to every other
|
|
graph := graphs.NewGraph[int]()
|
|
for first := 1; first <= 4; first++ {
|
|
for second := first + 1; second <= 4; second++ {
|
|
graph.AddVertex(first, second)
|
|
}
|
|
}
|
|
|
|
order := graph.DFS(1)
|
|
if len(order) != graph.Order() {
|
|
t.Errorf("DFS visited %d vertices, want %d", len(order), graph.Order())
|
|
}
|
|
assertNoDuplicates(t, order)
|
|
}
|
|
|
|
func TestDFSSkipsDisconnectedComponent(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("x", "y")
|
|
|
|
order := graph.DFS("a")
|
|
if !slices.Equal(order, []string{"a", "b"}) {
|
|
t.Errorf("DFS = %v, want only the component of a: [a b]", order)
|
|
}
|
|
}
|
|
|
|
func TestDFSReachesEveryVertexInConnectedGraph(t *testing.T) {
|
|
graph := graphs.NewGraph[int]()
|
|
graph.AddVertex(1, 2)
|
|
graph.AddVertex(2, 3)
|
|
graph.AddVertex(3, 4)
|
|
graph.AddVertex(4, 1)
|
|
graph.AddVertex(2, 5)
|
|
|
|
order := graph.DFS(1)
|
|
if len(order) != graph.Order() {
|
|
t.Errorf("DFS visited %d vertices, want %d", len(order), graph.Order())
|
|
}
|
|
for vertex := range graph.Display() {
|
|
if !slices.Contains(order, vertex) {
|
|
t.Errorf("vertex %d was never visited, order = %v", vertex, order)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDFSOrderIsReachableByBacktracking(t *testing.T) {
|
|
// a graph with cross edges and two cycles, where a naive traversal could
|
|
// emit a vertex that is not reachable from the current path
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("a", "c")
|
|
graph.AddVertex("b", "d")
|
|
graph.AddVertex("c", "d")
|
|
graph.AddVertex("d", "e")
|
|
graph.AddVertex("e", "f")
|
|
graph.AddVertex("f", "a")
|
|
graph.AddVertex("c", "f")
|
|
|
|
order := graph.DFS("a")
|
|
|
|
assertNoDuplicates(t, order)
|
|
if len(order) != graph.Order() {
|
|
t.Fatalf("DFS = %v, want all %d vertices", order, graph.Order())
|
|
}
|
|
assertValidDFSOrder(t, graph.Display(), order)
|
|
}
|
|
|
|
func TestDFSHandlesDeepChainWithoutRecursion(t *testing.T) {
|
|
// deep enough to blow a recursive implementation's stack budget
|
|
const length = 10000
|
|
|
|
graph := graphs.NewGraph[int]()
|
|
for vertex := 0; vertex < length-1; vertex++ {
|
|
graph.AddVertex(vertex, vertex+1)
|
|
}
|
|
|
|
order := graph.DFS(0)
|
|
if len(order) != length {
|
|
t.Fatalf("DFS visited %d vertices, want %d", len(order), length)
|
|
}
|
|
for index, vertex := range order {
|
|
if vertex != index {
|
|
t.Fatalf("DFS[%d] = %d, want %d", index, vertex, index)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDFSDoesNotMutateGraph(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("b", "c")
|
|
|
|
orderBefore, sizeBefore := graph.Order(), graph.Size()
|
|
graph.DFS("a")
|
|
|
|
if graph.Order() != orderBefore || graph.Size() != sizeBefore {
|
|
t.Errorf("after DFS Order/Size = %d/%d, want %d/%d",
|
|
graph.Order(), graph.Size(), orderBefore, sizeBefore)
|
|
}
|
|
}
|
|
|
|
func TestDFSIsRepeatable(t *testing.T) {
|
|
graph := graphs.NewGraph[string]()
|
|
graph.AddVertex("a", "b")
|
|
graph.AddVertex("a", "c")
|
|
graph.AddVertex("b", "d")
|
|
|
|
first := graph.DFS("a")
|
|
second := graph.DFS("a")
|
|
|
|
if !slices.Equal(first, second) {
|
|
t.Errorf("DFS not repeatable: %v then %v", first, second)
|
|
}
|
|
}
|
|
|
|
// assertValidDFSOrder checks that order is a legal depth-first ordering: each
|
|
// vertex must attach to the deepest vertex on the current path that is adjacent
|
|
// to it, which is exactly what backtracking out of a dead end produces.
|
|
func assertValidDFSOrder[T comparable](t *testing.T, adjacency map[T][]T, order []T) {
|
|
t.Helper()
|
|
|
|
if len(order) == 0 {
|
|
return
|
|
}
|
|
|
|
path := []T{order[0]}
|
|
for _, vertex := range order[1:] {
|
|
for len(path) > 0 && !slices.Contains(adjacency[path[len(path)-1]], vertex) {
|
|
path = path[:len(path)-1]
|
|
}
|
|
if len(path) == 0 {
|
|
t.Fatalf("DFS = %v: %v is not adjacent to anything on the path back to the start", order, vertex)
|
|
}
|
|
path = append(path, vertex)
|
|
}
|
|
}
|