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:
@@ -1,6 +1,7 @@
|
||||
package docsite
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -274,3 +275,256 @@ func TestFencePolicy(t *testing.T) {
|
||||
"modules/fixture/README.md:14: callout: unknown type CAUTION (use NOTE, TIP or WARNING)",
|
||||
})
|
||||
}
|
||||
|
||||
// indexFormsModule declares the receiver and embedding forms the index
|
||||
// must resolve.
|
||||
const indexFormsModule = `package forms
|
||||
|
||||
import "sync"
|
||||
|
||||
// List is generic over one type.
|
||||
type List[T any] struct{ items []T }
|
||||
|
||||
// Map is generic over two types.
|
||||
type Map[K comparable, V any] struct{ m map[K]V }
|
||||
|
||||
// Len has a generic receiver.
|
||||
func (l *List[T]) Len() int { return len(l.items) }
|
||||
|
||||
// Get has a two-parameter generic receiver.
|
||||
func (m Map[K, V]) Get(k K) V { return m.m[k] }
|
||||
|
||||
// Paren has a parenthesised receiver.
|
||||
func (l (*List[T])) Paren() {}
|
||||
|
||||
// Base is embedded by pointer.
|
||||
type Base struct{}
|
||||
|
||||
// Hello is promoted to Outer through *Base.
|
||||
func (*Base) Hello() {}
|
||||
|
||||
// Inner embeds Base.
|
||||
type Inner struct{ *Base }
|
||||
|
||||
// Outer embeds Inner, a qualified type and a generic type.
|
||||
type Outer struct {
|
||||
Inner
|
||||
sync.Mutex
|
||||
List[int]
|
||||
Named, Other string
|
||||
}
|
||||
|
||||
// Reader embeds an interface and declares a method.
|
||||
type Reader interface {
|
||||
Closer
|
||||
Read() error
|
||||
}
|
||||
|
||||
// Closer is embedded in Reader.
|
||||
type Closer interface{ Close() error }
|
||||
|
||||
// Func is not a struct or interface.
|
||||
type Func func()
|
||||
|
||||
// Empty is an interface without methods.
|
||||
type Empty interface{}
|
||||
`
|
||||
|
||||
func TestIdentifierIndexForms(t *testing.T) {
|
||||
root := writeTree(t, map[string]string{
|
||||
"go.mod": "module example.com/forms\n\ngo 1.27\n",
|
||||
"modules/forms/forms.go": indexFormsModule,
|
||||
"modules/forms/forms_test.go": "package forms\n\nfunc NotIndexed() {}\n",
|
||||
"modules/forms/_skip/skip.go": "package skip\n",
|
||||
"modules/forms/.hidden/h.go": "package hidden\n",
|
||||
"modules/forms/testdata/td.go": "package td\n",
|
||||
"modules/forms/deeper/leaf/l.go": "package leaf\n\n// Leaf is deep.\nconst Leaf = 1\n",
|
||||
})
|
||||
idx, problems, err := buildIdentIndex(root)
|
||||
if err != nil || len(problems) > 0 {
|
||||
t.Fatalf("buildIdentIndex: %v %q", err, problemLines(problems))
|
||||
}
|
||||
if _, ok := idx.pkgs["leaf"]; !ok || len(idx.pkgs) != 2 {
|
||||
t.Fatalf("indexed packages = %v", slices.Sorted(maps.Keys(idx.pkgs)))
|
||||
}
|
||||
for span, want := range map[string]string{
|
||||
"forms.List": "",
|
||||
"forms.List.Len": "",
|
||||
"*forms.List[int]": "",
|
||||
"forms.List[string].Len()": "",
|
||||
"forms.Map.Get": "",
|
||||
"forms.List.Paren": "",
|
||||
"forms.Outer.Hello": "",
|
||||
"forms.Outer.Inner": "",
|
||||
"forms.Outer.Base": "",
|
||||
"forms.Outer.Mutex": "",
|
||||
"forms.Outer.List": "",
|
||||
"forms.Outer.Len": "",
|
||||
"forms.Outer.Other": "",
|
||||
"forms.Reader.Close": "",
|
||||
"forms.Reader.Closer": "",
|
||||
"forms.Reader.Read": "",
|
||||
"forms.Func": "",
|
||||
"forms.Empty": "",
|
||||
"forms.Outer.lowercase": "",
|
||||
"leaf.Leaf": "",
|
||||
"forms.New(ctx, opts)": "forms.New does not exist in modules/forms",
|
||||
"forms.Outer.Lock": "forms.Outer.Lock does not exist in modules/forms",
|
||||
"forms.NotIndexed": "forms.NotIndexed does not exist in modules/forms",
|
||||
"forms.Outer.Hello.Extra": "",
|
||||
"other.Thing": "",
|
||||
"forms.lower": "",
|
||||
"Forms.Upper": "",
|
||||
"forms.Func[": "",
|
||||
"leaf.Missing": "leaf.Missing does not exist in modules/forms/deeper/leaf",
|
||||
" forms.Map.Get ": "",
|
||||
"fmt.Println(forms.List{}.Len)": "",
|
||||
} {
|
||||
if got := idx.checkSpan(span); got != want {
|
||||
t.Errorf("checkSpan(%q) = %q, want %q", span, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifierGoDocFallback(t *testing.T) {
|
||||
root := writeTree(t, map[string]string{
|
||||
"go.mod": "module example.com/forms\n\ngo 1.27\n",
|
||||
"modules/forms/forms.go": indexFormsModule,
|
||||
})
|
||||
idx, _, err := buildIdentIndex(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// go doc resolves what it is asked for in the package directory.
|
||||
if !idx.goDoc("modules/forms", "List.Len") || idx.goDoc("modules/forms", "List.Nope") {
|
||||
t.Fatal("go doc fallback does not agree with the package")
|
||||
}
|
||||
if v, ok := idx.docs["modules/forms List.Len"]; !ok || !v {
|
||||
t.Fatal("go doc result not cached")
|
||||
}
|
||||
// Cached answers are used without running go doc again.
|
||||
idx.docs["modules/forms Cached"] = true
|
||||
if !idx.goDoc("modules/forms", "Cached") {
|
||||
t.Fatal("cached go doc answer ignored")
|
||||
}
|
||||
// Anything that is not an identifier never reaches the go doc argv.
|
||||
for _, q := range []string{"List[int]", "-u", "A.B.C", "A;B", ""} {
|
||||
if idx.goDoc("modules/forms", q) {
|
||||
t.Errorf("goDoc(%q) = true", q)
|
||||
}
|
||||
if _, ok := idx.docs["modules/forms "+q]; ok {
|
||||
t.Errorf("goDoc(%q) ran go doc", q)
|
||||
}
|
||||
}
|
||||
// The index misses a member promoted from another package; the
|
||||
// fallback runs and refuses it too, so the span is a problem.
|
||||
if got := idx.checkSpan("forms.Outer.Unlock"); got != "forms.Outer.Unlock does not exist in modules/forms" {
|
||||
t.Fatalf("checkSpan(forms.Outer.Unlock) = %q", got)
|
||||
}
|
||||
if _, ok := idx.docs["modules/forms Outer.Unlock"]; !ok {
|
||||
t.Fatal("the fallback did not run for an index miss")
|
||||
}
|
||||
// No modules directory is an empty index.
|
||||
idx, problems, err := buildIdentIndex(t.TempDir())
|
||||
if err != nil || len(problems) > 0 || len(idx.pkgs) != 0 {
|
||||
t.Fatalf("buildIdentIndex without modules/ = %v %v %v", idx.pkgs, problems, err)
|
||||
}
|
||||
// A module that does not parse is an error.
|
||||
bad := writeTree(t, map[string]string{"modules/bad/bad.go": "package bad\n\nfunc {\n"})
|
||||
if _, _, err := buildIdentIndex(bad); err == nil || !strings.Contains(err.Error(), "index modules") {
|
||||
t.Fatalf("buildIdentIndex on a broken module = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandTokenForms(t *testing.T) {
|
||||
body := strings.Join([]string{
|
||||
"Spans: `summer docs:build`, `$ summer make:plugin acme.blog`, `summer --version`, `summer`, `./bin/acme serve --addr 127.0.0.1:8080`.",
|
||||
"",
|
||||
"```console",
|
||||
"$ summer docs:build --out site; summer make:plugin acme.x || ./bin/acme migrate | tee log",
|
||||
"summer docs:build \\",
|
||||
" --check",
|
||||
"# summer not:a-command in a comment",
|
||||
"echo summer not:first",
|
||||
"./bin/other-app.v2 serve",
|
||||
"```",
|
||||
"",
|
||||
"```shell",
|
||||
"$ summer make:plugin spaced",
|
||||
"```",
|
||||
"",
|
||||
}, "\n")
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, body),
|
||||
}), nil)
|
||||
|
||||
bad := body + "`summer bogus:one`\n\n```sh\n$ summer docs:build && summer bogus:two; ./bin/acme bogus:three\n```\n\n```bash\nsummer bogus:four\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, bad),
|
||||
}), []string{
|
||||
`docs/index.md:23: command: "bogus:one" is not a summer or application command`,
|
||||
`docs/index.md:26: command: "bogus:two" is not a summer or application command`,
|
||||
`docs/index.md:26: command: "bogus:three" is not a summer or application command`,
|
||||
`docs/index.md:30: command: "bogus:four" is not a summer or application command`,
|
||||
})
|
||||
}
|
||||
|
||||
func TestExampleCommandNames(t *testing.T) {
|
||||
dir := writeTree(t, map[string]string{
|
||||
"a/main.go": "package main\n\nimport \"x/bonfire\"\n\nvar a = bonfire.Command{Name: \"a:one\", Description: \"d\"}\n" +
|
||||
"var b = []bonfire.Command{{Name: `a:two`}, {Name: \"a:\" + \"skip\"}, bonfire.Command{Name: \"a:three\"}}\n" +
|
||||
"var c = []other.Command{{Name: \"c:skip\"}}\n" +
|
||||
"var d = struct{ Name string }{Name: \"d:skip\"}\n" +
|
||||
"var e = bonfire.Command{\"e:positional\"}\n" +
|
||||
"var f = bonfire.Command{Name: name}\n",
|
||||
"a/main_test.go": "package main\n\nvar t = bonfire.Command{Name: \"t:skip\"}\n",
|
||||
"testdata/x.go": "package x\n\nvar x = bonfire.Command{Name: \"x:skip\"}\n",
|
||||
"node_modules/n.go": "package n\n\nvar n = bonfire.Command{Name: \"n:skip\"}\n",
|
||||
"vendor/v.go": "package v\n\nvar v = bonfire.Command{Name: \"v:skip\"}\n",
|
||||
"_old/o.go": "package o\n\nvar o = bonfire.Command{Name: \"o:skip\"}\n",
|
||||
".git/g.go": "package g\n\nvar g = bonfire.Command{Name: \"g:skip\"}\n",
|
||||
"b/readme.md": "bonfire.Command{Name: \"md:skip\"}\n",
|
||||
"b/pointer.go": "package b\n\nvar p = &bonfire.Command{Name: \"b:pointer\"}\n",
|
||||
"b/nested/deeper/deep.go": "package deeper\n\nfunc f() { _ = []bonfire.Command{{Name: \"b:deep\"}} }\n",
|
||||
})
|
||||
names, err := exampleCommandNames(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.Sort(names)
|
||||
if want := []string{"a:one", "a:three", "a:two", "b:deep", "b:pointer"}; !slices.Equal(names, want) {
|
||||
t.Fatalf("exampleCommandNames = %v, want %v", names, want)
|
||||
}
|
||||
if names, err := exampleCommandNames(filepath.Join(dir, "missing")); err != nil || names != nil {
|
||||
t.Fatalf("missing dir = %v, %v", names, err)
|
||||
}
|
||||
writeFile(t, dir, "c/broken.go", "package c\n\nfunc {\n")
|
||||
if _, err := exampleCommandNames(dir); err == nil || !strings.Contains(err.Error(), "scan") {
|
||||
t.Fatalf("broken example = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkCheckerImagesAndSchemes(t *testing.T) {
|
||||
good := "## Top\n\n [top](#top) [mail](mailto:x@example.com)\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, good),
|
||||
}), nil)
|
||||
bad := good + "\n\n\n[ftp](ftp://example.com/file) [js](javascript:alert(1))\n\n[](setup/missing.md)\n"
|
||||
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
||||
"docs/index.md": page("Acme docs", "index", 0, bad),
|
||||
}), []string{
|
||||
"docs/index.md:13: link: local.png does not resolve",
|
||||
"docs/index.md:15: link: ftp://example.com/file does not resolve",
|
||||
"docs/index.md:15: link: javascript:alert(1) does not resolve",
|
||||
"docs/index.md:17: link: setup/missing.md does not resolve",
|
||||
})
|
||||
if got := lineOf([]byte("a\nb"), -1, 7); got != 7 {
|
||||
t.Errorf("lineOf(-1) = %d", got)
|
||||
}
|
||||
if got := lineOf([]byte("a\nb"), 99, 7); got != 7 {
|
||||
t.Errorf("lineOf(past the end) = %d", got)
|
||||
}
|
||||
if got := lineOf([]byte("a\nb\nc"), 4, 1); got != 3 {
|
||||
t.Errorf("lineOf(4) = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user