package checks import ( "fmt" "io/fs" "path" "regexp" "strings" "testing" "testing/fstest" ) // Structural checks over hq's own documents (records.py until ADR 0527). The decision records and research // efforts live in the knowledge base (ADR 0290, issue 426); what stays here judges what this repository still // holds, and keeps the records from coming back. var ( linkRx = regexp.MustCompile(`\[[^\]]*\]\(([^)]+)\)`) // A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three. decisionEntryRx = regexp.MustCompile(`^ADR \d{4}$`) adrMentionRx = regexp.MustCompile(`\bADRs?\s+(\d+)\b`) researchMentionRx = regexp.MustCompile(`\b[Rr]esearch\s+(\d+)\b`) codeSpanRx = regexp.MustCompile("`[^`]*`") recordFolders = []string{"02-DECISIONS", "01-RESEARCH"} recordFolderPoints = map[string]bool{"02-DECISIONS/README.md": true, "01-RESEARCH/README.md": true} ) // checkLinks: every relative link resolves to something that exists. func checkLinks(fsys fs.FS) []string { var out []string for _, p := range markdownFiles(fsys) { for n, line := range strings.Split(readFile(fsys, p), "\n") { for _, m := range linkRx.FindAllStringSubmatch(line, -1) { target := m[1] if strings.HasPrefix(target, "http:") || strings.HasPrefix(target, "https:") || strings.HasPrefix(target, "mailto:") { continue } target = strings.TrimSpace(strings.SplitN(target, "#", 2)[0]) if target == "" { continue } if _, err := fs.Stat(fsys, path.Join(path.Dir(p), target)); err != nil { out = append(out, fmt.Sprintf("links: %s:%d: link does not resolve: %s", p, n+1, target)) } } } } return out } // checkNoRecords: the two folders the records left hold their pointer README and nothing else (ADR 0290 rule 9). func checkNoRecords(fsys fs.FS) []string { var out []string for _, folder := range recordFolders { if _, err := fs.Stat(fsys, folder); err != nil { out = append(out, "no-records: "+folder+": the folder is gone; it keeps a README that points at the knowledge base") continue } _ = fs.WalkDir(fsys, folder, func(p string, d fs.DirEntry, err error) error { if err != nil || p == folder || recordFolderPoints[p] { return nil } out = append(out, "no-records: "+p+": a decision record or research effort lives in the knowledge base, never here: "+ "write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)") return nil }) } return out } // checkDecisionsEntries: a design's `decisions:` names each record as "ADR NNNN". func checkDecisionsEntries(fsys fs.FS) []string { var out []string for _, p := range markdownFiles(fsys) { if !strings.HasPrefix(p, "03-DESIGN/") { continue } for _, e := range frontmatter(readFile(fsys, p)).list("decisions") { if !decisionEntryRx.MatchString(unquote(e)) { out = append(out, fmt.Sprintf("decisions-form: %s: decisions: entry %q is not 'ADR NNNN' (four digits)", p, e)) } } } return out } // checkMentions: in running text a decision record is "ADR NNNN" and a research effort "research NNN"; a code // span is an example and is not read. func checkMentions(fsys fs.FS) []string { var out []string for _, p := range markdownFiles(fsys) { for n, line := range strings.Split(readFile(fsys, p), "\n") { line = codeSpanRx.ReplaceAllString(line, "") for _, m := range adrMentionRx.FindAllStringSubmatch(line, -1) { if len(m[1]) != 4 { out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a decision record is named 'ADR NNNN', four digits", p, n+1, m[0])) } } for _, m := range researchMentionRx.FindAllStringSubmatch(line, -1) { if len(m[1]) != 3 { out = append(out, fmt.Sprintf("mention-form: %s:%d: %q: a research effort is named 'research NNN', three digits", p, n+1, m[0])) } } } } return out } var codeEntryRx = regexp.MustCompile(`\s*\(.*\)$`) // checkStatusAgainstCode: a to-be design naming specific code (a path after the repository) may not still be // `designed`. Deliberately weak: it cannot tell whether the prose is true, only that a document stopped claiming // to be unbuilt once it points at something. `code: [mesh-controller]` is a plan and stays `designed`. func checkStatusAgainstCode(fsys fs.FS) []string { var out []string for _, p := range markdownFiles(fsys) { if !strings.HasPrefix(p, "03-DESIGN/01-to-be/") || strings.HasSuffix(p, "README.md") { continue } front := frontmatter(readFile(fsys, p)) if front.scalar("status") != "designed" { continue } for _, e := range front.list("code") { named := strings.SplitN(strings.TrimSpace(codeEntryRx.ReplaceAllString(e, "")), " ", 2) if len(named) > 1 { out = append(out, fmt.Sprintf("status-vs-code: %s: `designed`, but names %q in %s. Naming a file claims it implements this "+ "- use `in-progress`, or `implemented` once it is defensible from that repository's main branch.", p, strings.TrimSpace(named[1]), named[0])) break } } } return out } func doc(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} } // The real tree. func TestRecordsLinksResolve(t *testing.T) { expectNone(t, checkLinks(rootFS())) } func TestRecordsFoldersHoldOnlyPointers(t *testing.T) { expectNone(t, checkNoRecords(rootFS())) } func TestRecordsDecisionsEntriesForm(t *testing.T) { expectNone(t, checkDecisionsEntries(rootFS())) } func TestRecordsMentionForm(t *testing.T) { expectNone(t, checkMentions(rootFS())) } func TestRecordsStatusAgainstCode(t *testing.T) { expectNone(t, checkStatusAgainstCode(rootFS())) } // The checks themselves, each against a broken fixture, and against a sound one. func TestRecordsLinksCatchesABrokenLink(t *testing.T) { broken := fstest.MapFS{ "a/one.md": doc("see [two](two.md) and [gone](gone.md#x) and [web](https://example.com/x)\n"), "a/two.md": doc("[up](../a/one.md#top) [anchor](#here)\n"), } got := checkLinks(broken) expectProblem(t, got, "a/one.md:1: link does not resolve: gone.md") if len(got) != 1 { t.Errorf("only the broken link is a problem, got %q", got) } } func TestRecordsNoRecordsCatchesARecordPutBack(t *testing.T) { broken := fstest.MapFS{ "02-DECISIONS/README.md": doc("pointer"), "02-DECISIONS/0001-x.md": doc("a record"), "01-RESEARCH/README.md": doc("pointer"), "01-RESEARCH/045/00-x.md": doc("an effort"), } got := checkNoRecords(broken) expectProblem(t, got, "02-DECISIONS/0001-x.md") expectProblem(t, got, "01-RESEARCH/045") expectProblem(t, checkNoRecords(fstest.MapFS{"02-DECISIONS/README.md": doc("p")}), "01-RESEARCH: the folder is gone") } func TestRecordsDecisionsFormCatchesAShortEntry(t *testing.T) { broken := fstest.MapFS{ "03-DESIGN/01-to-be/1.md": doc("---\nlayer: to-be\ndecisions:\n - ADR 0290\n - ADR 290\n---\n"), "00-META/x.md": doc("---\ndecisions: [ADR 5]\n---\n"), } got := checkDecisionsEntries(broken) expectProblem(t, got, `entry "ADR 290"`) if len(got) != 1 { t.Errorf("only a design's entries are read, and ADR 0290 is sound: %q", got) } } func TestRecordsMentionFormCatchesShortNumbers(t *testing.T) { broken := fstest.MapFS{"x.md": doc("fine ADR 0290 and research 045\nbad ADR 12 and research 45\nexample `ADR 7` and `research 9`\n")} got := checkMentions(broken) expectProblem(t, got, "x.md:2: \"ADR 12\"") expectProblem(t, got, "x.md:2: \"research 45\"") if len(got) != 2 { t.Errorf("code spans and sound forms are not problems: %q", got) } } func TestRecordsStatusAgainstCodeCatchesADesignedDesignNamingAFile(t *testing.T) { broken := fstest.MapFS{ "03-DESIGN/01-to-be/9.md": doc("---\nstatus: designed\ncode:\n - mesh-controller internal/x.go (the thing)\n---\n"), "03-DESIGN/01-to-be/8.md": doc("---\nstatus: designed\ncode: [mesh-controller]\n---\n"), "03-DESIGN/01-to-be/7.md": doc("---\nstatus: in-progress\ncode: [mesh-controller internal/x.go]\n---\n"), } got := checkStatusAgainstCode(broken) expectProblem(t, got, "03-DESIGN/01-to-be/9.md: `designed`, but names \"internal/x.go\" in mesh-controller") if len(got) != 1 { t.Errorf("a plan without a path and an in-progress design are sound: %q", got) } }