Commit 72eaa3a587
Verified · cmc
Layout: unified · split
internal/control/plain_test.go added +38
| @@ -0,0 +1,38 @@ | |||
| 1 | package control | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "flag" | ||
| 5 | "os" | ||
| 6 | "path/filepath" | ||
| 7 | "regexp" | ||
| 8 | "testing" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var updatePlain = flag.Bool("update-plain", false, "rewrite testdata/plain from the current output") | ||
| 12 | |||
| 13 | var plainStamp = regexp.MustCompile(`\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z| UTC)?`) | ||
| 14 | |||
| 15 | // pinPlain compares a command's piped output with the copy taken before | ||
| 16 | // its terminal screen was rewritten. Timestamps are masked, since | ||
| 17 | // fixtures are created at test time. | ||
| 18 | func pinPlain(t *testing.T, name, got string) { | ||
| 19 | t.Helper() | ||
| 20 | got = plainStamp.ReplaceAllString(got, "<time>") | ||
| 21 | path := filepath.Join("testdata", "plain", name+".txt") | ||
| 22 | if *updatePlain { | ||
| 23 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | ||
| 24 | t.Fatal(err) | ||
| 25 | } | ||
| 26 | if err := os.WriteFile(path, []byte(got), 0o644); err != nil { | ||
| 27 | t.Fatal(err) | ||
| 28 | } | ||
| 29 | return | ||
| 30 | } | ||
| 31 | want, err := os.ReadFile(path) | ||
| 32 | if err != nil { | ||
| 33 | t.Fatalf("%v: capture it with -update-plain before changing the command", err) | ||
| 34 | } | ||
| 35 | if got != string(want) { | ||
| 36 | t.Errorf("piped output changed:\n--- want\n%s--- got\n%s", want, got) | ||
| 37 | } | ||
| 38 | } | ||
internal/control/screen_test.go +91
| @@ -206,3 +206,94 @@ func TestEmitViewRoutes(t *testing.T) { | |||
| 206 | t.Errorf("terminal: %q built=%v", out.String(), built) | 206 | t.Errorf("terminal: %q built=%v", out.String(), built) |
| 207 | } | 207 | } |
| 208 | } | 208 | } |
| 209 | |||
| 210 | // errorer is the part of *testing.T checkActions uses, so its own test | ||
| 211 | // can pass a recorder. | ||
| 212 | type errorer interface { | ||
| 213 | Helper() | ||
| 214 | Errorf(format string, args ...any) | ||
| 215 | } | ||
| 216 | |||
| 217 | type recorder struct{ failed bool } | ||
| 218 | |||
| 219 | func (r *recorder) Helper() {} | ||
| 220 | func (r *recorder) Errorf(string, ...any) { r.failed = true } | ||
| 221 | |||
| 222 | // cliLocal are commands cmd/gitbay runs itself; the registry does not | ||
| 223 | // know them, but a legend may suggest them. | ||
| 224 | var cliLocal = map[string]bool{"mr rebase": true, "mr checkout": true, "repo clone": true} | ||
| 225 | |||
| 226 | // checkActions fails t for any legend or "more" command that the | ||
| 227 | // registry would not dispatch, or whose flags it does not declare. | ||
| 228 | func checkActions(t errorer, s screen) { | ||
| 229 | t.Helper() | ||
| 230 | var all [][]string | ||
| 231 | for _, a := range s.actions { | ||
| 232 | all = append(all, a.argv) | ||
| 233 | } | ||
| 234 | for _, sec := range s.sections { | ||
| 235 | if len(sec.more) > 0 { | ||
| 236 | all = append(all, sec.more) | ||
| 237 | } | ||
| 238 | } | ||
| 239 | for _, argv := range all { | ||
| 240 | if len(argv) >= 2 && cliLocal[argv[0]+" "+argv[1]] { | ||
| 241 | continue | ||
| 242 | } | ||
| 243 | cmd, rest, ok := Lookup(argv) | ||
| 244 | if !ok { | ||
| 245 | t.Errorf("no command for %q", argv) | ||
| 246 | continue | ||
| 247 | } | ||
| 248 | if err := checkFlags(cmd, rest); err != nil { | ||
| 249 | t.Errorf("%q: %v", argv, err) | ||
| 250 | } | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | // checkFlags refuses a flag the command does not declare, and a value | ||
| 255 | // flag with nothing after it. Words that are not flags are positionals. | ||
| 256 | func checkFlags(cmd Command, rest []string) error { | ||
| 257 | takes := map[string]bool{} | ||
| 258 | for _, f := range cmd.Flags { | ||
| 259 | takes[f.Name] = f.Arg != "" | ||
| 260 | } | ||
| 261 | for i := 0; i < len(rest); i++ { | ||
| 262 | a := rest[i] | ||
| 263 | if !strings.HasPrefix(a, "--") { | ||
| 264 | continue | ||
| 265 | } | ||
| 266 | name, _, inline := strings.Cut(a, "=") | ||
| 267 | value, ok := takes[name] | ||
| 268 | if !ok { | ||
| 269 | return fmt.Errorf("%s does not take %s", joinPath(cmd.Path), name) | ||
| 270 | } | ||
| 271 | if value && !inline { | ||
| 272 | if i+1 >= len(rest) { | ||
| 273 | return fmt.Errorf("%s needs a value", name) | ||
| 274 | } | ||
| 275 | i++ | ||
| 276 | } | ||
| 277 | } | ||
| 278 | return nil | ||
| 279 | } | ||
| 280 | |||
| 281 | func TestCheckActions(t *testing.T) { | ||
| 282 | for _, tc := range []struct { | ||
| 283 | argv []string | ||
| 284 | fail bool | ||
| 285 | }{ | ||
| 286 | {[]string{"mr", "dif", "a/b", "1"}, true}, | ||
| 287 | {[]string{"mr", "diff", "a/b", "1", "--bogus"}, true}, | ||
| 288 | {[]string{"mr", "review", "a/b", "1", "--approve"}, false}, | ||
| 289 | {[]string{"mr", "list", "a/b", "--state"}, true}, | ||
| 290 | {[]string{"mr", "list", "a/b", "--state", "all"}, false}, | ||
| 291 | {[]string{"mr", "rebase", "1"}, false}, | ||
| 292 | } { | ||
| 293 | r := &recorder{} | ||
| 294 | checkActions(r, screen{actions: []action{{"G", tc.argv}}}) | ||
| 295 | if r.failed != tc.fail { | ||
| 296 | t.Errorf("%q: failed = %v, want %v", tc.argv, r.failed, tc.fail) | ||
| 297 | } | ||
| 298 | } | ||
| 299 | } | ||