package main import ( "go/ast" "go/parser" "go/token" "os" "os/exec" "strconv" "strings" "testing" ) // dispatchedSubcommands reads main.go and returns the case strings of the // switch that dispatches os.Args[1]. Reading the source (rather than keeping a // second hand-written list) is the point: this is what makes the drift test // below authoritative instead of just another copy to forget. func dispatchedSubcommands(t *testing.T) []string { t.Helper() f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0) if err != nil { t.Fatalf("parse main.go: %v", err) } var found []string ast.Inspect(f, func(n ast.Node) bool { fn, ok := n.(*ast.FuncDecl) if !ok || fn.Name.Name != "main" { return true } ast.Inspect(fn, func(n ast.Node) bool { sw, ok := n.(*ast.SwitchStmt) // Only the switch on os.Args[1] — main() contains others. if !ok || !strings.Contains(exprText(sw.Tag), "os.Args[1]") { return true } for _, stmt := range sw.Body.List { cc, ok := stmt.(*ast.CaseClause) if !ok { continue } for _, e := range cc.List { if lit, ok := e.(*ast.BasicLit); ok && lit.Kind == token.STRING { if s, err := strconv.Unquote(lit.Value); err == nil { found = append(found, s) } } } } return false }) return false }) if len(found) == 0 { t.Fatal("found no dispatch cases in main() — has the switch moved? this test must be updated with it") } return found } // exprText renders an expression enough to recognise os.Args[1]. func exprText(e ast.Expr) string { idx, ok := e.(*ast.IndexExpr) if !ok { return "" } sel, ok := idx.X.(*ast.SelectorExpr) if !ok { return "" } pkg, ok := sel.X.(*ast.Ident) if !ok { return "" } lit, ok := idx.Index.(*ast.BasicLit) if !ok { return "" } return pkg.Name + "." + sel.Sel.Name + "[" + lit.Value + "]" } // TestUsageCoversEverySubcommand is the contract: `lchat help` lists exactly // what main() dispatches. A new subcommand with no help entry is a silent // feature; a help entry that dispatches nowhere is a lie in the CLI's own // documentation. Both fail here. func TestUsageCoversEverySubcommand(t *testing.T) { documented := map[string]bool{} for _, c := range subcommands { if documented[c.name] { t.Errorf("subcommands lists %q twice", c.name) } documented[c.name] = true } dispatched := map[string]bool{} for _, name := range dispatchedSubcommands(t) { dispatched[name] = true if !documented[name] { t.Errorf("main() dispatches %q but `lchat help` does not list it — add it to subcommands", name) } } for _, c := range subcommands { if !dispatched[c.name] { t.Errorf("`lchat help` lists %q but main() does not dispatch it", c.name) } } } func TestLooksLikeFlag(t *testing.T) { for _, tc := range []struct { arg string flag bool }{ {"-db", true}, {"--db", true}, {"-h", true}, {"-addr=:1747", true}, {"-", false}, // not a flag: a typo we should name, not pass to flag parsing {"router", false}, {"routes", false}, {"", false}, } { if got := looksLikeFlag(tc.arg); got != tc.flag { t.Errorf("looksLikeFlag(%q) = %v, want %v", tc.arg, got, tc.flag) } } } func TestIsSubcommand(t *testing.T) { for _, name := range []string{"router", "bootstrap", "links", "version"} { if !isSubcommand(name) { t.Errorf("isSubcommand(%q) = false, want true", name) } } // The words that used to boot a server instead of erroring. for _, name := range []string{"routes", "help", "", "Router", "add_instance"} { if isSubcommand(name) { t.Errorf("isSubcommand(%q) = true, want false", name) } } } func TestWriteUsageListsEveryCommand(t *testing.T) { var sb strings.Builder writeUsage(&sb) out := sb.String() for _, c := range subcommands { if !strings.Contains(out, c.name) { t.Errorf("usage output omits %q", c.name) } if c.blurb == "" { t.Errorf("%q has no blurb", c.name) } } if !strings.Contains(out, "lchat ") { t.Error("usage output should show the command form") } } // TestCheckSubcommandExits covers the behaviour that matters — the exit code // and stream — by re-running this test binary as a helper, since // checkSubcommand calls os.Exit. `help` must succeed on stdout; an unknown // word must fail on stderr; a flag must return so main() falls through to the // single-instance form. func TestCheckSubcommandExits(t *testing.T) { for _, tc := range []struct { name, arg string wantCode int wantStderr bool wantText string }{ {"help", "help", 0, false, "Commands:"}, {"unknown word", "routes", 2, true, `unknown command "routes"`}, {"bare dash", "-", 2, true, "unknown command"}, {"flag falls through", "-db", 0, false, "FELL-THROUGH"}, } { t.Run(tc.name, func(t *testing.T) { cmd := exec.Command(os.Args[0], "-test.run=TestCheckSubcommandHelper") cmd.Env = append(os.Environ(), "LCHAT_SUBCOMMAND_HELPER="+tc.arg) var stdout, stderr strings.Builder cmd.Stdout, cmd.Stderr = &stdout, &stderr err := cmd.Run() code := 0 if ee, ok := err.(*exec.ExitError); ok { code = ee.ExitCode() } else if err != nil { t.Fatalf("run helper: %v", err) } if code != tc.wantCode { t.Errorf("exit code = %d, want %d (stderr: %s)", code, tc.wantCode, stderr.String()) } stream, where := stdout.String(), "stdout" if tc.wantStderr { stream, where = stderr.String(), "stderr" } if !strings.Contains(stream, tc.wantText) { t.Errorf("%s = %q, want it to contain %q", where, stream, tc.wantText) } }) } } // TestCheckSubcommandHelper is not a real test: it is the subprocess body for // TestCheckSubcommandExits, and does nothing unless that env var is set. func TestCheckSubcommandHelper(t *testing.T) { arg, ok := os.LookupEnv("LCHAT_SUBCOMMAND_HELPER") if !ok { t.Skip("helper process only") } checkSubcommand(arg) // Only reached for a flag — the case that must still start an instance. os.Stdout.WriteString("FELL-THROUGH\n") os.Exit(0) }