Files
hq/00-META/checks/walk_test.go
T

151 lines
3.9 KiB
Go

// Package checks holds hq's four checks as Go tests (ADR 0527): records, index, cycle and words.
//
// The package has tests only. Each check is a function over an fs.FS that returns its problems, so
// the test of the real tree reads the repository's root, and the test of the check itself reads a
// deliberately broken tree built in memory and must see the problem named (every check here failed on
// a broken fixture before it passed). The shared reading of Markdown and frontmatter is in this file.
package checks
import (
"io/fs"
"os"
"regexp"
"sort"
"strings"
"testing"
)
// rootFS is hq's tree: the checks run from 00-META/checks, two folders under the root.
func rootFS() fs.FS { return os.DirFS("../..") }
// skipDirs are folders no check reads: git's, the agent tooling's and a session's scratch folder.
var skipDirs = map[string]bool{".git": true, ".claude": true, ".work": true}
// markdownFiles lists every Markdown file of the tree, sorted, in slash paths from the root.
func markdownFiles(fsys fs.FS) []string {
var out []string
_ = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
if p != "." && skipDirs[d.Name()] {
return fs.SkipDir
}
return nil
}
if strings.HasSuffix(p, ".md") {
out = append(out, p)
}
return nil
})
sort.Strings(out)
return out
}
func readFile(fsys fs.FS, p string) string {
b, err := fs.ReadFile(fsys, p)
if err != nil {
return ""
}
return string(b)
}
// Front is a document's frontmatter: a scalar field holds a string, a list field a []string.
type Front map[string]any
func (f Front) scalar(key string) string {
s, _ := f[key].(string)
return s
}
func (f Front) list(key string) []string {
switch v := f[key].(type) {
case []string:
return v
case string:
if strings.TrimSpace(v) != "" {
return []string{v}
}
}
return nil
}
var (
fmItem = regexp.MustCompile(`^\s+-\s*(.+?)\s*$`)
fmPair = regexp.MustCompile(`^([A-Za-z_-]+):\s*(.*)$`)
)
// frontmatter reads the block between the first two --- lines. It is not a YAML parser on purpose: a
// dependency to read a few scalar fields and one list would cost more than it returns. It returns nil
// when the document has no frontmatter.
func frontmatter(text string) Front {
if !strings.HasPrefix(text, "---\n") {
return nil
}
end := strings.Index(text[4:], "\n---")
if end == -1 {
return nil
}
out := Front{}
key := ""
for _, line := range strings.Split(text[4:4+end], "\n") {
if m := fmItem.FindStringSubmatch(line); m != nil && key != "" {
list, _ := out[key].([]string)
out[key] = append(list, m[1])
continue
}
m := fmPair.FindStringSubmatch(line)
if m == nil {
continue
}
key = m[1]
value := strings.TrimSpace(m[2])
switch {
case strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]"):
list := []string{}
for _, v := range strings.Split(value[1:len(value)-1], ",") {
if v = strings.TrimSpace(v); v != "" {
list = append(list, v)
}
}
out[key] = list
case value == "":
out[key] = []string{}
default:
out[key] = value
}
}
return out
}
func unquote(v string) string {
v = strings.TrimSpace(v)
if len(v) >= 2 && v[0] == v[len(v)-1] && (v[0] == '"' || v[0] == '\'') {
return v[1 : len(v)-1]
}
return v
}
// lineOf is the 1-based line of byte offset i in text.
func lineOf(text string, i int) int { return strings.Count(text[:i], "\n") + 1 }
// expectNone fails the test with every problem a check found in the real tree.
func expectNone(t *testing.T, problems []string) {
t.Helper()
for _, p := range problems {
t.Error(p)
}
}
// expectProblem fails unless some problem contains want: the broken fixture must be caught, by name.
func expectProblem(t *testing.T, problems []string, want string) {
t.Helper()
for _, p := range problems {
if strings.Contains(p, want) {
return
}
}
t.Errorf("no problem mentions %q; got %q", want, problems)
}