From c12a9b5503a72875a22241d34b0056ebe86c2d29 Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Wed, 6 Sep 2023 03:37:13 +0800 Subject: [PATCH 1/7] Add DOT encoding for dependency graph --- internal/graph/dotbuilder.go | 49 ++++++++++++++++++++++++++++++++++++ internal/graph/graph.go | 36 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 internal/graph/dotbuilder.go diff --git a/internal/graph/dotbuilder.go b/internal/graph/dotbuilder.go new file mode 100644 index 0000000..fb2e7ec --- /dev/null +++ b/internal/graph/dotbuilder.go @@ -0,0 +1,49 @@ +package graph + +import ( + "fmt" + "io" +) + +const attr = `fontname="Helvetica,Arial,sans-serif" +node [fontname="Helvetica,Arial,sans-serif"] +edge [fontname="Helvetica,Arial,sans-serif"]` + +// dotBuilder wraps an io.Writer and writes a graph in DOT format +type dotBuilder struct { + io.Writer +} + +// init initializes a graph +func (b *dotBuilder) init() error { + _, err := fmt.Fprintln(b, `digraph G {`) + if err != nil { + return err + } + + _, err = fmt.Fprintln(b, attr) + if err != nil { + return err + } + + return nil +} + +// finish closes the opening curly bracket of the current graph +func (b *dotBuilder) finish() error { + _, err := fmt.Fprintln(b, "}") + return err +} + +// addNode adds a node to the graph +func (b *dotBuilder) addNode(id int, label string) error { + attr := fmt.Sprintf(`label="%s"`, label) + _, err := fmt.Fprintf(b, "n%d [%s]\n", id, attr) + return err +} + +// addEdge adds an edge to the graph +func (b *dotBuilder) addEdge(from, to int) error { + _, err := fmt.Fprintf(b, "n%d -> n%d\n", from, to) + return err +} \ No newline at end of file diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 4d26620..90c27ef 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -2,6 +2,7 @@ package graph import ( "fmt" + "io" "sort" ) @@ -213,3 +214,38 @@ func (g *Graph[V]) TopologicallySortWithPriority(isLowerPriority func(V, V) bool return output, nil } + +func (g *Graph[V]) EncodeDOT(w io.Writer) (err error) { + builder := &dotBuilder{w} + err = builder.init() + if err != nil { + return err + } + defer func() { + err = builder.finish() + }() + + n := 0 + ids := make(map[string]int) + for k := range g.verticesById { + err = builder.addNode(n, k) + if err != nil { + return err + } + ids[k] = n + n++ + } + + for source, adjacentEdgesMap := range g.edges { + for target, isAdjacent := range adjacentEdgesMap { + if isAdjacent { + err = builder.addEdge(ids[source], ids[target]) + if err != nil { + return err + } + } + } + } + + return err +} From 452ee652c40ba78e7a649e2533bfdfbbe3b2167c Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Wed, 6 Sep 2023 16:12:05 +0800 Subject: [PATCH 2/7] Fix string formatting on DOT format labels --- internal/graph/dotbuilder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/graph/dotbuilder.go b/internal/graph/dotbuilder.go index fb2e7ec..34d6c16 100644 --- a/internal/graph/dotbuilder.go +++ b/internal/graph/dotbuilder.go @@ -37,7 +37,7 @@ func (b *dotBuilder) finish() error { // addNode adds a node to the graph func (b *dotBuilder) addNode(id int, label string) error { - attr := fmt.Sprintf(`label="%s"`, label) + attr := fmt.Sprintf(`label=%q`, label) _, err := fmt.Fprintf(b, "n%d [%s]\n", id, attr) return err } From 8a89c0c253e95f095b20b1f6fcc88560daed0f18 Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Wed, 6 Sep 2023 17:31:29 +0800 Subject: [PATCH 3/7] Add test for DOT encoding --- internal/graph/graph.go | 25 ++++++++++++++-------- internal/graph/graph_test.go | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 90c27ef..60ee552 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -215,7 +215,7 @@ func (g *Graph[V]) TopologicallySortWithPriority(isLowerPriority func(V, V) bool return output, nil } -func (g *Graph[V]) EncodeDOT(w io.Writer) (err error) { +func (g *Graph[V]) EncodeDOT(w io.Writer, sortVertices bool) (err error) { builder := &dotBuilder{w} err = builder.init() if err != nil { @@ -225,21 +225,30 @@ func (g *Graph[V]) EncodeDOT(w io.Writer) (err error) { err = builder.finish() }() - n := 0 - ids := make(map[string]int) + vertexIds := make([]string, 0, len(g.verticesById)) for k := range g.verticesById { - err = builder.addNode(n, k) + vertexIds = append(vertexIds, k) + } + if sortVertices { + // Sort the vertices so that the ordering of the output is deterministic, + // mainly for testing purposes. + sort.Strings(vertexIds) + } + + nodeIdsByVertex := make(map[string]int) + for i, vid := range vertexIds { + err = builder.addNode(i, vid) if err != nil { return err } - ids[k] = n - n++ + nodeIdsByVertex[vid] = i } - for source, adjacentEdgesMap := range g.edges { + for _, source := range vertexIds { + adjacentEdgesMap := g.edges[source] for target, isAdjacent := range adjacentEdgesMap { if isAdjacent { - err = builder.addEdge(ids[source], ids[target]) + err = builder.addEdge(nodeIdsByVertex[source], nodeIdsByVertex[target]) if err != nil { return err } diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index de0a010..70de545 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -1,6 +1,7 @@ package graph import ( + "bytes" "fmt" "strconv" "testing" @@ -396,6 +397,45 @@ func TestTopologicallySortWithPriority(t *testing.T) { assert.Error(t, err) } +func TestDOTEncoding(t *testing.T) { + g := NewGraph[vertex]() + adjList := map[string][]string{ + "v_1": {"v_2", "v_3"}, + "v_2": {"v_3"}, + "v_3": {}, + } + expected := `digraph G { +fontname="Helvetica,Arial,sans-serif" +node [fontname="Helvetica,Arial,sans-serif"] +edge [fontname="Helvetica,Arial,sans-serif"] +n0 [label="v_1"] +n1 [label="v_2"] +n2 [label="v_3"] +n0 -> n1 +n0 -> n2 +n1 -> n2 +} +` + // add vertices + for id := range adjList { + g.AddVertex(NewV(id)) + } + + // add edges + for id, neighbors := range adjList { + for _, neighborId := range neighbors { + fmt.Println(id, neighborId) + assert.NoError(t, g.AddEdge(id, neighborId)) + } + } + + // encode to DOT + buf := bytes.Buffer{} + // sort vertices to ensure deterministic ordering of nodes and edges + assert.NoError(t, g.EncodeDOT(&buf, true)) + assert.Equal(t, expected, buf.String()) +} + func getEdgeCount[V Vertex](g *Graph[V], v Vertex) int { edgeCount := 0 for _, hasEdge := range g.edges[v.GetId()] { From 1b62927fe7d13946916a368cf48822209e41f949 Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Wed, 6 Sep 2023 17:59:59 +0800 Subject: [PATCH 4/7] go fmt --- internal/graph/dotbuilder.go | 2 +- internal/graph/graph.go | 2 +- internal/graph/graph_test.go | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/graph/dotbuilder.go b/internal/graph/dotbuilder.go index 34d6c16..bc346dc 100644 --- a/internal/graph/dotbuilder.go +++ b/internal/graph/dotbuilder.go @@ -46,4 +46,4 @@ func (b *dotBuilder) addNode(id int, label string) error { func (b *dotBuilder) addEdge(from, to int) error { _, err := fmt.Fprintf(b, "n%d -> n%d\n", from, to) return err -} \ No newline at end of file +} diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 60ee552..9af3e25 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -224,7 +224,7 @@ func (g *Graph[V]) EncodeDOT(w io.Writer, sortVertices bool) (err error) { defer func() { err = builder.finish() }() - + vertexIds := make([]string, 0, len(g.verticesById)) for k := range g.verticesById { vertexIds = append(vertexIds, k) diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index 70de545..78c0f72 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -424,7 +424,6 @@ n1 -> n2 // add edges for id, neighbors := range adjList { for _, neighborId := range neighbors { - fmt.Println(id, neighborId) assert.NoError(t, g.AddEdge(id, neighborId)) } } From e08f16dfd43a6a8dab6cd6a97b8352b2c056d905 Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Thu, 7 Sep 2023 22:02:16 +0800 Subject: [PATCH 5/7] code review changes --- internal/graph/dotbuilder.go | 49 -------------- internal/graph/encode_dot.go | 103 ++++++++++++++++++++++++++++++ internal/graph/encode_dot_test.go | 74 +++++++++++++++++++++ internal/graph/graph.go | 45 ------------- internal/graph/graph_test.go | 39 ----------- 5 files changed, 177 insertions(+), 133 deletions(-) delete mode 100644 internal/graph/dotbuilder.go create mode 100644 internal/graph/encode_dot.go create mode 100644 internal/graph/encode_dot_test.go diff --git a/internal/graph/dotbuilder.go b/internal/graph/dotbuilder.go deleted file mode 100644 index bc346dc..0000000 --- a/internal/graph/dotbuilder.go +++ /dev/null @@ -1,49 +0,0 @@ -package graph - -import ( - "fmt" - "io" -) - -const attr = `fontname="Helvetica,Arial,sans-serif" -node [fontname="Helvetica,Arial,sans-serif"] -edge [fontname="Helvetica,Arial,sans-serif"]` - -// dotBuilder wraps an io.Writer and writes a graph in DOT format -type dotBuilder struct { - io.Writer -} - -// init initializes a graph -func (b *dotBuilder) init() error { - _, err := fmt.Fprintln(b, `digraph G {`) - if err != nil { - return err - } - - _, err = fmt.Fprintln(b, attr) - if err != nil { - return err - } - - return nil -} - -// finish closes the opening curly bracket of the current graph -func (b *dotBuilder) finish() error { - _, err := fmt.Fprintln(b, "}") - return err -} - -// addNode adds a node to the graph -func (b *dotBuilder) addNode(id int, label string) error { - attr := fmt.Sprintf(`label=%q`, label) - _, err := fmt.Fprintf(b, "n%d [%s]\n", id, attr) - return err -} - -// addEdge adds an edge to the graph -func (b *dotBuilder) addEdge(from, to int) error { - _, err := fmt.Fprintf(b, "n%d -> n%d\n", from, to) - return err -} diff --git a/internal/graph/encode_dot.go b/internal/graph/encode_dot.go new file mode 100644 index 0000000..7930964 --- /dev/null +++ b/internal/graph/encode_dot.go @@ -0,0 +1,103 @@ +package graph + +import ( + "fmt" + "io" + "sort" +) + +// dotBuilder wraps an io.Writer and writes a graph in DOT format +type dotBuilder struct { + io.Writer +} + +func newDotBuilder(w io.Writer) *dotBuilder { + builder := dotBuilder{w} + if err := builder.init(); err != nil { + panic(err) + } + return &builder +} + +// init initializes a graph +func (b *dotBuilder) init() error { + _, err := fmt.Fprintln(b, `digraph G {`) + if err != nil { + return err + } + + _, err = fmt.Fprintln(b, `node [fontname="Helvetica,Arial,sans-serif"]`) + if err != nil { + return err + } + + return nil +} + +// finish closes the opening curly bracket of the current graph +func (b *dotBuilder) finish() error { + _, err := fmt.Fprintln(b, "}") + return err +} + +// addNode adds a node to the graph +func (b *dotBuilder) addNode(id int, label string) error { + attr := fmt.Sprintf(`label=%q`, label) + _, err := fmt.Fprintf(b, "n%d [%s]\n", id, attr) + return err +} + +// addEdge adds an edge to the graph +func (b *dotBuilder) addEdge(from, to int) error { + _, err := fmt.Fprintf(b, "n%d -> n%d\n", from, to) + return err +} + +// EncodeDOT encodes a graph in DOT format to enable visualization of the graph +func EncodeDOT[V Vertex](g *Graph[V], w io.Writer, sortVertices bool) error { + builder := newDotBuilder(w) + + vertexIds := make([]string, 0, len(g.verticesById)) + for id := range g.verticesById { + vertexIds = append(vertexIds, id) + } + if sortVertices { + // Sort the vertices so that the ordering of the output is deterministic, + // mainly for testing purposes. + sort.Strings(vertexIds) + } + + nodeIdsByVertex := make(map[string]int) + for i, id := range vertexIds { + err := builder.addNode(i, id) + if err != nil { + return fmt.Errorf("addNode(%d, %s): %w", i, id, err) + } + nodeIdsByVertex[id] = i + } + + for _, source := range vertexIds { + adjacentEdgesMap := g.edges[source] + + adjacentVerticesIds := make([]string, 0, len(adjacentEdgesMap)) + for targetId := range adjacentEdgesMap { + adjacentVerticesIds = append(adjacentVerticesIds, targetId) + } + if sortVertices { + sort.Strings(adjacentVerticesIds) + } + + for _, target := range adjacentVerticesIds { + if adjacentEdgesMap[target] { + err := builder.addEdge(nodeIdsByVertex[source], nodeIdsByVertex[target]) + if err != nil { + return fmt.Errorf( + "addEdge(%d, %d): %w", nodeIdsByVertex[source], nodeIdsByVertex[target], err, + ) + } + } + } + } + + return builder.finish() +} diff --git a/internal/graph/encode_dot_test.go b/internal/graph/encode_dot_test.go new file mode 100644 index 0000000..7963944 --- /dev/null +++ b/internal/graph/encode_dot_test.go @@ -0,0 +1,74 @@ +package graph + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDOTEncoding(t *testing.T) { + for _, tc := range []struct { + name string + adjList map[string][]string + expected string + }{ + { + name: "empty graph", + adjList: map[string][]string{}, + expected: +`digraph G { +node [fontname="Helvetica,Arial,sans-serif"] +} +`, + }, + { + name: "multiple vertices", + adjList: map[string][]string{ + "v_0": {"v_1", "v_2"}, + "v_1": {"v_2"}, + "v_2": {"v_3"}, + "v_3": {}, + "v_4": {"v_0", "v_3"}, + }, + expected: +`digraph G { +node [fontname="Helvetica,Arial,sans-serif"] +n0 [label="v_0"] +n1 [label="v_1"] +n2 [label="v_2"] +n3 [label="v_3"] +n4 [label="v_4"] +n0 -> n1 +n0 -> n2 +n1 -> n2 +n2 -> n3 +n4 -> n0 +n4 -> n3 +} +`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewGraph[vertex]() + + // add vertices + for id := range tc.adjList { + g.AddVertex(NewV(id)) + } + + // add edges + for id, neighbors := range tc.adjList { + for _, neighborId := range neighbors { + assert.NoError(t, g.AddEdge(id, neighborId)) + } + } + + // encode to DOT + buf := bytes.Buffer{} + // sort vertices to ensure deterministic ordering of nodes and edges + assert.NoError(t, EncodeDOT(g, &buf, true)) + assert.Equal(t, tc.expected, buf.String()) + }) + } +} diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 9af3e25..4d26620 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -2,7 +2,6 @@ package graph import ( "fmt" - "io" "sort" ) @@ -214,47 +213,3 @@ func (g *Graph[V]) TopologicallySortWithPriority(isLowerPriority func(V, V) bool return output, nil } - -func (g *Graph[V]) EncodeDOT(w io.Writer, sortVertices bool) (err error) { - builder := &dotBuilder{w} - err = builder.init() - if err != nil { - return err - } - defer func() { - err = builder.finish() - }() - - vertexIds := make([]string, 0, len(g.verticesById)) - for k := range g.verticesById { - vertexIds = append(vertexIds, k) - } - if sortVertices { - // Sort the vertices so that the ordering of the output is deterministic, - // mainly for testing purposes. - sort.Strings(vertexIds) - } - - nodeIdsByVertex := make(map[string]int) - for i, vid := range vertexIds { - err = builder.addNode(i, vid) - if err != nil { - return err - } - nodeIdsByVertex[vid] = i - } - - for _, source := range vertexIds { - adjacentEdgesMap := g.edges[source] - for target, isAdjacent := range adjacentEdgesMap { - if isAdjacent { - err = builder.addEdge(nodeIdsByVertex[source], nodeIdsByVertex[target]) - if err != nil { - return err - } - } - } - } - - return err -} diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index 78c0f72..de0a010 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -1,7 +1,6 @@ package graph import ( - "bytes" "fmt" "strconv" "testing" @@ -397,44 +396,6 @@ func TestTopologicallySortWithPriority(t *testing.T) { assert.Error(t, err) } -func TestDOTEncoding(t *testing.T) { - g := NewGraph[vertex]() - adjList := map[string][]string{ - "v_1": {"v_2", "v_3"}, - "v_2": {"v_3"}, - "v_3": {}, - } - expected := `digraph G { -fontname="Helvetica,Arial,sans-serif" -node [fontname="Helvetica,Arial,sans-serif"] -edge [fontname="Helvetica,Arial,sans-serif"] -n0 [label="v_1"] -n1 [label="v_2"] -n2 [label="v_3"] -n0 -> n1 -n0 -> n2 -n1 -> n2 -} -` - // add vertices - for id := range adjList { - g.AddVertex(NewV(id)) - } - - // add edges - for id, neighbors := range adjList { - for _, neighborId := range neighbors { - assert.NoError(t, g.AddEdge(id, neighborId)) - } - } - - // encode to DOT - buf := bytes.Buffer{} - // sort vertices to ensure deterministic ordering of nodes and edges - assert.NoError(t, g.EncodeDOT(&buf, true)) - assert.Equal(t, expected, buf.String()) -} - func getEdgeCount[V Vertex](g *Graph[V], v Vertex) int { edgeCount := 0 for _, hasEdge := range g.edges[v.GetId()] { From 1aa2b522de15d8dad77a3fc8e118703bc6acd5a2 Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Thu, 7 Sep 2023 22:06:22 +0800 Subject: [PATCH 6/7] small naming change --- internal/graph/encode_dot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/graph/encode_dot.go b/internal/graph/encode_dot.go index 7930964..1c8f530 100644 --- a/internal/graph/encode_dot.go +++ b/internal/graph/encode_dot.go @@ -80,8 +80,8 @@ func EncodeDOT[V Vertex](g *Graph[V], w io.Writer, sortVertices bool) error { adjacentEdgesMap := g.edges[source] adjacentVerticesIds := make([]string, 0, len(adjacentEdgesMap)) - for targetId := range adjacentEdgesMap { - adjacentVerticesIds = append(adjacentVerticesIds, targetId) + for tid := range adjacentEdgesMap { + adjacentVerticesIds = append(adjacentVerticesIds, tid) } if sortVertices { sort.Strings(adjacentVerticesIds) From 93b181f03021de57826cefc867c5bed0a0cffc19 Mon Sep 17 00:00:00 2001 From: Kevin Mingtarja Date: Fri, 8 Sep 2023 03:18:02 +0800 Subject: [PATCH 7/7] lint fix --- internal/graph/encode_dot_test.go | 38 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/internal/graph/encode_dot_test.go b/internal/graph/encode_dot_test.go index 7963944..83282d3 100644 --- a/internal/graph/encode_dot_test.go +++ b/internal/graph/encode_dot_test.go @@ -16,8 +16,7 @@ func TestDOTEncoding(t *testing.T) { { name: "empty graph", adjList: map[string][]string{}, - expected: -`digraph G { + expected: `digraph G { node [fontname="Helvetica,Arial,sans-serif"] } `, @@ -31,8 +30,7 @@ node [fontname="Helvetica,Arial,sans-serif"] "v_3": {}, "v_4": {"v_0", "v_3"}, }, - expected: -`digraph G { + expected: `digraph G { node [fontname="Helvetica,Arial,sans-serif"] n0 [label="v_0"] n1 [label="v_1"] @@ -50,25 +48,25 @@ n4 -> n3 }, } { t.Run(tc.name, func(t *testing.T) { - g := NewGraph[vertex]() - - // add vertices - for id := range tc.adjList { - g.AddVertex(NewV(id)) - } + g := NewGraph[vertex]() - // add edges - for id, neighbors := range tc.adjList { - for _, neighborId := range neighbors { - assert.NoError(t, g.AddEdge(id, neighborId)) + // add vertices + for id := range tc.adjList { + g.AddVertex(NewV(id)) } - } - // encode to DOT - buf := bytes.Buffer{} - // sort vertices to ensure deterministic ordering of nodes and edges - assert.NoError(t, EncodeDOT(g, &buf, true)) - assert.Equal(t, tc.expected, buf.String()) + // add edges + for id, neighbors := range tc.adjList { + for _, neighborId := range neighbors { + assert.NoError(t, g.AddEdge(id, neighborId)) + } + } + + // encode to DOT + buf := bytes.Buffer{} + // sort vertices to ensure deterministic ordering of nodes and edges + assert.NoError(t, EncodeDOT(g, &buf, true)) + assert.Equal(t, tc.expected, buf.String()) }) } }