internal/control/help_test.go
144 lines · 4609 bytes
1package control
2
3import (
4 "bytes"
5 "regexp"
6 "slices"
7 "strings"
8 "testing"
9
10 "gitbay.org/gitbay/internal/protocol"
11)
12
13func helpOut(t *testing.T, term Term, prefix ...string) string {
14 t.Helper()
15 var out, errOut bytes.Buffer
16 c := &Ctx{Stdout: &out, Stderr: &errOut, Term: term, Scope: "full"}
17 c.Cfg.Server.SiteURL = "https://forge.test"
18 if code := Dispatch(c, append([]string{"help"}, prefix...)); code != protocol.ExitOK {
19 t.Fatalf("help %v: exit %d: %s", prefix, code, errOut.String())
20 }
21 return out.String()
22}
23
24func TestHelpVerb(t *testing.T) {
25 out := helpOut(t, Term{Cols: 100}, "issue", "list")
26 for _, want := range []string{
27 "list issues\n",
28 "USAGE\n gitbay issue list <owner/name> [flags]\n",
29 "FLAGS\n",
30 " --state open|closed|all",
31 "which issues (default open)\n",
32 " --json",
33 "EXAMPLES\n gitbay issue list krz/gitbay --label bug --state all\n",
34 } {
35 if !strings.Contains(out, want) {
36 t.Errorf("missing %q in:\n%s", want, out)
37 }
38 }
39 plain := helpOut(t, Term{}, "issue", "list")
40 if !strings.Contains(plain, " ssh git@forge.test issue list krz/gitbay --label bug --state all\n") {
41 t.Errorf("plain examples not ssh:\n%s", plain)
42 }
43 if !strings.Contains(plain, "USAGE\n ssh git@forge.test issue list <owner/name> [flags]\n") {
44 t.Errorf("plain usage:\n%s", plain)
45 }
46}
47
48// TestHelpVerbUsageKeepsRequiredFlags covers a usage line with no
49// optional flag to cut at: it prints whole, not truncated to "[flags]"
50// as though --yes were optional.
51func TestHelpVerbUsageKeepsRequiredFlags(t *testing.T) {
52 out := helpOut(t, Term{Cols: 100}, "repo", "delete")
53 if !strings.Contains(out, "USAGE\n gitbay repo delete <owner/name> --yes\n") {
54 t.Errorf("missing required --yes in usage:\n%s", out)
55 }
56 if strings.Contains(out, "[flags]") {
57 t.Errorf("repo delete has no optional flags; should not print [flags]:\n%s", out)
58 }
59}
60
61// TestHelpVerbUsageKeepsAlternative covers a usage line whose flag is
62// one side of a "|" alternative, not an optional extra.
63func TestHelpVerbUsageKeepsAlternative(t *testing.T) {
64 out := helpOut(t, Term{Cols: 100}, "notifications", "read")
65 if !strings.Contains(out, "USAGE\n gitbay notifications read <id>... | --all\n") {
66 t.Errorf("missing whole alternative in usage:\n%s", out)
67 }
68}
69
70func TestHelpNoun(t *testing.T) {
71 out := helpOut(t, Term{Cols: 100}, "issue")
72 for _, want := range []string{"issues\n", "READ\n", "WRITE\n", " list ", " create ", "gitbay issue <verb> --help for flags.\n"} {
73 if !strings.Contains(out, want) {
74 t.Errorf("missing %q in:\n%s", want, out)
75 }
76 }
77 if strings.Index(out, " list ") > strings.Index(out, "WRITE") {
78 t.Errorf("list is not under READ:\n%s", out)
79 }
80}
81
82func TestEveryNounHasASummary(t *testing.T) {
83 for _, cmd := range Commands() {
84 if nounSummaries[cmd.Path[0]] == "" {
85 t.Errorf("no noun summary for %q", cmd.Path[0])
86 }
87 }
88}
89
90var usageFlag = regexp.MustCompile(`--[a-z][a-z0-9-]*`)
91
92// trailingRedirect strips a shell redirect an example ends with, the way a
93// person's shell would before gitbay ever sees argv: protocol.Tokenize
94// rejects a bare < or > as a shell metacharacter, so the redirected part
95// is not something the command's own tokenizer parses.
96var trailingRedirect = regexp.MustCompile(`^(.*?)\s+[<>]\s*\S+$`)
97
98// Help is written once, in the registry. Every flag in a usage line has
99// a description, every description names a flag in the usage line, and
100// every command has an example that runs it.
101func TestHelpIsComplete(t *testing.T) {
102 for _, cmd := range Commands() {
103 path := strings.Join(cmd.Path, " ")
104 inUsage := map[string]bool{}
105 for _, f := range usageFlag.FindAllString(cmd.Usage, -1) {
106 if f != "--json" {
107 inUsage[f] = true
108 }
109 }
110 described := map[string]bool{}
111 for _, f := range cmd.Flags {
112 described[f.Name] = true
113 if f.Desc == "" {
114 t.Errorf("%s: %s has no description", path, f.Name)
115 }
116 if !inUsage[f.Name] {
117 t.Errorf("%s: %s is described but not in the usage", path, f.Name)
118 }
119 }
120 for f := range inUsage {
121 if !described[f] {
122 t.Errorf("%s: %s is in the usage with no description", path, f)
123 }
124 }
125 if len(cmd.Examples) == 0 {
126 t.Errorf("%s: no example", path)
127 }
128 for _, ex := range cmd.Examples {
129 toParse := ex
130 if m := trailingRedirect.FindStringSubmatch(ex); m != nil {
131 toParse = m[1]
132 }
133 argv, err := protocol.Tokenize(toParse)
134 if err != nil {
135 t.Errorf("%s: example %q: %v", path, ex, err)
136 continue
137 }
138 got, _, ok := Lookup(argv)
139 if !ok || !slices.Equal(got.Path, cmd.Path) {
140 t.Errorf("%s: example %q runs %v", path, ex, got.Path)
141 }
142 }
143 }
144}