package docgen import ( "os" "path/filepath" "runtime" "slices" "strings" "testing" ) // repoRoot is where the source paths in Sources are relative to. func repoRoot(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("cannot locate this test") } return filepath.Join(filepath.Dir(file), "..", "..") } func sourcePaths(t *testing.T) []string { t.Helper() root := repoRoot(t) out := make([]string, len(Sources)) for i, s := range Sources { out[i] = filepath.Join(root, s) } return out } func TestTheReferenceIsNotStale(t *testing.T) { // The whole point. A generated document that nobody regenerates is the // same lie as a hand-written one, arrived at more slowly: it describes the // configuration of some earlier week, and the setting somebody came to // look up is the one that is missing. blocks, err := Collect(sourcePaths(t)) if err != nil { t.Fatal(err) } want := Render(blocks) path := filepath.Join(repoRoot(t), "docs", "reference.md") got, err := os.ReadFile(path) if err != nil { t.Fatalf("docs/reference.md is missing; run go generate ./...: %v", err) } if string(got) != want { t.Error("docs/reference.md no longer matches the configuration structs.\n" + "Run: go generate ./...") } } func TestEveryConfigurationFileIsListed(t *testing.T) { // Sources is a list, and a list is a thing to forget. A new file full of // settings would otherwise be absent from the reference with nothing // saying so — which is exactly the failure the reference exists to fix. root := repoRoot(t) var tagged []string err := filepath.Walk(filepath.Join(root, "internal"), func(p string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") { return err } b, err := os.ReadFile(p) if err != nil { return err } if !strings.Contains(string(b), `koanf:"`) { return nil } rel, err := filepath.Rel(root, p) if err != nil { return err } tagged = append(tagged, rel) return nil }) if err != nil { t.Fatal(err) } if len(tagged) == 0 { t.Fatal("found no configuration files at all, so this test proves nothing") } for _, f := range tagged { if slices.Contains(Sources, f) { continue } // Deliberately out: these hold koanf tags for structs an operator // never writes — inventory and catalog entries come from the API or // from a model file, not from the configuration. if slices.Contains(notReference, f) { continue } t.Errorf("%s declares configuration and is not in Sources or notReference", f) } } func TestADocumentedKeyKeepsItsComment(t *testing.T) { // The descriptions are the field comments, which is what makes the // reference worth reading rather than a list of names. A field whose // comment was the only explanation anywhere must not render blank. blocks, err := Collect(sourcePaths(t)) if err != nil { t.Fatal(err) } var lease Field for _, b := range blocks { for _, f := range b.Fields { if f.Key == "lease_ttl" { lease = f } } } if lease.Key == "" { t.Fatal("lease_ttl is not in the reference") } if !strings.Contains(lease.Doc, "shared store") { t.Errorf("lease_ttl lost its explanation: %q", lease.Doc) } } func TestAFieldMarkedNotConfigurableIsLeftOut(t *testing.T) { // koanf:"-" means the field is carried in the struct and never read from // a file. Listing it would send somebody looking for a setting that does // not exist. blocks, err := Collect(sourcePaths(t)) if err != nil { t.Fatal(err) } for _, b := range blocks { for _, f := range b.Fields { if f.Key == "-" || f.Key == "" { t.Errorf("%s lists a non-configurable field: %+v", b.Name, f) } } } } func TestTheSettingsScreensDescribeTheCode(t *testing.T) { // The screens list Reference; it must be what the structs say now. blocks, err := Collect(sourcePaths(t)) if err != nil { t.Fatal(err) } have, err := os.ReadFile(filepath.Join(repoRoot(t), "internal", "docgen", "reference_gen.go")) if err != nil { t.Fatalf("reference_gen.go is missing; run go generate ./...: %v", err) } if string(have) != RenderGo(blocks) { t.Error("internal/docgen/reference_gen.go no longer matches the configuration structs; run go generate ./...") } }