diff --git a/00-META/checks/index_test.go b/00-META/checks/index_test.go new file mode 100644 index 00000000..ca4b7871 --- /dev/null +++ b/00-META/checks/index_test.go @@ -0,0 +1,93 @@ +package checks + +import ( + "fmt" + "io/fs" + "regexp" + "strings" + "testing" + "testing/fstest" +) + +// The reading order of the decision records (index.py until ADR 0527): its topics are written in +// 02-DECISIONS/README.md, its records are not. The knowledge base reads these lines and refuses a record whose +// topic is not one of them, so the form is a contract; a topic's records are `knowledge-base.holds` with that +// topic, never stored here (ADR 0248, issue 297). + +const readmeOfDecisions = "02-DECISIONS/README.md" + +// A topic line: `1. **Label** — `topic: name`. ...` +var topicLineRx = regexp.MustCompile(`(?m)^\d+\.\s+\*\*([^*]+)\*\*\s+—\s+` + "`topic:\\s*([^`]+)`") + +// What a stored list looks like: the old markers, or a line of the list index.py used to print. +var storedLists = []struct { + rx *regexp.Regexp + what string +}{ + {regexp.MustCompile(``), "an index marker"}, + {regexp.MustCompile(`(?m)^- \*\*\d{4}\*\* — \[`), "a line of a generated list"}, +} + +func readingOrder(text string) []string { + var topics []string + for _, m := range topicLineRx.FindAllStringSubmatch(text, -1) { + topics = append(topics, strings.TrimSpace(m[2])) + } + return topics +} + +func checkIndex(fsys fs.FS) []string { + text := readFile(fsys, readmeOfDecisions) + topics := readingOrder(text) + var out []string + if len(topics) == 0 { + out = append(out, "index: "+readmeOfDecisions+" writes no reading order: no line of the form '1. **Label** — `topic: name`. ...'; "+ + "the knowledge base refuses every record without it") + } + seen := map[string]bool{} + for _, topic := range topics { + if seen[topic] { + out = append(out, fmt.Sprintf("index: %s names a topic twice in its reading order: %s", readmeOfDecisions, topic)) + break + } + seen[topic] = true + } + for _, s := range storedLists { + if s.rx.MatchString(text) { + out = append(out, fmt.Sprintf("index: %s stores a list of records (%s). The records live in the knowledge base, and a topic's list is "+ + "`knowledge-base.holds` with that topic", readmeOfDecisions, s.what)) + } + } + return out +} + +func TestIndexReadingOrderIsWrittenAndStoresNoList(t *testing.T) { + expectNone(t, checkIndex(rootFS())) + if n := len(readingOrder(readFile(rootFS(), readmeOfDecisions))); n == 0 { + t.Error("the reading order has no topic") + } +} + +const sound = "1. **One** — `topic: one`. x\n2. **Two** — `topic: two`. y\n" + +func TestIndexCatchesAMissingReadingOrder(t *testing.T) { + expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc("nothing written here\n")}), "writes no reading order") +} + +func TestIndexCatchesADuplicateTopic(t *testing.T) { + broken := fstest.MapFS{readmeOfDecisions: doc(sound + "3. **Again** — `topic: one`. z\n")} + expectProblem(t, checkIndex(broken), "names a topic twice") +} + +func TestIndexCatchesAStoredList(t *testing.T) { + for what, stored := range map[string]string{ + "an index marker": "\n", + "a line of a generated list": "- **0001** — [a title](x.md)\n", + } { + expectProblem(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound + stored)}), what) + } +} + +func TestIndexAcceptsASoundReadme(t *testing.T) { + expectNone(t, checkIndex(fstest.MapFS{readmeOfDecisions: doc(sound)})) +}