Commit 72eaa3a587

72eaa3a587c6e7c3109b1c8187f7b879e17d26bf

parent: d6663a0512

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-02 03:17 UTC

control: test helpers for screen migrations

Ref #317

Layout: unified · split

internal/control/plain_test.go added +38
@@ -0,0 +1,38 @@
1package control
2
3import (
4 "flag"
5 "os"
6 "path/filepath"
7 "regexp"
8 "testing"
9)
10
11var updatePlain = flag.Bool("update-plain", false, "rewrite testdata/plain from the current output")
12
13var 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.
18func 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.
212type errorer interface {
213 Helper()
214 Errorf(format string, args ...any)
215}
216
217type recorder struct{ failed bool }
218
219func (r *recorder) Helper() {}
220func (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.
224var 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.
228func 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.
256func 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
281func 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}