eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "os"
8 "os/exec"
9 "strconv"
10 "strings"
11 "testing"
12)
13
14// dispatchedSubcommands reads main.go and returns the case strings of the
15// switch that dispatches os.Args[1]. Reading the source (rather than keeping a
16// second hand-written list) is the point: this is what makes the drift test
17// below authoritative instead of just another copy to forget.
18func dispatchedSubcommands(t *testing.T) []string {
19 t.Helper()
20 f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
21 if err != nil {
22 t.Fatalf("parse main.go: %v", err)
23 }
24 var found []string
25 ast.Inspect(f, func(n ast.Node) bool {
26 fn, ok := n.(*ast.FuncDecl)
27 if !ok || fn.Name.Name != "main" {
28 return true
29 }
30 ast.Inspect(fn, func(n ast.Node) bool {
31 sw, ok := n.(*ast.SwitchStmt)
32 // Only the switch on os.Args[1] — main() contains others.
33 if !ok || !strings.Contains(exprText(sw.Tag), "os.Args[1]") {
34 return true
35 }
36 for _, stmt := range sw.Body.List {
37 cc, ok := stmt.(*ast.CaseClause)
38 if !ok {
39 continue
40 }
41 for _, e := range cc.List {
42 if lit, ok := e.(*ast.BasicLit); ok && lit.Kind == token.STRING {
43 if s, err := strconv.Unquote(lit.Value); err == nil {
44 found = append(found, s)
45 }
46 }
47 }
48 }
49 return false
50 })
51 return false
52 })
53 if len(found) == 0 {
54 t.Fatal("found no dispatch cases in main() — has the switch moved? this test must be updated with it")
55 }
56 return found
57}
58
59// exprText renders an expression enough to recognise os.Args[1].
60func exprText(e ast.Expr) string {
61 idx, ok := e.(*ast.IndexExpr)
62 if !ok {
63 return ""
64 }
65 sel, ok := idx.X.(*ast.SelectorExpr)
66 if !ok {
67 return ""
68 }
69 pkg, ok := sel.X.(*ast.Ident)
70 if !ok {
71 return ""
72 }
73 lit, ok := idx.Index.(*ast.BasicLit)
74 if !ok {
75 return ""
76 }
77 return pkg.Name + "." + sel.Sel.Name + "[" + lit.Value + "]"
78}
79
80// TestUsageCoversEverySubcommand is the contract: `lchat help` lists exactly
81// what main() dispatches. A new subcommand with no help entry is a silent
82// feature; a help entry that dispatches nowhere is a lie in the CLI's own
83// documentation. Both fail here.
84func TestUsageCoversEverySubcommand(t *testing.T) {
85 documented := map[string]bool{}
86 for _, c := range subcommands {
87 if documented[c.name] {
88 t.Errorf("subcommands lists %q twice", c.name)
89 }
90 documented[c.name] = true
91 }
92 dispatched := map[string]bool{}
93 for _, name := range dispatchedSubcommands(t) {
94 dispatched[name] = true
95 if !documented[name] {
96 t.Errorf("main() dispatches %q but `lchat help` does not list it — add it to subcommands", name)
97 }
98 }
99 for _, c := range subcommands {
100 if !dispatched[c.name] {
101 t.Errorf("`lchat help` lists %q but main() does not dispatch it", c.name)
102 }
103 }
104}
105
106func TestLooksLikeFlag(t *testing.T) {
107 for _, tc := range []struct {
108 arg string
109 flag bool
110 }{
111 {"-db", true},
112 {"--db", true},
113 {"-h", true},
114 {"-addr=:1747", true},
115 {"-", false}, // not a flag: a typo we should name, not pass to flag parsing
116 {"router", false},
117 {"routes", false},
118 {"", false},
119 } {
120 if got := looksLikeFlag(tc.arg); got != tc.flag {
121 t.Errorf("looksLikeFlag(%q) = %v, want %v", tc.arg, got, tc.flag)
122 }
123 }
124}
125
126func TestIsSubcommand(t *testing.T) {
127 for _, name := range []string{"router", "bootstrap", "links", "version"} {
128 if !isSubcommand(name) {
129 t.Errorf("isSubcommand(%q) = false, want true", name)
130 }
131 }
132 // The words that used to boot a server instead of erroring.
133 for _, name := range []string{"routes", "help", "", "Router", "add_instance"} {
134 if isSubcommand(name) {
135 t.Errorf("isSubcommand(%q) = true, want false", name)
136 }
137 }
138}
139
140func TestWriteUsageListsEveryCommand(t *testing.T) {
141 var sb strings.Builder
142 writeUsage(&sb)
143 out := sb.String()
144 for _, c := range subcommands {
145 if !strings.Contains(out, c.name) {
146 t.Errorf("usage output omits %q", c.name)
147 }
148 if c.blurb == "" {
149 t.Errorf("%q has no blurb", c.name)
150 }
151 }
152 if !strings.Contains(out, "lchat <command>") {
153 t.Error("usage output should show the command form")
154 }
155}
156
157// TestCheckSubcommandExits covers the behaviour that matters — the exit code
158// and stream — by re-running this test binary as a helper, since
159// checkSubcommand calls os.Exit. `help` must succeed on stdout; an unknown
160// word must fail on stderr; a flag must return so main() falls through to the
161// single-instance form.
162func TestCheckSubcommandExits(t *testing.T) {
163 for _, tc := range []struct {
164 name, arg string
165 wantCode int
166 wantStderr bool
167 wantText string
168 }{
169 {"help", "help", 0, false, "Commands:"},
170 {"unknown word", "routes", 2, true, `unknown command "routes"`},
171 {"bare dash", "-", 2, true, "unknown command"},
172 {"flag falls through", "-db", 0, false, "FELL-THROUGH"},
173 } {
174 t.Run(tc.name, func(t *testing.T) {
175 cmd := exec.Command(os.Args[0], "-test.run=TestCheckSubcommandHelper")
176 cmd.Env = append(os.Environ(), "LCHAT_SUBCOMMAND_HELPER="+tc.arg)
177 var stdout, stderr strings.Builder
178 cmd.Stdout, cmd.Stderr = &stdout, &stderr
179 err := cmd.Run()
180
181 code := 0
182 if ee, ok := err.(*exec.ExitError); ok {
183 code = ee.ExitCode()
184 } else if err != nil {
185 t.Fatalf("run helper: %v", err)
186 }
187 if code != tc.wantCode {
188 t.Errorf("exit code = %d, want %d (stderr: %s)", code, tc.wantCode, stderr.String())
189 }
190 stream, where := stdout.String(), "stdout"
191 if tc.wantStderr {
192 stream, where = stderr.String(), "stderr"
193 }
194 if !strings.Contains(stream, tc.wantText) {
195 t.Errorf("%s = %q, want it to contain %q", where, stream, tc.wantText)
196 }
197 })
198 }
199}
200
201// TestCheckSubcommandHelper is not a real test: it is the subprocess body for
202// TestCheckSubcommandExits, and does nothing unless that env var is set.
203func TestCheckSubcommandHelper(t *testing.T) {
204 arg, ok := os.LookupEnv("LCHAT_SUBCOMMAND_HELPER")
205 if !ok {
206 t.Skip("helper process only")
207 }
208 checkSubcommand(arg)
209 // Only reached for a flag — the case that must still start an instance.
210 os.Stdout.WriteString("FELL-THROUGH\n")
211 os.Exit(0)
212}
213