test(11.1-06): acceptance subtests per success criterion and the final docs gate
- TestPhase11_1Acceptance asserts SC1 to SC5 on the real tree: sections and module pages, theme markers and no Node exec, AI outputs in sync, every go fence a src= copy run by go test, concept map and walkthrough - check-phase11.1.sh --named runs every named test of the phase by exact name (modules' TestDocs* and output Examples derived from source) and refuses failures, skips, missing or renamed tests and no tests to run - --all runs preconditions, deps, self-test, docs, forbidden, claude, named and the full go vet and go test
This commit is contained in:
516
cmd/summer/phase11_1_acceptance_test.go
Normal file
516
cmd/summer/phase11_1_acceptance_test.go
Normal file
@@ -0,0 +1,516 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/docsite"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
)
|
||||
|
||||
// TestPhase11_1Acceptance asserts the Phase 11.1 success criteria on the
|
||||
// real tree, one subtest per ROADMAP criterion.
|
||||
func TestPhase11_1Acceptance(t *testing.T) {
|
||||
opts := docsite.Options{Root: repoRoot, Commands: docsCommands()}
|
||||
pages, loadProblems, err := docsite.Pages(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
problems, err := docsite.Check(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, _ := buildRealTree(t)
|
||||
|
||||
// SC1: docs/ holds Markdown pages with frontmatter in the
|
||||
// Winter-mirroring sections, and every framework module is reachable
|
||||
// from the sidebar.
|
||||
t.Run("SC1", func(t *testing.T) {
|
||||
site, err := docsite.LoadSite(filepath.Join(repoRoot, "docs", "site.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var names []string
|
||||
for _, s := range site.Sections {
|
||||
names = append(names, s.Name)
|
||||
}
|
||||
if !slices.Equal(names, sectionOrder) {
|
||||
t.Errorf("site.yaml sections = %v, want %v", names, sectionOrder)
|
||||
}
|
||||
for _, p := range append(slices.Clone(loadProblems), problems...) {
|
||||
switch p.Rule {
|
||||
case "frontmatter", "section", "site", "page", "readme":
|
||||
t.Errorf("load problem: %s", p)
|
||||
}
|
||||
}
|
||||
guides := 0
|
||||
for _, p := range pages {
|
||||
if p.Module != "" {
|
||||
continue
|
||||
}
|
||||
guides++
|
||||
if p.Title == "" || p.Description == "" || !strings.HasPrefix(p.Source, "docs/") || !strings.HasSuffix(p.Source, ".md") {
|
||||
t.Errorf("page %s has incomplete frontmatter: %+v", p.URL, p)
|
||||
}
|
||||
if p.Section != "index" && (!slices.Contains(sectionOrder, p.Section) || p.Section == "api") {
|
||||
t.Errorf("page %s is in section %q", p.URL, p.Section)
|
||||
}
|
||||
}
|
||||
for _, sec := range sectionOrder[:len(sectionOrder)-1] {
|
||||
if !slices.ContainsFunc(pages, func(p docsite.Page) bool { return p.Section == sec }) {
|
||||
t.Errorf("section %s has no pages", sec)
|
||||
}
|
||||
}
|
||||
if guides < len(requiredPages) {
|
||||
t.Errorf("%d guide pages, want at least %d", guides, len(requiredPages))
|
||||
}
|
||||
sidebar := sidebarOf(t, out, "setup/installation.html")
|
||||
for _, m := range frameworkModules(t) {
|
||||
if !slices.ContainsFunc(pages, func(p docsite.Page) bool { return p.URL == "api/"+m && p.Module == m }) {
|
||||
t.Errorf("module %s has no api/%s page", m, m)
|
||||
}
|
||||
if !strings.Contains(sidebar, `href="/api/`+m+`.html"`) {
|
||||
t.Errorf("the sidebar does not link api/%s", m)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// SC2: summer builds a self-contained static site with the UI-SPEC
|
||||
// theme parts, search and dark mode, with no Node toolchain.
|
||||
t.Run("SC2", func(t *testing.T) {
|
||||
guide := readFile(t, filepath.Join(out, "setup", "installation.html"))
|
||||
for _, marker := range []string{
|
||||
`<body class="docs">`,
|
||||
`<header class="site-header">`,
|
||||
`<nav class="sidebar" aria-label="Documentation">`,
|
||||
`<aside class="toc" aria-label="On this page">`,
|
||||
`<details class="toc-inline">`,
|
||||
`<div class="page-actions">`,
|
||||
`<nav class="pager" aria-label="Previous and next page">`,
|
||||
`<footer class="site-footer">`,
|
||||
`<dialog class="search" id="search">`,
|
||||
`<button class="theme-toggle"`,
|
||||
`<figure class="code">`,
|
||||
`<a class="heading-anchor"`,
|
||||
`<svg class="icon icon-`,
|
||||
`<link rel="stylesheet" href="/assets/site.css">`,
|
||||
`<script src="/assets/theme-init.js"></script>`,
|
||||
`<script src="/assets/search.js"`,
|
||||
} {
|
||||
if !strings.Contains(guide, marker) {
|
||||
t.Errorf("setup/installation.html is missing the theme marker %s", marker)
|
||||
}
|
||||
}
|
||||
// Callouts of every type appear on the guide pages.
|
||||
guides := guidePagesHTML(t, out, pages)
|
||||
for _, kind := range []string{"note", "tip", "warning"} {
|
||||
marker := `<aside class="callout callout-` + kind + `" role="note">`
|
||||
if !strings.Contains(guides, marker) {
|
||||
t.Errorf("no guide page renders %s", marker)
|
||||
}
|
||||
}
|
||||
if nf := readFile(t, filepath.Join(out, "404.html")); !strings.Contains(nf, `<body class="docs docs-404">`) {
|
||||
t.Error("404.html has no not-found body marker")
|
||||
}
|
||||
for _, name := range []string{"site.css", "site.js", "search.js", "theme-init.js"} {
|
||||
assertFile(t, filepath.Join(out, "assets", name))
|
||||
}
|
||||
fonts, err := filepath.Glob(filepath.Join(out, "assets", "fonts", "*.woff2"))
|
||||
if err != nil || len(fonts) != 8 {
|
||||
t.Errorf("fonts = %v, want 8 woff2 files", fonts)
|
||||
}
|
||||
for _, name := range []string{"search-index.json", "assets/LICENSE-lucide.txt", "assets/fonts/LICENSE-dm-sans.txt", "assets/fonts/LICENSE-dm-mono.txt"} {
|
||||
assertFile(t, filepath.Join(out, filepath.FromSlash(name)))
|
||||
}
|
||||
if strings.Contains(guide, "//fonts.") || strings.Contains(guide, "cdn.") || strings.Contains(guide, "unpkg") {
|
||||
t.Error("the guide page loads an asset from a third-party origin")
|
||||
}
|
||||
|
||||
// No Node toolchain: nothing in the generator or the docs commands
|
||||
// executes node, npm or npx.
|
||||
files, err := filepath.Glob(filepath.Join(repoRoot, "internal", "docsite", "*.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files = append(files, "docs.go")
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
for _, prog := range execPrograms(t, f) {
|
||||
if prog == "" || slices.Contains([]string{"node", "npm", "npx", "pnpm", "yarn"}, filepath.Base(prog)) {
|
||||
t.Errorf("%s executes %q (the docs build must not need a Node toolchain)", f, prog)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !slices.ContainsFunc(toolCommands(), func(c bonfire.Command) bool { return c.Name == "docs:serve" }) {
|
||||
t.Error("docs:serve is not registered")
|
||||
}
|
||||
})
|
||||
|
||||
// SC3: llms.txt, llms-full.txt and a .md per page stay in sync with
|
||||
// the page tree.
|
||||
t.Run("SC3", func(t *testing.T) {
|
||||
var urls []string
|
||||
for _, p := range pages {
|
||||
urls = append(urls, p.URL)
|
||||
}
|
||||
html, md := outputPages(t, out)
|
||||
sorted := slices.Sorted(slices.Values(urls))
|
||||
if !slices.Equal(html, sorted) || !slices.Equal(md, sorted) {
|
||||
t.Errorf("html = %v\nmd = %v\nwant %v", html, md, sorted)
|
||||
}
|
||||
var linked []string
|
||||
for _, line := range strings.Split(readFile(t, filepath.Join(out, "llms.txt")), "\n") {
|
||||
if rest, ok := strings.CutPrefix(line, "- ["); ok {
|
||||
_, target, _ := strings.Cut(rest, "](/")
|
||||
target, _, _ = strings.Cut(target, ".md)")
|
||||
linked = append(linked, target)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(linked, urls) {
|
||||
t.Errorf("llms.txt links = %v, want %v", linked, urls)
|
||||
}
|
||||
var sources []string
|
||||
for _, line := range strings.Split(readFile(t, filepath.Join(out, "llms-full.txt")), "\n") {
|
||||
if rest, ok := strings.CutPrefix(line, "Source: /"); ok {
|
||||
sources = append(sources, strings.TrimSuffix(rest, ".html"))
|
||||
}
|
||||
}
|
||||
if !slices.Equal(sources, urls) {
|
||||
t.Errorf("llms-full.txt sources = %v, want %v", sources, urls)
|
||||
}
|
||||
for _, u := range []string{"index", "setup/installation"} {
|
||||
md := readFile(t, filepath.Join(out, filepath.FromSlash(u)+".md"))
|
||||
if !strings.HasPrefix(md, "# ") || strings.HasPrefix(md, "---") || strings.Contains(md, "src=") {
|
||||
t.Errorf("%s.md is not a clean Markdown page", u)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// SC4: every Go example in a docs/ page is compiled and run by go test,
|
||||
// and the checkers report nothing on the real tree.
|
||||
t.Run("SC4", func(t *testing.T) {
|
||||
for _, p := range problems {
|
||||
t.Errorf("docs problem: %s", p)
|
||||
}
|
||||
refs := docsGoFences(t)
|
||||
if len(refs) < 50 {
|
||||
t.Fatalf("%d go fences under docs/, expected the full content set", len(refs))
|
||||
}
|
||||
dirs := map[string]bool{}
|
||||
examples := 0
|
||||
for _, f := range refs {
|
||||
if f.src == "" {
|
||||
t.Errorf("%s:%d: go fence without src=", f.file, f.line)
|
||||
continue
|
||||
}
|
||||
path, frag, _ := strings.Cut(f.src, "#")
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
t.Errorf("%s:%d: go fence names a non-Go source %s", f.file, f.line, f.src)
|
||||
continue
|
||||
}
|
||||
dirs[filepath.Dir(path)] = true
|
||||
if strings.HasPrefix(frag, "Example") {
|
||||
examples++
|
||||
if !exampleWithOutput(t, filepath.Join(repoRoot, filepath.FromSlash(path)), frag) {
|
||||
t.Errorf("%s:%d: %s is not an Example with an // Output: comment", f.file, f.line, f.src)
|
||||
}
|
||||
}
|
||||
}
|
||||
if examples < 20 {
|
||||
t.Errorf("%d Example fences, expected the full content set", examples)
|
||||
}
|
||||
// Every referenced directory is a package of the root module, so
|
||||
// go test ./... compiles and runs it.
|
||||
listed := goList(t, "./...")
|
||||
for dir := range dirs {
|
||||
if !listed[filepath.ToSlash(dir)] {
|
||||
t.Errorf("%s is not a package of the root module", dir)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// SC5: the concept map and the acme/blog walkthrough exist, and the
|
||||
// walkthrough's code is verified under SC4.
|
||||
t.Run("SC5", func(t *testing.T) {
|
||||
for _, u := range []string{"setup/coming-from-wintercms", "setup/porting-a-plugin"} {
|
||||
if !slices.ContainsFunc(pages, func(p docsite.Page) bool { return p.URL == u }) {
|
||||
t.Errorf("page %s is missing", u)
|
||||
}
|
||||
}
|
||||
const walkthrough = "docs/setup/porting-a-plugin.md"
|
||||
srcs := 0
|
||||
for _, f := range pageFences(t, walkthrough) {
|
||||
lang, _, _ := strings.Cut(f.info, " ")
|
||||
switch {
|
||||
case lang == "go" && !strings.HasPrefix(f.src, "docs/examples/blog/"):
|
||||
t.Errorf("%s:%d: go fence is not a src= copy of docs/examples/blog (%q)", walkthrough, f.line, f.info)
|
||||
case f.src == "" && strings.Contains(f.body, "docs/examples/blog"):
|
||||
t.Errorf("%s:%d: fence shows docs/examples/blog code without src=", walkthrough, f.line)
|
||||
case strings.HasPrefix(f.src, "docs/examples/blog/"):
|
||||
srcs++
|
||||
}
|
||||
}
|
||||
if srcs < 15 {
|
||||
t.Errorf("%d src= fences into docs/examples/blog, expected the full walkthrough", srcs)
|
||||
}
|
||||
concept := readFile(t, filepath.Join(repoRoot, "docs", "setup", "coming-from-wintercms.md"))
|
||||
if !strings.Contains(concept, "porting-a-plugin.md") {
|
||||
t.Error("the concept map does not link the walkthrough")
|
||||
}
|
||||
listed := goList(t, "./docs/examples/...")
|
||||
for _, pkg := range []string{"docs/examples/blog", "docs/examples/blog/models", "docs/examples/blog/updates", "docs/examples/blog/controllers", "docs/examples/blog/console"} {
|
||||
if !listed[pkg] {
|
||||
t.Errorf("go list ./docs/examples/... does not include %s", pkg)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(repoRoot, "docs", "examples", "blog", "go.mod")); err == nil {
|
||||
t.Error("docs/examples/blog is a nested module; go test ./... would not run it")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func assertFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if st, err := os.Stat(path); err != nil || st.Size() == 0 {
|
||||
t.Errorf("missing or empty %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// sidebarOf returns the sidebar nav of a built page.
|
||||
func sidebarOf(t *testing.T, out, name string) string {
|
||||
t.Helper()
|
||||
html := readFile(t, filepath.Join(out, filepath.FromSlash(name)))
|
||||
start := strings.Index(html, `<nav class="sidebar" aria-label="Documentation">`)
|
||||
if start < 0 {
|
||||
t.Fatalf("%s has no sidebar", name)
|
||||
}
|
||||
end := strings.Index(html[start:], "</nav>")
|
||||
if end < 0 {
|
||||
t.Fatalf("%s has an unterminated sidebar", name)
|
||||
}
|
||||
return html[start : start+end]
|
||||
}
|
||||
|
||||
// guidePagesHTML concatenates the built HTML of every guide page.
|
||||
func guidePagesHTML(t *testing.T, out string, pages []docsite.Page) string {
|
||||
t.Helper()
|
||||
var b strings.Builder
|
||||
for _, p := range pages {
|
||||
if p.Module == "" {
|
||||
b.WriteString(readFile(t, filepath.Join(out, filepath.FromSlash(p.URL)+".html")))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// outputPages lists the built .html and .md pages (404.html and assets
|
||||
// excluded), sorted.
|
||||
func outputPages(t *testing.T, out string) (html, md []string) {
|
||||
t.Helper()
|
||||
err := filepath.WalkDir(out, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
rel, _ := filepath.Rel(out, p)
|
||||
rel = filepath.ToSlash(rel)
|
||||
if strings.HasPrefix(rel, "assets/") || rel == "404.html" {
|
||||
return nil
|
||||
}
|
||||
switch filepath.Ext(rel) {
|
||||
case ".html":
|
||||
html = append(html, strings.TrimSuffix(rel, ".html"))
|
||||
case ".md":
|
||||
md = append(md, strings.TrimSuffix(rel, ".md"))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.Sort(html)
|
||||
slices.Sort(md)
|
||||
return html, md
|
||||
}
|
||||
|
||||
// execPrograms returns the program argument of every exec.Command and
|
||||
// exec.CommandContext call in a Go file: the literal value, or "" when it
|
||||
// is not a string literal.
|
||||
func execPrograms(t *testing.T, file string) []string {
|
||||
t.Helper()
|
||||
f, err := parser.ParseFile(token.NewFileSet(), file, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
execName := ""
|
||||
for _, imp := range f.Imports {
|
||||
if p, _ := strconv.Unquote(imp.Path.Value); p == "os/exec" {
|
||||
execName = "exec"
|
||||
if imp.Name != nil {
|
||||
execName = imp.Name.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if execName == "" {
|
||||
return nil
|
||||
}
|
||||
var progs []string
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if x, ok := sel.X.(*ast.Ident); !ok || x.Name != execName {
|
||||
return true
|
||||
}
|
||||
arg := -1
|
||||
switch sel.Sel.Name {
|
||||
case "Command":
|
||||
arg = 0
|
||||
case "CommandContext":
|
||||
arg = 1
|
||||
}
|
||||
if arg < 0 || len(call.Args) <= arg {
|
||||
return true
|
||||
}
|
||||
prog := ""
|
||||
if lit, ok := call.Args[arg].(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||||
prog, _ = strconv.Unquote(lit.Value)
|
||||
}
|
||||
progs = append(progs, prog)
|
||||
return true
|
||||
})
|
||||
return progs
|
||||
}
|
||||
|
||||
// docFence is one fenced code block of a docs page, found by an
|
||||
// independent scanner (not docsite's).
|
||||
type docFence struct {
|
||||
file, info, src, body string
|
||||
line int
|
||||
}
|
||||
|
||||
func pageFences(t *testing.T, rel string) []docFence {
|
||||
t.Helper()
|
||||
var fences []docFence
|
||||
lines := strings.Split(readFile(t, filepath.Join(repoRoot, filepath.FromSlash(rel))), "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
trimmed := strings.TrimLeft(lines[i], " ")
|
||||
if !strings.HasPrefix(trimmed, "```") && !strings.HasPrefix(trimmed, "~~~") {
|
||||
continue
|
||||
}
|
||||
marker := trimmed[:3]
|
||||
f := docFence{file: rel, info: strings.TrimSpace(strings.TrimLeft(trimmed, marker[:1])), line: i + 1}
|
||||
for _, field := range strings.Fields(f.info) {
|
||||
if v, ok := strings.CutPrefix(field, "src="); ok {
|
||||
f.src = v
|
||||
}
|
||||
}
|
||||
var body []string
|
||||
for i++; i < len(lines) && !strings.HasPrefix(strings.TrimLeft(lines[i], " "), marker); i++ {
|
||||
body = append(body, lines[i])
|
||||
}
|
||||
f.body = strings.Join(body, "\n")
|
||||
fences = append(fences, f)
|
||||
}
|
||||
return fences
|
||||
}
|
||||
|
||||
// docsGoFences lists every go fence of every page under docs/ (the
|
||||
// docs/examples tree is code, not pages).
|
||||
func docsGoFences(t *testing.T) []docFence {
|
||||
t.Helper()
|
||||
var out []docFence
|
||||
err := filepath.WalkDir(filepath.Join(repoRoot, "docs"), func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() && d.Name() == "examples" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(p, ".md") {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(repoRoot, p)
|
||||
for _, f := range pageFences(t, filepath.ToSlash(rel)) {
|
||||
if lang, _, _ := strings.Cut(f.info, " "); lang == "go" {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// exampleWithOutput reports whether file declares the Example function name
|
||||
// with an // Output: comment, so go test runs it.
|
||||
func exampleWithOutput(t *testing.T, file, name string) bool {
|
||||
t.Helper()
|
||||
f, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Errorf("parse %s: %v", file, err)
|
||||
return false
|
||||
}
|
||||
for _, ex := range doc.Examples(f) {
|
||||
if "Example"+ex.Name == name {
|
||||
return ex.Output != "" || ex.EmptyOutput
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// goList returns the repository-relative directories of the packages a
|
||||
// go list pattern matches in the root module (workspace off).
|
||||
func goList(t *testing.T, pattern string) map[string]bool {
|
||||
t.Helper()
|
||||
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", pattern)
|
||||
cmd.Dir = repoRoot
|
||||
// Without the workspace, ./... matches the root module's packages only,
|
||||
// which is what the root go test ./... runs.
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
raw, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("go list %s: %v\n%s", pattern, err, stderr.String())
|
||||
}
|
||||
root, err := filepath.Abs(repoRoot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dirs := map[string]bool{}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
|
||||
if rel, err := filepath.Rel(root, line); err == nil && !strings.HasPrefix(rel, "..") {
|
||||
dirs[filepath.ToSlash(rel)] = true
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
@@ -8,7 +8,10 @@
|
||||
# orchestrates them and adds repository-level assertions: preconditions,
|
||||
# the dependency delta, the built output and the forbidden-name sweep.
|
||||
# --self-test plants one violation per rule in a scratch copy and requires
|
||||
# docs:build --check to refuse it for that rule.
|
||||
# docs:build --check to refuse it for that rule, then runs the Go
|
||||
# planted-violation corpus. --named runs every named test of the phase by
|
||||
# exact name and requires each to PASS: a failure, a skip, a missing or
|
||||
# renamed test and "no tests to run" all refuse.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${PHASE11_1_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
@@ -31,6 +34,7 @@ usage:
|
||||
check-phase11.1.sh --forbidden
|
||||
check-phase11.1.sh --claude
|
||||
check-phase11.1.sh --self-test
|
||||
check-phase11.1.sh --named
|
||||
check-phase11.1.sh --go
|
||||
check-phase11.1.sh --all
|
||||
EOF
|
||||
@@ -302,6 +306,63 @@ run_self_test() {
|
||||
echo "phase11.1 self-test passed"
|
||||
}
|
||||
|
||||
# docs_example_tests prints the TestDocs* functions and the Example*
|
||||
# functions with an output comment (the ones go test runs) declared in the
|
||||
# _test.go files of one package directory.
|
||||
docs_example_tests() {
|
||||
python3 - "$ROOT/$1" <<'PY'
|
||||
import glob, os, re, sys
|
||||
names = []
|
||||
for path in sorted(glob.glob(os.path.join(sys.argv[1], "*_test.go"))):
|
||||
src = open(path, encoding="utf-8").read()
|
||||
for m in re.finditer(r"^func (TestDocs\w*|Example\w*)\(", src, re.M):
|
||||
name = m.group(1)
|
||||
if name.startswith("Example"):
|
||||
end = re.search(r"^}", src[m.end():], re.M)
|
||||
body = src[m.end():m.end() + (end.start() if end else len(src))]
|
||||
if not re.search(r"//\s*(Unordered output|Output):", body):
|
||||
continue
|
||||
names.append(name)
|
||||
print(" ".join(names))
|
||||
PY
|
||||
}
|
||||
|
||||
# Named tests of the phase, by package. The docs examples in modules are
|
||||
# derived from the sources (every TestDocs* and every Example with an
|
||||
# output comment); the generator, CLI and walkthrough tests are listed.
|
||||
DOCSITE_TESTS=(TestPlantedViolations TestCleanFixture TestBuildOutputGuard TestSlugIDs TestSlugIDsEdgeCases
|
||||
TestReadmeIngestion TestSnippetForms TestSnippetConfinement TestSyncRewritesDrift TestSnippetGenericsAndGroups
|
||||
TestSyncPreservesAndReports TestIdentifierChecker TestIdentifierIndexDuplicateName TestIdentifierIndexForms
|
||||
TestIdentifierGoDocFallback TestLinkChecker TestCommandChecker TestCommandTokenForms TestForbiddenChecker
|
||||
TestFencePolicy TestFrontmatterEncodingProblems TestParseSite TestLLMSTxtShape TestLLMSFullBlocks
|
||||
TestMarkdownSiblings TestSearchIndexSchema TestBaseURLPrefixing TestTOCThreshold TestLinkRewriting
|
||||
TestCalloutRendering TestHighlightGo TestHighlightYAML TestHighlightShell TestHighlightFallback
|
||||
TestBuildSiteMarkers TestThemeAssetsAndPager TestServeHandler TestServeHandlerBranches TestServeAddrPolicy
|
||||
TestServeRefusesNonLoopback TestServeRebuildKeepsLastGoodBuild TestServeWatchRebuilds)
|
||||
SUMMER_TESTS=(TestPhase11_1Acceptance TestDocsTree TestDocsBuildRealTree TestEveryModuleInSidebar
|
||||
TestDocsAIOutputsInSync TestDocsRequiredPages TestDocsCommandNames TestDocsCommandsMirrorGeneratedMain
|
||||
TestDocsBuildCheckOutput TestDocsSyncOutput TestDocsServeRefusal TestToolCommandNames)
|
||||
|
||||
run_named() {
|
||||
local dir names
|
||||
go_json_named ./internal/docsite "${DOCSITE_TESTS[@]}" || refuse "named: internal/docsite"
|
||||
go_json_named ./cmd/summer "${SUMMER_TESTS[@]}" || refuse "named: cmd/summer"
|
||||
go_json_named ./docs/examples/blog TestPluginActivates TestRoutesRegistered TestScaffoldLayout \
|
||||
TestMigrateUpAndRollback TestPostsRouteAgainstDatabase TestPublishCommandAgainstDatabase \
|
||||
TestPublishCommandOpensDatabase || refuse "named: docs/examples/blog"
|
||||
go_json_named ./docs/examples/blog/models TestPostTableAndFill || refuse "named: docs/examples/blog/models"
|
||||
go_json_named ./docs/examples/blog/updates TestMigrationIDs || refuse "named: docs/examples/blog/updates"
|
||||
go_json_named ./docs/examples/blog/controllers TestPostsControllerDeclaration || refuse "named: docs/examples/blog/controllers"
|
||||
go_json_named ./docs/examples/blog/console TestPublishCommandShape || refuse "named: docs/examples/blog/console"
|
||||
while IFS= read -r dir; do
|
||||
names="$(docs_example_tests "$dir")"
|
||||
[ -n "$names" ] || refuse "named: $dir has an example_test.go but no Example with output or TestDocs test"
|
||||
# shellcheck disable=SC2086 # names is a space-separated list of identifiers
|
||||
go_json_named "./$dir" $names || refuse "named: $dir"
|
||||
done < <(cd "$ROOT" && find modules -name example_test.go -not -path '*/testdata/*' -printf '%h\n' | sort -u)
|
||||
echo "phase11.1 named passed"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--preconditions) run_preconditions ;;
|
||||
--deps) run_deps ;;
|
||||
@@ -309,6 +370,7 @@ case "${1:-}" in
|
||||
--forbidden) run_forbidden ;;
|
||||
--claude) run_claude ;;
|
||||
--self-test) run_self_test ;;
|
||||
--named) run_named ;;
|
||||
--go) run_go ;;
|
||||
--all)
|
||||
run_preconditions
|
||||
@@ -317,6 +379,7 @@ case "${1:-}" in
|
||||
run_docs
|
||||
run_forbidden
|
||||
run_claude
|
||||
run_named
|
||||
run_go
|
||||
echo "phase11.1 all passed"
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user