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 | 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 | } | |