| 1 | package runs |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "reflect" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // TestCommandGranted is Review Focus 4's pin among others: a prefix matches |
| 11 | // word for word and exactly, never as a substring, never case-folded, and a |
| 12 | // zero-word prefix matches nothing. |
| 13 | func TestCommandGranted(t *testing.T) { |
| 14 | ic := [][]string{{"import", "classic"}} |
| 15 | for _, tc := range []struct { |
| 16 | name string |
| 17 | argv []string |
| 18 | prefixes [][]string |
| 19 | want bool |
| 20 | }{ |
| 21 | {"prefix then more words", []string{"import", "classic", "--slug", "x"}, ic, true}, |
| 22 | {"exactly the prefix", []string{"import", "classic"}, ic, true}, |
| 23 | {"shorter than the prefix", []string{"import"}, ic, false}, |
| 24 | {"a word that only starts with the prefix word", []string{"importer", "classic"}, ic, false}, |
| 25 | {"a word the prefix word only starts", []string{"imp", "classic"}, ic, false}, |
| 26 | {"case differs", []string{"Import", "classic"}, ic, false}, |
| 27 | {"empty argv", []string{}, ic, false}, |
| 28 | {"nil argv", nil, ic, false}, |
| 29 | {"empty argv under a zero-word prefix", []string{}, [][]string{{}}, false}, |
| 30 | {"second of two prefixes", []string{"import", "sync", "--all"}, [][]string{{"import", "classic"}, {"import", "sync"}}, true}, |
| 31 | {"neither of two prefixes", []string{"export", "sync"}, [][]string{{"import", "classic"}, {"import", "sync"}}, false}, |
| 32 | {"zero-word prefix", []string{"anything"}, [][]string{{}}, false}, |
| 33 | {"nil prefix", []string{"anything"}, [][]string{nil}, false}, |
| 34 | {"no prefixes", []string{"anything"}, nil, false}, |
| 35 | {"zero-word prefix beside a real one does not widen it", []string{"rm", "-rf"}, [][]string{nil, {"import"}}, false}, |
| 36 | } { |
| 37 | if got := CommandGranted(tc.argv, tc.prefixes); got != tc.want { |
| 38 | t.Errorf("%s: CommandGranted(%q, %q) = %v, want %v", tc.name, tc.argv, tc.prefixes, got, tc.want) |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func TestCheckCommandPrefixes(t *testing.T) { |
| 44 | if err := CheckCommandPrefixes(nil); !errors.Is(err, ErrNoCommands) { |
| 45 | t.Errorf("nil: err = %v, want ErrNoCommands", err) |
| 46 | } |
| 47 | if err := CheckCommandPrefixes([][]string{}); !errors.Is(err, ErrNoCommands) { |
| 48 | t.Errorf("empty: err = %v, want ErrNoCommands", err) |
| 49 | } |
| 50 | word128 := strings.Repeat("a", MaxCommandWordBytes) |
| 51 | many := func(n int) [][]string { |
| 52 | out := make([][]string, n) |
| 53 | for i := range out { |
| 54 | out[i] = []string{word128} |
| 55 | } |
| 56 | return out |
| 57 | } |
| 58 | for name, bad := range map[string][][]string{ |
| 59 | "zero-word prefix": {{"import"}, {}}, |
| 60 | "nil prefix": {nil}, |
| 61 | "empty word": {{"import", ""}}, |
| 62 | "129-byte word": {{"import", word128 + "a"}}, |
| 63 | "33 prefixes": many(MaxCommandPrefixes + 1), |
| 64 | "zero-word prefix only": {{}}, |
| 65 | "a word with a space": {{"import classic"}}, |
| 66 | "a word with a comma": {{"import,classic"}}, |
| 67 | "a word with a tab": {{"import\tclassic"}}, |
| 68 | "a word with ESC": {{"import", "\x1b[8mclassic"}}, |
| 69 | "a word with NUL": {{"import", "a\x00"}}, |
| 70 | "invalid UTF-8": {{"import", "\xff"}}, |
| 71 | "U+FFFD": {{"import", "\ufffd"}}, |
| 72 | "a no-break space": {{"import\u00a0classic"}}, |
| 73 | "a zero-width space": {{"import\u200bclassic"}}, |
| 74 | "a newline": {{"import\n"}}, |
| 75 | } { |
| 76 | if err := CheckCommandPrefixes(bad); err == nil { |
| 77 | t.Errorf("%s: accepted; want refused", name) |
| 78 | } |
| 79 | } |
| 80 | if err := CheckCommandPrefixes(many(MaxCommandPrefixes)); err != nil { |
| 81 | t.Errorf("32 prefixes of 128-byte words: %v; want accepted", err) |
| 82 | } |
| 83 | if err := CheckCommandPrefixes([][]string{{"import", "classic"}, {"import", "sync"}}); err != nil { |
| 84 | t.Errorf("two prefixes: %v", err) |
| 85 | } |
| 86 | if err := CheckCommandPrefixes([][]string{{"import", "--slug=café", "über/ü", "a:b;c"}}); err != nil { |
| 87 | t.Errorf("printable non-ASCII and punctuation: %v; want accepted", err) |
| 88 | } |
| 89 | if err := CheckCommandPrefixes([][]string{{"import", "\x1b"}}); !errors.Is(err, ErrCommandWordChars) { |
| 90 | t.Errorf("ESC: err = %v, want ErrCommandWordChars", err) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // TestParseCommandList is the one text form both the CLI flag and the |
| 95 | // console form take: prefixes split on commas, words on spaces, empty |
| 96 | // entries dropped. |
| 97 | func TestParseCommandList(t *testing.T) { |
| 98 | for _, tc := range []struct { |
| 99 | in string |
| 100 | want [][]string |
| 101 | }{ |
| 102 | {"", nil}, |
| 103 | {" , ,", nil}, |
| 104 | {"import classic", [][]string{{"import", "classic"}}}, |
| 105 | {"import classic,import sync", [][]string{{"import", "classic"}, {"import", "sync"}}}, |
| 106 | {" import classic , import sync ,", [][]string{{"import", "classic"}, {"import", "sync"}}}, |
| 107 | } { |
| 108 | if got := ParseCommandList(tc.in); !reflect.DeepEqual(got, tc.want) { |
| 109 | t.Errorf("ParseCommandList(%q) = %q, want %q", tc.in, got, tc.want) |
| 110 | } |
| 111 | } |
| 112 | if got := FormatCommandList([][]string{{"import", "classic"}, {"import", "sync"}}); got != "import classic, import sync" { |
| 113 | t.Errorf("FormatCommandList = %q", got) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | // TestCommandListRoundTrips: every accepted list survives being written out |
| 118 | // as text and read back, so a listing copied into a new mint grants exactly |
| 119 | // what it showed. |
| 120 | func TestCommandListRoundTrips(t *testing.T) { |
| 121 | word128 := strings.Repeat("ü", MaxCommandWordBytes/2) |
| 122 | max := make([][]string, MaxCommandPrefixes) |
| 123 | for i := range max { |
| 124 | max[i] = []string{word128, "x"} |
| 125 | } |
| 126 | for _, p := range [][][]string{ |
| 127 | {{"import"}}, |
| 128 | {{"import", "classic"}, {"import", "sync", "--all"}}, |
| 129 | {{"--slug=café"}, {"a:b;c", "[x]", "\"q\""}}, |
| 130 | max, |
| 131 | } { |
| 132 | if err := CheckCommandPrefixes(p); err != nil { |
| 133 | t.Fatalf("fixture %q is not accepted: %v", p, err) |
| 134 | } |
| 135 | if got := ParseCommandList(FormatCommandList(p)); !reflect.DeepEqual(got, p) { |
| 136 | t.Errorf("round trip of %q gave %q", p, got) |
| 137 | } |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // FuzzCommandListRoundTrip: whatever text is typed, if the list it parses |
| 142 | // to is accepted, formatting and parsing it again gives the same list. |
| 143 | func FuzzCommandListRoundTrip(f *testing.F) { |
| 144 | for _, s := range []string{"import classic,import sync", " a ,, b c ", "a\u00a0b", "x\x1b", "\xff", "café, über"} { |
| 145 | f.Add(s) |
| 146 | } |
| 147 | f.Fuzz(func(t *testing.T, s string) { |
| 148 | p := ParseCommandList(s) |
| 149 | if CheckCommandPrefixes(p) != nil { |
| 150 | return |
| 151 | } |
| 152 | if got := ParseCommandList(FormatCommandList(p)); !reflect.DeepEqual(got, p) { |
| 153 | t.Fatalf("round trip of %q (from %q) gave %q", p, s, got) |
| 154 | } |
| 155 | }) |
| 156 | } |
| 157 | |