| // 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 ( | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "reflect" | |
| "sort" | |
| "strings" | |
| "testing" | |
| ) | |
| func TestDigraph(t *testing.T) { | |
| const g1 = ` | |
| socks shoes | |
| shorts pants | |
| pants belt shoes | |
| shirt tie sweater | |
| sweater jacket | |
| hat | |
| ` | |
| const g2 = ` | |
| a b c | |
| b d | |
| c d | |
| d c | |
| e e | |
| ` | |
| for _, test := range []struct { | |
| name string | |
| input string | |
| cmd string | |
| args []string | |
| want string | |
| }{ | |
| {"nodes", g1, "nodes", nil, "belt\nhat\njacket\npants\nshirt\nshoes\nshorts\nsocks\nsweater\ntie\n"}, | |
| {"reverse", g1, "reverse", []string{"jacket"}, "jacket\nshirt\nsweater\n"}, | |
| {"transpose", g1, "transpose", nil, "belt pants\njacket sweater\npants shorts\nshoes pants\nshoes socks\nsweater shirt\ntie shirt\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, "c d\ne\n"}, | |
| {"scc", g2, "scc", []string{"d"}, "c\nd\n"}, | |
| {"succs", g2, "succs", []string{"a"}, "b\nc\n"}, | |
| {"succs-long-token", g2 + "x " + strings.Repeat("x", 96*1024), "succs", []string{"x"}, strings.Repeat("x", 96*1024) + "\n"}, | |
| {"preds", g2, "preds", []string{"c"}, "a\nd\n"}, | |
| {"preds multiple args", g2, "preds", []string{"c", "d"}, "a\nb\nc\nd\n"}, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| stdin = strings.NewReader(test.input) | |
| stdout = new(bytes.Buffer) | |
| if err := doDigraph(test.cmd, test.args); err != nil { | |
| t.Fatal(err) | |
| } | |
| 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): | |
| // - 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", | |
| }, | |
| { | |
| // C <--> B --> A --> D <--> E | |
| // ⋃ | |
| name: "non-regression test for #74842", | |
| in: "A D\nB A\nB B\nB C\nC B\nD E\nE D", | |
| to: "D", | |
| want: "A D\nD E\nE D\n", | |
| }, | |
| { | |
| // A --> B --> D | |
| // ^ | |
| // v | |
| // C[123] | |
| name: "regression test for #74842", | |
| in: "A B\nB C1\nB C2\nB C3\nB D\nC1 B\nC2 B\nC3 B\n", | |
| to: "D", | |
| want: "A B\nB C1\nB C2\nB C3\nB D\nC1 B\nC2 B\nC3 B\n", | |
| }, | |
| { | |
| // A -------> B --> D | |
| // \--> C ---^ | | |
| // ^----------+ | |
| name: "another regression test for #74842", | |
| in: "A B\nA C\nB D\nC B\nD C\n", | |
| to: "D", | |
| want: "A B\nA C\nB D\nC B\nD C\n", | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| stdin = strings.NewReader(test.in) | |
| stdout = new(bytes.Buffer) | |
| if err := doDigraph("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 TestSomepath(t *testing.T) { | |
| for _, test := range []struct { | |
| name string | |
| in string | |
| to string | |
| // somepath is non-deterministic, so we have to provide all the | |
| // possible options. Each option is separated with |. | |
| wantAnyOf string | |
| }{ | |
| { | |
| name: "Basic", | |
| in: "A B\n", | |
| to: "B", | |
| wantAnyOf: "A B", | |
| }, | |
| { | |
| name: "Basic With Cycle", | |
| in: "A B\nB A", | |
| to: "B", | |
| wantAnyOf: "A B", | |
| }, | |
| { | |
| name: "Two Paths", | |
| // /-> B --\ | |
| // A -- -> D | |
| // \-> C --/ | |
| in: "A B\nA C\nB D\nC D", | |
| to: "D", | |
| wantAnyOf: "A B\nB D|A C\nC D", | |
| }, | |
| { | |
| name: "Printed path is minimal", | |
| // A -> B1->B2->B3 -> E | |
| // A -> C1->C2 -> E | |
| // A -> D -> E | |
| in: "A D C1 B1\nD E\nC1 C2\nC2 E\nB1 B2\nB2 B3\nB3 E", | |
| to: "E", | |
| wantAnyOf: "A D\nD E", | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| stdin = strings.NewReader(test.in) | |
| stdout = new(bytes.Buffer) | |
| if err := doDigraph("somepath", []string{"A", test.to}); err != nil { | |
| t.Fatal(err) | |
| } | |
| got := stdout.(fmt.Stringer).String() | |
| lines := strings.Split(got, "\n") | |
| sort.Strings(lines) | |
| got = strings.Join(lines[1:], "\n") | |
| var oneMatch bool | |
| for want := range strings.SplitSeq(test.wantAnyOf, "|") { | |
| if got == want { | |
| oneMatch = true | |
| } | |
| } | |
| if !oneMatch { | |
| t.Errorf("digraph(somepath, A, %s) = got %q, want any of\n%s", test.to, got, test.wantAnyOf) | |
| } | |
| }) | |
| } | |
| } | |
| func TestSplit(t *testing.T) { | |
| for _, test := range []struct { | |
| line string | |
| want []string | |
| }{ | |
| {`one "2a 2b" three`, []string{"one", "2a 2b", "three"}}, | |
| {`one tw"\n\x0a\u000a\012"o three`, []string{"one", "tw\n\n\n\no", "three"}}, | |
| } { | |
| got, err := split(test.line) | |
| if err != nil { | |
| t.Errorf("split(%s) failed: %v", test.line, err) | |
| } | |
| if !reflect.DeepEqual(got, test.want) { | |
| t.Errorf("split(%s) = %v, want %v", test.line, got, test.want) | |
| } | |
| } | |
| } | |
| func TestQuotedLength(t *testing.T) { | |
| for _, test := range []struct { | |
| input string | |
| want int | |
| }{ | |
| {`"abc"`, 5}, | |
| {`"abc"def`, 5}, | |
| {`"abc\"d"ef`, 8}, // "abc\"d" is consumed, ef is residue | |
| {`"\012\n\x0a\u000a\U0000000a"`, 28}, | |
| {"\"\xff\"", 3}, // bad UTF-8 is ok | |
| {`"\xff"`, 6}, // hex escape for bad UTF-8 is ok | |
| } { | |
| got, ok := quotedLength(test.input) | |
| if !ok { | |
| got = 0 | |
| } | |
| if got != test.want { | |
| t.Errorf("quotedLength(%s) = %d, want %d", test.input, got, test.want) | |
| } | |
| } | |
| // errors | |
| for _, input := range []string{ | |
| ``, // not a quotation | |
| `a`, // not a quotation | |
| `'a'`, // not a quotation | |
| `"a`, // not terminated | |
| `"\0"`, // short octal escape | |
| `"\x1"`, // short hex escape | |
| `"\u000"`, // short \u escape | |
| `"\U0000000"`, // short \U escape | |
| `"\k"`, // invalid escape | |
| "\"ab\nc\"", // newline | |
| } { | |
| if n, ok := quotedLength(input); ok { | |
| t.Errorf("quotedLength(%s) = %d, want !ok", input, n) | |
| } | |
| } | |
| } | |
| func TestFocus(t *testing.T) { | |
| for _, test := range []struct { | |
| name string | |
| in string | |
| focus string | |
| want string | |
| }{ | |
| { | |
| name: "Basic", | |
| in: "A B", | |
| focus: "B", | |
| want: "A B\n", | |
| }, | |
| { | |
| name: "Some Nodes Not Included", | |
| // C does not have a path involving B, and should not be included | |
| // in the output. | |
| in: "A B\nA C", | |
| focus: "B", | |
| want: "A B\n", | |
| }, | |
| { | |
| name: "Cycle In Path", | |
| // A <-> B -> C | |
| in: "A B\nB A\nB C", | |
| focus: "C", | |
| want: "A B\nB A\nB C\n", | |
| }, | |
| { | |
| name: "Cycle Out Of Path", | |
| // C <- A <->B | |
| in: "A B\nB A\nB C", | |
| focus: "C", | |
| want: "A B\nB A\nB C\n", | |
| }, | |
| { | |
| name: "Complex", | |
| // Paths in and out from focus. | |
| // /-> F | |
| // /-> B -> D -- | |
| // A -- \-> E | |
| // \-> C | |
| in: "A B\nA C\nB D\nD F\nD E", | |
| focus: "D", | |
| want: "A B\nB D\nD E\nD F\n", | |
| }, | |
| } { | |
| t.Run(test.name, func(t *testing.T) { | |
| stdin = strings.NewReader(test.in) | |
| stdout = new(bytes.Buffer) | |
| if err := doDigraph("focus", []string{test.focus}); err != nil { | |
| t.Fatal(err) | |
| } | |
| got := stdout.(fmt.Stringer).String() | |
| if got != test.want { | |
| t.Errorf("digraph(focus, %s) = got %q, want %q", test.focus, got, test.want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestToDot(t *testing.T) { | |
| in := `a b c | |
| b "d\"\\d" | |
| c "d\"\\d"` | |
| want := `digraph "" { | |
| n0 [label="a"]; | |
| n1 [label="b"]; | |
| n2 [label="c"]; | |
| n3 [label="d\"\\d"]; | |
| n0 -> n1; | |
| n0 -> n2; | |
| n1 -> n3; | |
| n2 -> n3; | |
| } | |
| ` | |
| defer func(in io.Reader, out io.Writer) { stdin, stdout = in, out }(stdin, stdout) | |
| stdin = strings.NewReader(in) | |
| stdout = new(bytes.Buffer) | |
| if err := doDigraph("to", []string{"dot"}); err != nil { | |
| t.Fatal(err) | |
| } | |
| got := stdout.(fmt.Stringer).String() | |
| if got != want { | |
| t.Errorf("digraph(to, dot) = got\n %q\nwant\n %q", got, want) | |
| } | |
| } |