internal/control/help_test.go

v1.36.0
gitbay/internal/control/help_test.go history · blame · raw

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}