test(11.1-06): branch-level tests for every docsite stage (94.8% coverage)
- load, render, emit, snippet, highlight, serve and checker branch tests; docs:build --check, docs:sync and docs:serve output tests in cmd/summer - fix: a CRLF or BOM page is reported as such, not as a page without a frontmatter block (TestFrontmatterEncodingProblems) - fix: src=#Type.Method finds a method with a parenthesised receiver (TestSnippetGenericsAndGroups, Box.Paren) - fix: docs:serve adds its watches before announcing the server, so an edit made right after the serving line rebuilds (TestServeWatchRebuilds)
This commit is contained in:
312
internal/docsite/load_test.go
Normal file
312
internal/docsite/load_test.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package docsite
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSite(t *testing.T) {
|
||||
const valid = "title: Acme\ndescription: Acme docs.\nsections:\n - name: setup\n title: Setup\n"
|
||||
s, err := ParseSite([]byte(valid))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.Title != "Acme" || len(s.Sections) != 1 || s.sectionTitle("setup") != "Setup" || s.sectionTitle("other") != "other" {
|
||||
t.Fatalf("ParseSite = %+v", s)
|
||||
}
|
||||
for _, tc := range []struct{ name, raw, want string }{
|
||||
{"unknown key", valid + "colour: red\n", "docsite: parse site config: "},
|
||||
{"not a mapping", "- a\n- b\n", "docsite: parse site config: "},
|
||||
{"no title", "description: d\nsections:\n - name: a\n title: A\n", "title is required"},
|
||||
{"no description", "title: t\nsections:\n - name: a\n title: A\n", "description is required"},
|
||||
{"no sections", "title: t\ndescription: d\n", "sections is required"},
|
||||
{"section without title", "title: t\ndescription: d\nsections:\n - name: a\n", "every section needs a name and a title"},
|
||||
{"reserved index", "title: t\ndescription: d\nsections:\n - name: index\n title: I\n", `section name "index" is reserved`},
|
||||
{"bad slug", "title: t\ndescription: d\nsections:\n - name: Setup\n title: S\n", `section name "Setup" must be lowercase`},
|
||||
{"duplicate", "title: t\ndescription: d\nsections:\n - name: a\n title: A\n - name: a\n title: B\n", `section "a" is listed twice`},
|
||||
} {
|
||||
if _, err := ParseSite([]byte(tc.raw)); err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Errorf("%s: err = %v, want %q", tc.name, err, tc.want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(func() string { _, err := ParseSite([]byte(valid + "colour: red\n")); return err.Error() }(), "\n") {
|
||||
t.Error("a decode error must be reported on one line")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "site.yaml")
|
||||
if err := os.WriteFile(path, []byte(valid), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := LoadSite(path); err != nil || got.Title != "Acme" {
|
||||
t.Fatalf("LoadSite = %+v, %v", got, err)
|
||||
}
|
||||
if _, err := LoadSite(filepath.Join(dir, "missing.yaml")); err == nil || !strings.Contains(err.Error(), "read site config") {
|
||||
t.Fatalf("LoadSite(missing) = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitFrontmatter(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
raw string
|
||||
ok bool
|
||||
fm, body string
|
||||
bodyLine int
|
||||
}{
|
||||
{"plain", "---\ntitle: A\n---\n# A\n", true, "title: A\n", "# A\n", 4},
|
||||
{"trailing spaces on the closing line", "---\ntitle: A\n--- \n# A\n", true, "title: A\n", "# A\n", 4},
|
||||
{"no body", "---\ntitle: A\n---", true, "title: A\n", "", 4},
|
||||
{"no body with newline", "---\ntitle: A\n---\n", true, "title: A\n", "", 4},
|
||||
{"empty block", "---\n---\n# A\n", true, "", "# A\n", 3},
|
||||
{"no closing delimiter", "---\ntitle: A\n# A\n", false, "", "", 0},
|
||||
{"no opening delimiter", "title: A\n---\n# A\n", false, "", "", 0},
|
||||
{"CRLF line endings", "---\r\ntitle: A\r\n---\r\n# A\r\n", false, "", "", 0},
|
||||
{"byte order mark", "\xef\xbb\xbf---\ntitle: A\n---\n# A\n", false, "", "", 0},
|
||||
} {
|
||||
fm, body, line, ok := splitFrontmatter([]byte(tc.raw))
|
||||
if ok != tc.ok || string(fm) != tc.fm || string(body) != tc.body || line != tc.bodyLine {
|
||||
t.Errorf("%s: splitFrontmatter = %q, %q, %d, %v; want %q, %q, %d, %v",
|
||||
tc.name, fm, body, line, ok, tc.fm, tc.body, tc.bodyLine, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrontmatterEncodingProblems pins the problem a CRLF or BOM page gets:
|
||||
// it names the encoding, not a missing frontmatter block the file has.
|
||||
func TestFrontmatterEncodingProblems(t *testing.T) {
|
||||
crlf := strings.ReplaceAll(page("Win", "setup", 30, "Text.\n"), "\n", "\r\n")
|
||||
bom := "\xef\xbb\xbf" + page("Bom", "setup", 40, "Text.\n")
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
||||
"docs/setup/win.md": crlf,
|
||||
"docs/setup/bom.md": bom,
|
||||
}), []string{
|
||||
"docs/setup/bom.md:1: frontmatter: the file starts with a UTF-8 byte order mark; save it without one",
|
||||
"docs/setup/win.md:1: frontmatter: the file uses CRLF line endings; save it with LF line endings",
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseFrontmatter(t *testing.T) {
|
||||
fm, details := parseFrontmatter([]byte("title: A\ndescription: B\nsection: setup\norder: 3\n"))
|
||||
if len(details) > 0 || fm != (Frontmatter{Title: "A", Description: "B", Section: "setup", Order: 3}) {
|
||||
t.Fatalf("parseFrontmatter = %+v %q", fm, details)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name, raw string
|
||||
want []string
|
||||
}{
|
||||
{"unknown fields sorted, then missing", "title: A\nzeta: 1\nalpha: 2\nsection: s\norder: 1\n",
|
||||
[]string{`unknown field "alpha"`, `unknown field "zeta"`, `missing field "description"`}},
|
||||
{"empty and null values are missing", "title: \"\"\ndescription: ~\nsection: s\norder: 1\n",
|
||||
[]string{`missing field "title"`, `missing field "description"`}},
|
||||
{"everything missing", "{}\n",
|
||||
[]string{`missing field "title"`, `missing field "description"`, `missing field "section"`, `missing field "order"`}},
|
||||
} {
|
||||
if _, got := parseFrontmatter([]byte(tc.raw)); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("%s: details = %q, want %q", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{"- a\n- b\n", "title: A\ndescription: B\nsection: s\norder: x\n"} {
|
||||
if _, got := parseFrontmatter([]byte(raw)); len(got) != 1 || strings.Contains(got[0], "\n") {
|
||||
t.Errorf("parseFrontmatter(%q) = %q, want one single-line decode error", raw, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkPagesExclusions(t *testing.T) {
|
||||
root := writeTree(t, map[string]string{
|
||||
"docs/index.md": "x",
|
||||
"docs/setup/a.md": "x",
|
||||
"docs/setup/examples/b.md": "x",
|
||||
"docs/setup/notes.txt": "x",
|
||||
"docs/setup/_draft.md": "x",
|
||||
"docs/setup/.hidden.md": "x",
|
||||
"docs/_partials/c.md": "x",
|
||||
"docs/.cache/d.md": "x",
|
||||
"docs/examples/blog/README.md": "x",
|
||||
"docs/examples/blog/plugin.go": "x",
|
||||
"docs/archive/examples/keep.md": "x",
|
||||
})
|
||||
files, err := walkPages(filepath.Join(root, "docs"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []string
|
||||
for _, f := range files {
|
||||
rel, _ := filepath.Rel(root, f)
|
||||
got = append(got, filepath.ToSlash(rel))
|
||||
}
|
||||
want := []string{"docs/archive/examples/keep.md", "docs/index.md", "docs/setup/a.md", "docs/setup/examples/b.md"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("walkPages = %v, want %v", got, want)
|
||||
}
|
||||
if _, err := walkPages(filepath.Join(root, "nope")); err == nil {
|
||||
t.Fatal("walkPages on a missing directory returned no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitReadme(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, raw string
|
||||
ok bool
|
||||
title, desc string
|
||||
body string
|
||||
}{
|
||||
{"summary after blank lines", "# alpha\n\n\n Alpha does it. \n\n## Usage\n", true, "alpha", "Alpha does it.", "# alpha\n\n\n\n\n## Usage\n"},
|
||||
{"summary on line two", "# alpha\nAlpha does it.\n", true, "alpha", "Alpha does it.", "# alpha\n\n"},
|
||||
{"missing H1", "## alpha\n\nAlpha.\n", false, "", "", ""},
|
||||
{"no heading at all", "alpha\n\nAlpha.\n", false, "", "", ""},
|
||||
{"empty title", "# \n\nAlpha.\n", false, "", "", ""},
|
||||
{"heading instead of summary", "# alpha\n\n## Usage\n", false, "", "", ""},
|
||||
{"title only", "# alpha\n\n", false, "", "", ""},
|
||||
{"empty file", "", false, "", "", ""},
|
||||
} {
|
||||
title, desc, body, ok := splitReadme([]byte(tc.raw))
|
||||
if ok != tc.ok || title != tc.title || desc != tc.desc || string(body) != tc.body {
|
||||
t.Errorf("%s: splitReadme = %q, %q, %q, %v", tc.name, title, desc, body, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleNames(t *testing.T) {
|
||||
root := writeTree(t, map[string]string{
|
||||
"modules/zeta/zeta.go": "package zeta\n",
|
||||
"modules/alpha/alpha.go": "package alpha\n",
|
||||
"modules/tests/only_test.go": "package tests\n",
|
||||
"modules/nested/sub/sub.go": "package sub\n",
|
||||
"modules/_skip/skip.go": "package skip\n",
|
||||
"modules/.hidden/hidden.go": "package hidden\n",
|
||||
"modules/file.go": "package modules\n",
|
||||
"modules/docs/README.md": "# docs\n\nNo Go.\n",
|
||||
"modules/gofolder.go/keep.md": "not a Go file\n",
|
||||
})
|
||||
names, err := moduleNames(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(names, []string{"alpha", "zeta"}) {
|
||||
t.Fatalf("moduleNames = %v", names)
|
||||
}
|
||||
if names, err := moduleNames(t.TempDir()); err != nil || names != nil {
|
||||
t.Fatalf("moduleNames without modules/ = %v, %v", names, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSectionRules(t *testing.T) {
|
||||
// A module without an api section in site.yaml.
|
||||
noAPI := "title: Acme\ndescription: Acme docs.\nsections:\n - name: setup\n title: Setup\n - name: \"empty\"\n title: Empty\n"
|
||||
root := writeTree(t, map[string]string{
|
||||
"docs/site.yaml": noAPI,
|
||||
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
||||
"docs/setup/start.md": page("Start", "setup", 10, "Text.\n"),
|
||||
"modules/fixture/fixture.go": "package fixture\n",
|
||||
"modules/fixture/README.md": "# fixture\n\nFixture.\n",
|
||||
})
|
||||
_, problems, err := Pages(Options{Root: root})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertProblems(t, problemLines(problems), []string{
|
||||
`docs/site.yaml:1: section: "api" is not listed (every module README is published there)`,
|
||||
`docs/site.yaml:6: section: "empty" has no pages (add the section in the same change as its first page)`,
|
||||
})
|
||||
|
||||
// An invalid site.yaml stops the load with one site problem.
|
||||
root = writeTree(t, map[string]string{"docs/site.yaml": "title: Acme\n"})
|
||||
pages, problems, err := Pages(Options{Root: root})
|
||||
if err != nil || pages != nil {
|
||||
t.Fatalf("Pages = %v, %v", pages, err)
|
||||
}
|
||||
assertProblems(t, problemLines(problems), []string{"docs/site.yaml:1: site: site config: description is required"})
|
||||
|
||||
// A missing site.yaml is an error, not a problem.
|
||||
if _, _, err := Pages(Options{Root: t.TempDir()}); err == nil || !strings.Contains(err.Error(), "read site config") {
|
||||
t.Fatalf("Pages without site.yaml = %v", err)
|
||||
}
|
||||
if _, err := Check(Options{Root: t.TempDir(), Commands: fixtureCommands}); err == nil {
|
||||
t.Fatal("Check without site.yaml returned no error")
|
||||
}
|
||||
if _, _, err := Build(Options{Root: t.TempDir(), Out: filepath.Join(t.TempDir(), "site"), Commands: fixtureCommands}); err == nil {
|
||||
t.Fatal("Build without site.yaml returned no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSectionLine(t *testing.T) {
|
||||
raw := []byte("title: t\nsections:\n - name: setup\n title: S\n - name: 'quoted'\n title: Q\n - name: \"api\"\n")
|
||||
for name, want := range map[string]int{"setup": 3, "quoted": 5, "api": 7, "missing": 1} {
|
||||
if got := sectionLine(raw, name); got != want {
|
||||
t.Errorf("sectionLine(%s) = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOrderAndSources(t *testing.T) {
|
||||
root := writeTree(t, map[string]string{
|
||||
"docs/site.yaml": fixtureSite,
|
||||
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
||||
"docs/setup/b.md": page("Bee", "setup", 10, "Text.\n"),
|
||||
"docs/setup/a.md": page("Ay", "setup", 10+10, "Text.\n"),
|
||||
"docs/setup/c.md": page("Cee", "setup", 5, "Text.\n"),
|
||||
"modules/zeta/zeta.go": "package zeta\n",
|
||||
"modules/zeta/README.md": "# zeta\n\nZeta.\n",
|
||||
"modules/beta/beta.go": "package beta\n",
|
||||
"modules/beta/README.md": "# beta\n\nBeta.\n",
|
||||
})
|
||||
pages, problems, err := Pages(Options{Root: root})
|
||||
if err != nil || len(problems) > 0 {
|
||||
t.Fatalf("Pages: %v %q", err, problemLines(problems))
|
||||
}
|
||||
var urls []string
|
||||
for _, p := range pages {
|
||||
urls = append(urls, p.URL)
|
||||
}
|
||||
if want := []string{"index", "setup/c", "setup/b", "setup/a", "api/beta", "api/zeta"}; !slices.Equal(urls, want) {
|
||||
t.Fatalf("reading order = %v, want %v", urls, want)
|
||||
}
|
||||
if pages[1].Source != "docs/setup/c.md" || pages[1].BodyLine != 7 || !strings.HasPrefix(string(pages[1].Body), "# Cee\n") {
|
||||
t.Fatalf("setup/c = %+v", pages[1])
|
||||
}
|
||||
|
||||
// A Src outside the root is shown as an absolute path.
|
||||
src := filepath.Join(t.TempDir(), "docs")
|
||||
if err := os.Rename(filepath.Join(root, "docs"), src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(src, "setup/c.md")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(src, "setup/c.md"), []byte("no frontmatter\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, problems, err = Pages(Options{Root: root, Src: src})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.ToSlash(filepath.Join(src, "setup/c.md")) + `:1: frontmatter: the file must start with a "---" frontmatter block closed by a "---" line`
|
||||
assertProblems(t, problemLines(problems), []string{want})
|
||||
}
|
||||
|
||||
func TestNormalizeDefaults(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
o, err := Options{}.normalize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.Root != wd || o.Src != filepath.Join(wd, "docs") || o.Out != filepath.Join(wd, "site") {
|
||||
t.Fatalf("normalize() = %+v", o)
|
||||
}
|
||||
o, err = Options{Root: "a/../b", Src: "x", Out: "y/"}.normalize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if o.Root != filepath.Join(wd, "b") || o.Src != filepath.Join(wd, "x") || o.Out != filepath.Join(wd, "y") {
|
||||
t.Fatalf("normalize(relative) = %+v", o)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user