Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init unit testing #10

Merged
merged 7 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ func main() {
}
defer fp.Close()

wg, err := graph.BuildDirectedGraph(fp)
lg, err := graph.ParseInputGraph(fp)
if err != nil {
panic(err)
}

cycles := topo.DirectedCyclesIn(wg.Graph)
cycles := topo.DirectedCyclesIn(lg.Graph)
slog.Debug("directed cycles in", "count", len(cycles), "cycles", cycles)

if len(cycles) > 0 {
slog.Info("skipping topological generations . . .")
} else {
tg, err := graph.TopologicalGenerationsOf(wg.Graph)
tg, err := graph.TopologicalGenerationsOf(lg.Graph)
if err != nil {
panic(err)
}
Expand Down
20 changes: 10 additions & 10 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package main

import (
"fmt"
"testing"
)
// import (
// "fmt"
// "testing"
// )

func TestTopologicalGenerationsSkippedIfCyclesFound(t *testing.T) {
fmt.Println("testTopologicalGenerationsSkippedIfCyclesFound")
}
// func TestTopologicalGenerationsSkippedIfCyclesFound(t *testing.T) {
// fmt.Println("testTopologicalGenerationsSkippedIfCyclesFound")
// }

func TestTopologicalGenerationsSkippedIfNoCyclesFound(t *testing.T) {
fmt.Println("testTopologicalGenerationsSkippedIfNoCyclesFound")
}
// func TestTopologicalGenerationsSkippedIfNoCyclesFound(t *testing.T) {
// fmt.Println("testTopologicalGenerationsSkippedIfNoCyclesFound")
// }
38 changes: 21 additions & 17 deletions pkg/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,60 +71,64 @@ func removeElementsAfterPrefix(slice []string, prefix string) []string {
return slice
}

func BuildDirectedGraph(inputFile io.Reader) (*LabledGraph, error) {
func ParseInputGraph(inputFile io.Reader) (*LabledGraph, error) {
delimiter := ' ' // todo: make this a parameter
commentMarker := "#" // todo: make this a parameter

// todo: the delimiter may not be the same as the comment marker
// todo: no node name may contain the delimiter or commentMarker

reader := csv.NewReader(inputFile)
reader.FieldsPerRecord = -1
reader.Comma = delimiter

wg := newLabledGraph()
lg := newLabledGraph()

lines, err := reader.ReadAll()
if err != nil {
return &wg, fmt.Errorf("error reading adjacency list: %w", err)
return &lg, fmt.Errorf("error reading adjacency list input: %w", err)
}

for _, line := range lines {
// todo: check for empty lines too
if !strings.HasPrefix(line[0], commentMarker) { // is entire line a comment
wg.include(line[0], removeElementsAfterPrefix(line[1:], commentMarker)) // todo: this is inefficient since we loop over toNodes twice, improve
lg.include(line[0], removeElementsAfterPrefix(line[1:], commentMarker)) // todo: this is inefficient since we loop over toNodes twice, improve
}
}
return &wg, nil
return &lg, nil
}

func TopologicalGenerationsOf(dg *multi.DirectedGraph) ([][]int, error) {
indegreeMap := make(map[int]int)
var zeroIndegree []int
inDegreeMap := make(map[int]int)
var zeroInDegree []int
var generations [][]int

nodes := dg.Nodes()
for nodes.Next() {
node := nodes.Node()
inNodes := dg.To(node.ID())
if inNodes.Len() > 0 {
indegreeMap[int(node.ID())] = inNodes.Len()
inDegreeMap[int(node.ID())] = inNodes.Len()
} else {
zeroIndegree = append(zeroIndegree, int(node.ID()))
zeroInDegree = append(zeroInDegree, int(node.ID()))
}
// fmt.Println(strconv.Itoa(int(node.ID())) + " has an indegree of " + strconv.Itoa(inNodes.Len()))
// fmt.Println(strconv.Itoa(int(node.ID())) + " has an in-degree of " + strconv.Itoa(inNodes.Len()))
}

for zeroIndegree != nil {
generations = append(generations, zeroIndegree)
zeroIndegree = nil
for zeroInDegree != nil {
generations = append(generations, zeroInDegree)
zeroInDegree = nil

for _, nodeId := range generations[len(generations)-1] {
outNodes := dg.From(int64(nodeId))
for outNodes.Next() {
node := outNodes.Node()
outNodeId := int(node.ID())
indegreeMap[outNodeId] -= 1
if indegreeMap[int(node.ID())] == 0 {
inDegreeMap[outNodeId] -= 1
if inDegreeMap[int(node.ID())] == 0 {
// fmt.Println("Adding " + strconv.Itoa(outNodeId) + " to zeroIndegree slice")
zeroIndegree = append(zeroIndegree, outNodeId)
delete(indegreeMap, outNodeId)
zeroInDegree = append(zeroInDegree, outNodeId)
delete(inDegreeMap, outNodeId)
}
}
}
Expand Down
166 changes: 143 additions & 23 deletions pkg/graph/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,162 @@ package graph

import (
// "regexp"
"fmt"
"reflect"
"strings"
"testing"
)

func TestTopologicalGenerationsOfCyclicGraph(t *testing.T) {
fmt.Println("TestTopologicalGenerationsOfCyclicGraph")
type testEdge = struct {
from string
to string
expected bool
}

func TestTopologicalGenerationsOfAcyclicGraph(t *testing.T) {
fmt.Println("TestTopologicalGenerationsOfAcyclicGraph")
}
// func TestTopologicalGenerationsOfCyclicGraph(t *testing.T) {
// fmt.Println("TestTopologicalGenerationsOfCyclicGraph")
// }

func TestTopologicalGenerationsOfMultiGraph(t *testing.T) {
fmt.Println("TestTopologicalGenerationsOfMultiGraph")
}
// func TestTopologicalGenerationsOfAcyclicGraph(t *testing.T) {
// fmt.Println("TestTopologicalGenerationsOfAcyclicGraph")
// }

func TestTopologicalGenerationsOfEmptyGraph(t *testing.T) {
fmt.Println("TestTopologicalGenerationsOfEmptyGraph")
}
// func TestTopologicalGenerationsOfMultiGraph(t *testing.T) {
// fmt.Println("TestTopologicalGenerationsOfMultiGraph")
// }

func TestBuildDirectedGraphFileNotFoundError(t *testing.T) {
fmt.Println("TestBuildDirectedGraphFileNotFoundError")
}
// func TestTopologicalGenerationsOfEmptyGraph(t *testing.T) {
// fmt.Println("TestTopologicalGenerationsOfEmptyGraph")
// }

func TestBuildDirectedGraphOfEmptyFile(t *testing.T) {
fmt.Println("TestBuildDirectedGraphFileNotFoundError")
// func TestBuildDirectedGraphFileNotFoundError(t *testing.T) {
// fmt.Println("TestBuildDirectedGraphFileNotFoundError")
// }

func trimWhitespaceFromLines(input string) string { // todo: move to internal/testing.go and unit test
lines := strings.Split(input, "\n")
for i, line := range lines {
lines[i] = strings.TrimLeft(line, " \t") // trim leading spaces and tabs
}
return strings.Join(lines, "\n")
}

func TestBuildDirectedGraphOfFile(t *testing.T) {
fmt.Println("TestBuildDirectedGraphFileNotFoundError")
func areValuesDistinct(m map[string]int64) bool { // todo: move to internal/testing.go and unit test
seen := make(map[int64]struct{})
for _, v := range m {
if _, exists := seen[v]; exists {
return false // If the value has been seen before, not distinct
}
seen[v] = struct{}{}
}
return true // All values are distinct
}

func TestBuildDirectedGraphOfFileWithStrings(t *testing.T) {
fmt.Println("TestBuildDirectedGraphFileNotFoundError")
func TestParseInputGraph(t *testing.T) {
input := trimWhitespaceFromLines(`
a b c
d
`)
lg, _ := ParseInputGraph(strings.NewReader(input))

expectedNodeCount := 4
actualNodeCount := lg.Graph.Nodes().Len()
if actualNodeCount != expectedNodeCount {
t.Errorf(
"ParseInputGraph(%v).Graph.Nodes().Len() = %v, want %v",
input, actualNodeCount, expectedNodeCount,
)
}

actualNumberOfLabels := len(lg.labels)
if actualNumberOfLabels != expectedNodeCount {
t.Errorf(
"len(ParseInputGraph(%v).labels) = %v, want %v",
input, actualNumberOfLabels, expectedNodeCount,
)
}
if !areValuesDistinct(lg.labels) {
t.Errorf("areValuesDistinct(%v) = false, want true", lg.labels)
}

expectedEdgeCount := 2
actualEdgeCount := lg.Graph.Edges().Len()
if expectedEdgeCount != actualEdgeCount {
t.Errorf(
"ParseInputGraph(%v).Graph.Edges().Len() = %v, want %v",
input, actualEdgeCount, expectedEdgeCount,
)
}

testEdges := []testEdge{
{"a", "a", false},
{"a", "b", true},
{"a", "c", true},
{"a", "d", false},
{"b", "a", false},
{"b", "b", false},
{"b", "c", false},
{"b", "d", false},
{"c", "a", false},
{"c", "b", false},
{"c", "c", false},
{"c", "d", false},
{"d", "a", false},
{"d", "b", false},
{"d", "c", false},
{"d", "d", false},
}

// this may seem excessive but caught a bug
expectedTestEdgesCount := expectedNodeCount * expectedNodeCount
actualTestEdgesCount := len(testEdges)
if actualTestEdgesCount != expectedTestEdgesCount {
t.Errorf("Found %v edge test cases, want %v for full coverage",
actualTestEdgesCount, expectedTestEdgesCount,
)
}

for _, testEdge := range testEdges {
actual := lg.Graph.HasEdgeFromTo(lg.labels[testEdge.from], lg.labels[testEdge.to])
if actual != testEdge.expected {
t.Errorf(
"ParseInputGraph(%v).Graph.HasEdgeFromTo(%v, %v) = %v, want %v",
input, testEdge.from, testEdge.to, actual, testEdge.expected,
)
}
}
}

func TestBuildDirectedGraphHandlesEmptyLines(t *testing.T) {
fmt.Println("TestBuildDirectedGraphFileNotFoundError")
// func TestBuildDirectedGraphOfFile(t *testing.T) {
// fmt.Println("TestBuildDirectedGraphFileNotFoundError")
// }

// func TestBuildDirectedGraphOfFileWithStrings(t *testing.T) {
// fmt.Println("TestBuildDirectedGraphFileNotFoundError")
// }

// func TestBuildDirectedGraphHandlesEmptyLines(t *testing.T) {
// fmt.Println("TestBuildDirectedGraphFileNotFoundError")
// }

func TestRemoveElementsAfterPrefix(t *testing.T) {
var tests = []struct {
slice []string
prefix string
expected []string
}{
{[]string{"a", "#b", "c"}, "#", []string{"a"}},
{[]string{}, "#", []string{}}, // empty slice
{[]string{"a"}, "", []string{}}, // empty prefix
{[]string{"a#", "#b", "c"}, "#", []string{"a#"}}, // only checks prefix
}

for _, test := range tests {
actual := removeElementsAfterPrefix(test.slice, test.prefix)
if !reflect.DeepEqual(actual, test.expected) {
t.Errorf(
"TestRemoveElementsAfterPrefix(%v, %v) = %v, want %v",
test.slice, test.prefix, actual, test.expected,
)
}
}
}