mirror of
https://github.com/golang/go
synced 2024-11-18 09:04:49 -07:00
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 <from> <to> | digraph nodes Change-Id: I2eb7c377f5fe1e1e90c5b74eaa78d5211192bb2a Reviewed-on: https://go-review.googlesource.com/c/tools/+/184337 Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
parent
9b2cb0e5f6
commit
fefcef05ab
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user