From fefcef05abb16e5cc4907457d0008e26c57246ca Mon Sep 17 00:00:00 2001 From: Jean de Klerk Date: Fri, 12 Jul 2019 10:22:56 -0600 Subject: [PATCH] digraph: refactor allpaths to print an adjacency list Refactors allpaths to print an adjacency list from "from" to "to", instead of just the nodes. This allows other tools like modgraphviz to print the subgraph. For example, the following command: cd $GOPATH/src/cloud.google.com/go && \ go mod graph | \ digraph allpaths cloud.google.com/go golang.org/x/text@v0.3.2 | \ modgraphviz | \ dot -Tpng -o graph.png Generates the following graph.png: https://user-images.githubusercontent.com/3584893/60481727-df0a8680-9c4b-11e9-8df9-c581d599edd1.png Also splits out the allpaths tests into their own test, and adds many test cases. This is a breaking change. The previous behavior returned vertices; now it returns edges. If you relied on the previous behavior, use: my-application | digraph allpaths | digraph nodes Change-Id: I2eb7c377f5fe1e1e90c5b74eaa78d5211192bb2a Reviewed-on: https://go-review.googlesource.com/c/tools/+/184337 Reviewed-by: Alan Donovan --- cmd/digraph/digraph.go | 70 ++++++++++------- cmd/digraph/digraph_test.go | 148 ++++++++++++++++++++++++++++++------ 2 files changed, 168 insertions(+), 50 deletions(-) diff --git a/cmd/digraph/digraph.go b/cmd/digraph/digraph.go index e3f8fac5..6393f44a 100644 --- a/cmd/digraph/digraph.go +++ b/cmd/digraph/digraph.go @@ -262,6 +262,47 @@ func (g graph) sccs() []nodeset { return sccs } +func (g graph) allpaths(from, to string) error { + // Mark all nodes to "to". + seen := make(nodeset) // value of seen[x] indicates whether x is on some path to "to" + var visit func(node string) bool + visit = func(node string) bool { + reachesTo, ok := seen[node] + if !ok { + reachesTo = node == to + seen[node] = reachesTo + for e := range g[node] { + if visit(e) { + reachesTo = true + } + } + if reachesTo && node != to { + seen[node] = true + } + } + return reachesTo + } + visit(from) + + // For each marked node, collect its marked successors. + var edges []string + for n := range seen { + for succ := range g[n] { + if seen[succ] { + edges = append(edges, n+" "+succ) + } + } + } + + // Sort (so that this method is deterministic) and print edges. + sort.Strings(edges) + for _, e := range edges { + fmt.Fprintln(stdout, e) + } + + return nil +} + func parse(rd io.Reader) (graph, error) { g := make(graph) @@ -284,6 +325,7 @@ func parse(rd io.Reader) (graph, error) { return g, nil } +// Overridable for testing purposes. var stdin io.Reader = os.Stdin var stdout io.Writer = os.Stdout @@ -398,33 +440,7 @@ func digraph(cmd string, args []string) error { if g[to] == nil { return fmt.Errorf("no such 'to' node %q", to) } - - seen := make(nodeset) // value of seen[x] indicates whether x is on some path to 'to' - var visit func(label string) bool - visit = func(label string) bool { - reachesTo, ok := seen[label] - if !ok { - reachesTo = label == to - - seen[label] = reachesTo - for e := range g[label] { - if visit(e) { - reachesTo = true - } - } - seen[label] = reachesTo - } - return reachesTo - } - if !visit(from) { - return fmt.Errorf("no path from %q to %q", from, to) - } - for label, reachesTo := range seen { - if !reachesTo { - delete(seen, label) - } - } - seen.sort().println("\n") + g.allpaths(from, to) case "sccs": if len(args) != 0 { diff --git a/cmd/digraph/digraph_test.go b/cmd/digraph/digraph_test.go index 835290d5..3376065f 100644 --- a/cmd/digraph/digraph_test.go +++ b/cmd/digraph/digraph_test.go @@ -1,7 +1,6 @@ // Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. - package main import ( @@ -30,35 +29,34 @@ d c ` for _, test := range []struct { + name string input string cmd string args []string want string }{ - {g1, "nodes", nil, "belt\nhat\njacket\npants\nshirt\nshoes\nshorts\nsocks\nsweater\ntie\n"}, - {g1, "reverse", []string{"jacket"}, "jacket\nshirt\nsweater\n"}, - {g1, "forward", []string{"socks"}, "shoes\nsocks\n"}, - {g1, "forward", []string{"socks", "sweater"}, "jacket\nshoes\nsocks\nsweater\n"}, - - {g2, "allpaths", []string{"a", "d"}, "a\nb\nc\nd\n"}, - - {g2, "sccs", nil, "a\nb\nc d\n"}, - {g2, "scc", []string{"d"}, "c\nd\n"}, - {g2, "succs", []string{"a"}, "b\nc\n"}, - {g2, "preds", []string{"c"}, "a\nd\n"}, - {g2, "preds", []string{"c", "d"}, "a\nb\nc\nd\n"}, + {"nodes", g1, "nodes", nil, "belt\nhat\njacket\npants\nshirt\nshoes\nshorts\nsocks\nsweater\ntie\n"}, + {"reverse", g1, "reverse", []string{"jacket"}, "jacket\nshirt\nsweater\n"}, + {"forward", g1, "forward", []string{"socks"}, "shoes\nsocks\n"}, + {"forward multiple args", g1, "forward", []string{"socks", "sweater"}, "jacket\nshoes\nsocks\nsweater\n"}, + {"scss", g2, "sccs", nil, "a\nb\nc d\n"}, + {"scc", g2, "scc", []string{"d"}, "c\nd\n"}, + {"succs", g2, "succs", []string{"a"}, "b\nc\n"}, + {"preds", g2, "preds", []string{"c"}, "a\nd\n"}, + {"preds multiple args", g2, "preds", []string{"c", "d"}, "a\nb\nc\nd\n"}, } { - stdin = strings.NewReader(test.input) - stdout = new(bytes.Buffer) - if err := digraph(test.cmd, test.args); err != nil { - t.Error(err) - continue - } + t.Run(test.name, func(t *testing.T) { + stdin = strings.NewReader(test.input) + stdout = new(bytes.Buffer) + if err := digraph(test.cmd, test.args); err != nil { + t.Fatal(err) + } - got := stdout.(fmt.Stringer).String() - if got != test.want { - t.Errorf("digraph(%s, %s) = %q, want %q", test.cmd, test.args, got, test.want) - } + got := stdout.(fmt.Stringer).String() + if got != test.want { + t.Errorf("digraph(%s, %s) = got %q, want %q", test.cmd, test.args, got, test.want) + } + }) } // TODO(adonovan): @@ -66,6 +64,110 @@ d c // - test errors } +func TestAllpaths(t *testing.T) { + for _, test := range []struct { + name string + in string + to string // from is always "A" + want string + }{ + { + name: "Basic", + in: "A B\nB C", + to: "B", + want: "A B\n", + }, + { + name: "Long", + in: "A B\nB C\n", + to: "C", + want: "A B\nB C\n", + }, + { + name: "Cycle Basic", + in: "A B\nB A", + to: "B", + want: "A B\nB A\n", + }, + { + name: "Cycle Path Out", + // A <-> B -> C -> D + in: "A B\nB A\nB C\nC D", + to: "C", + want: "A B\nB A\nB C\n", + }, + { + name: "Cycle Path Out Further Out", + // A -> B <-> C -> D -> E + in: "A B\nB C\nC D\nC B\nD E", + to: "D", + want: "A B\nB C\nC B\nC D\n", + }, + { + name: "Two Paths Basic", + // /-> C --\ + // A -> B -- -> E -> F + // \-> D --/ + in: "A B\nB C\nC E\nB D\nD E\nE F", + to: "E", + want: "A B\nB C\nB D\nC E\nD E\n", + }, + { + name: "Two Paths With One Immediately From Start", + // /-> B -+ -> D + // A -- | + // \-> C <+ + in: "A B\nA C\nB C\nB D", + to: "C", + want: "A B\nA C\nB C\n", + }, + { + name: "Two Paths Further Up", + // /-> B --\ + // A -- -> D -> E -> F + // \-> C --/ + in: "A B\nA C\nB D\nC D\nD E\nE F", + to: "E", + want: "A B\nA C\nB D\nC D\nD E\n", + }, + { + // We should include A - C - D even though it's further up the + // second path than D (which would already be in the graph by + // the time we get around to integrating the second path). + name: "Two Splits", + // /-> B --\ /-> E --\ + // A -- -> D -- -> G -> H + // \-> C --/ \-> F --/ + in: "A B\nA C\nB D\nC D\nD E\nD F\nE G\nF G\nG H", + to: "G", + want: "A B\nA C\nB D\nC D\nD E\nD F\nE G\nF G\n", + }, + { + // D - E should not be duplicated. + name: "Two Paths - Two Splits With Gap", + // /-> B --\ /-> F --\ + // A -- -> D -> E -- -> H -> I + // \-> C --/ \-> G --/ + in: "A B\nA C\nB D\nC D\nD E\nE F\nE G\nF H\nG H\nH I", + to: "H", + want: "A B\nA C\nB D\nC D\nD E\nE F\nE G\nF H\nG H\n", + }, + } { + t.Run(test.name, func(t *testing.T) { + stdin = strings.NewReader(test.in) + stdout = new(bytes.Buffer) + if err := digraph("allpaths", []string{"A", test.to}); err != nil { + t.Fatal(err) + } + + got := stdout.(fmt.Stringer).String() + if got != test.want { + t.Errorf("digraph(allpaths, A, %s) = got %q, want %q", test.to, got, test.want) + } + }) + } +} + func TestSplit(t *testing.T) { for _, test := range []struct { line string