package main import ( "bytes" "flag" "io" "os" "regexp" "strings" "testing" "github.com/lukaszkasprzak/prognosis/internal/config" ) func readRepoFile(t *testing.T, rel string) string { t.Helper() // Tests run in the package directory; the docs live at the repo root. b, err := os.ReadFile("../../" + rel) if err != nil { t.Fatalf("cannot read %s: %v", rel, err) } // roff escapes a literal hyphen as \- , so "-no-warnings" is written // "\-no\-warnings". Undo that before searching, or every multi-word flag // looks undocumented when it is not. return strings.ReplaceAll(string(b), `\-`, "-") } // Every flag must appear in -h, in the README and in the man page. // // The flag set comes from defineFlags, the same function run() uses, so this // cannot be satisfied by a stale hand-written list: adding a flag and // forgetting to document it fails the build. func TestEveryFlagIsDocumented(t *testing.T) { fs := flag.NewFlagSet("prognosis", flag.ContinueOnError) fs.SetOutput(io.Discard) defineFlags(fs) var help bytes.Buffer usageTo(&help) docs := map[string]string{ "-h": help.String(), "README.md": readRepoFile(t, "README.md"), "man/prognosis.1": readRepoFile(t, "man/prognosis.1"), } fs.VisitAll(func(f *flag.Flag) { for where, text := range docs { if !strings.Contains(text, "-"+f.Name) { t.Errorf("flag -%s is not documented in %s", f.Name, where) } } }) } // The reverse: -h must not advertise a flag that does not exist, which would // send someone chasing a typo. func TestHelpAdvertisesNoPhantomFlags(t *testing.T) { fs := flag.NewFlagSet("prognosis", flag.ContinueOnError) fs.SetOutput(io.Discard) defineFlags(fs) real := map[string]bool{} fs.VisitAll(func(f *flag.Flag) { real[f.Name] = true }) var help bytes.Buffer usageTo(&help) for _, m := range regexp.MustCompile(`(?m)^ -([a-z-]+)`).FindAllStringSubmatch(help.String(), -1) { if !real[m[1]] { t.Errorf("-h lists -%s, which is not a real flag", m[1]) } } } // Every key the generated config file contains must be documented in the man // page. The generated file is the authoritative list of user-facing settings, // so this catches a new key that never reached the documentation. func TestEveryConfigKeyIsDocumented(t *testing.T) { path := t.TempDir() + "/config" if err := config.WriteDefault(path, config.Default()); err != nil { t.Fatal(err) } generated, err := os.ReadFile(path) if err != nil { t.Fatal(err) } man := readRepoFile(t, "man/prognosis.1") seen := map[string]bool{} for _, line := range strings.Split(string(generated), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } key, _, ok := strings.Cut(line, "=") if !ok || seen[key] { continue } seen[key] = true if !strings.Contains(man, key) { t.Errorf("config key %q is written into the generated config but not documented in the man page", key) } } if len(seen) < 8 { t.Fatalf("only found %d config keys; the parser above is probably wrong", len(seen)) } } // The columns a user can name must all be documented, or the error message // listing them points at something the man page never explains. func TestEveryColumnIsDocumented(t *testing.T) { man := readRepoFile(t, "man/prognosis.1") readme := readRepoFile(t, "README.md") for _, col := range config.ValidColumns() { if !strings.Contains(man, col) { t.Errorf("column %q is not documented in the man page", col) } if !strings.Contains(readme, col) { t.Errorf("column %q is not documented in the README", col) } } }