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