- eighteen violation plants pin nested src=, Go aliases, unrun examples, build membership and command forms - unit tests cover fence collection, captions, goLang, go doc -c, commandWord and Sync
665 lines
25 KiB
Go
665 lines
25 KiB
Go
package docsite
|
|
|
|
import (
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fixtureModule is a small package exercising every declaration kind the
|
|
// identifier index records.
|
|
const fixtureModule = `package fixture
|
|
|
|
import "context"
|
|
|
|
// Bus is a generic-method host with a field and an embedded type.
|
|
type Bus struct {
|
|
Name string
|
|
Base
|
|
}
|
|
|
|
// Base is embedded in Bus.
|
|
type Base struct{ ID int }
|
|
|
|
// Ping is promoted to Bus.
|
|
func (Base) Ping() {}
|
|
|
|
// Handler is an interface.
|
|
type Handler interface {
|
|
Handle(ctx context.Context) error
|
|
}
|
|
|
|
// Mode is a const.
|
|
const Mode = 1
|
|
|
|
// Default is a var.
|
|
var Default = &Bus{}
|
|
|
|
// New builds a Bus.
|
|
func New() *Bus { return &Bus{} }
|
|
|
|
// Fire is a generic method.
|
|
func (b *Bus) Fire[T any](v T) {}
|
|
|
|
// Close is a value-receiver method.
|
|
func (b Bus) Close() error { return nil }
|
|
`
|
|
|
|
func identFixture(t *testing.T, indexBody, readme string) string {
|
|
t.Helper()
|
|
return writeTree(t, map[string]string{
|
|
"docs/site.yaml": fixtureSite,
|
|
"docs/index.md": page("Acme docs", "index", 0, indexBody),
|
|
"docs/setup/start.md": page("Start", "setup", 10, "Text.\n"),
|
|
"modules/fixture/fixture.go": fixtureModule,
|
|
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n" + readme,
|
|
"modules/fixture/sub/sub.go": "package sub\n\n// Thing is exported.\ntype Thing struct{}\n",
|
|
"modules/fixture/testdata/x.go": "package x\n\n// Hidden is never indexed.\nfunc Hidden() {}\n",
|
|
})
|
|
}
|
|
|
|
// writeFile writes one file under root, creating its directory.
|
|
func writeFile(t *testing.T, root, name, body string) {
|
|
t.Helper()
|
|
p := filepath.Join(root, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestIdentifierChecker(t *testing.T) {
|
|
passing := strings.Join([]string{
|
|
"- `fixture.New()` and `fixture.New`",
|
|
"- `*fixture.Bus` and `fixture.Bus.Close`",
|
|
"- `fixture.Bus.Fire[string](\"x\")` (generic method)",
|
|
"- `fixture.Bus.Name` and `fixture.Bus.Base` (field, embedded)",
|
|
"- `fixture.Bus.ID` and `fixture.Bus.Ping()` (promoted through Base)",
|
|
"- `fixture.Handler.Handle` (interface method)",
|
|
"- `fixture.Mode`, `fixture.Default`, `fixture.Bus.lowercase`",
|
|
"- `sub.Thing` (sub-package)",
|
|
"- `http.Handler`, `fields.yaml`, `acme.blog`, `summer.yaml`, `fixture.lower`",
|
|
"",
|
|
"```text",
|
|
"fixture.NotChecked() // fenced blocks are skipped",
|
|
"```",
|
|
"",
|
|
}, "\n")
|
|
root := identFixture(t, passing, "## Usage\n\nCall `fixture.New()`.\n")
|
|
problems, err := Check(Options{Root: root, Commands: fixtureCommands})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(problems) > 0 {
|
|
t.Fatalf("passing fixture: %q", problemLines(problems))
|
|
}
|
|
|
|
failing := passing + "Then `fixture.Missing` and `fixture.Bus.Nope`.\n\n`fixture.Hidden` lives in testdata.\n"
|
|
root = identFixture(t, failing, "## Usage\n\nCall `fixture.Gone()`.\n")
|
|
writeFile(t, root, "README.md", "# Root\n\nSee `x.Y`.\n\n`sub.Nothing`\n")
|
|
problems, err = Check(Options{Root: root, Commands: fixtureCommands})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []string{
|
|
"README.md:5: identifier: sub.Nothing does not exist in modules/fixture/sub",
|
|
"docs/index.md:22: identifier: fixture.Missing does not exist in modules/fixture",
|
|
"docs/index.md:22: identifier: fixture.Bus.Nope does not exist in modules/fixture",
|
|
"docs/index.md:24: identifier: fixture.Hidden does not exist in modules/fixture",
|
|
"modules/fixture/README.md:7: identifier: fixture.Gone does not exist in modules/fixture",
|
|
}
|
|
if got := problemLines(problems); !slices.Equal(got, want) {
|
|
t.Fatalf("problems =\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
|
}
|
|
}
|
|
|
|
func TestIdentifierIndexDuplicateName(t *testing.T) {
|
|
root := writeTree(t, map[string]string{
|
|
"modules/alpha/alpha.go": "package alpha\n",
|
|
"modules/alpha/util/util.go": "package util\n",
|
|
"modules/beta/util/util.go": "package util\n",
|
|
})
|
|
_, problems, err := buildIdentIndex(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := `modules/beta/util: identifier: package name "util" is used by modules/alpha/util and modules/beta/util; spans cannot tell them apart`
|
|
if got := problemLines(problems); !slices.Equal(got, []string{want}) {
|
|
t.Fatalf("problems = %q, want [%q]", got, want)
|
|
}
|
|
}
|
|
|
|
// checkFixture writes a fixture tree around one index body and one
|
|
// fixture README section and returns its problem lines.
|
|
func checkFixture(t *testing.T, cmds *Commands, files map[string]string) []string {
|
|
t.Helper()
|
|
tree := map[string]string{
|
|
"docs/site.yaml": fixtureSite,
|
|
"docs/setup/start.md": page("Start", "setup", 10, "## First steps\n\nText.\n"),
|
|
"modules/fixture/fixture.go": "package fixture\n",
|
|
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n## Usage\n\nText.\n",
|
|
}
|
|
for k, v := range files {
|
|
tree[k] = v
|
|
}
|
|
root := writeTree(t, tree)
|
|
problems, err := Check(Options{Root: root, Commands: cmds})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return problemLines(problems)
|
|
}
|
|
|
|
func assertProblems(t *testing.T, got, want []string) {
|
|
t.Helper()
|
|
if !slices.Equal(got, want) {
|
|
t.Fatalf("problems =\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
|
}
|
|
}
|
|
|
|
func TestLinkChecker(t *testing.T) {
|
|
good := "## Alpha\n\nSee [alpha](#alpha), [start](setup/start.md#first-steps), " +
|
|
"[usage](../modules/fixture/README.md#usage), [web](https://example.com) and [mail](mailto:a@example.com).\n\n" +
|
|
"\n"
|
|
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, good),
|
|
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n## Usage\n\n" +
|
|
"See [start](../../docs/setup/start.md) and [source](fixture.go).\n",
|
|
}), nil)
|
|
|
|
bad := good + "\n[a](#nope) [b](setup/missing.md) [c](setup/start.md#nope)\n\n" +
|
|
"[d](../modules/fixture/fixture.go) [e](/abs.html)\n"
|
|
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, bad),
|
|
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n## Usage\n\n" +
|
|
"See [other](../other/README.md) and [anchor](#missing).\n",
|
|
}), []string{
|
|
"docs/index.md:15: link: #nope not found in docs/index.md",
|
|
"docs/index.md:15: link: setup/missing.md does not resolve",
|
|
"docs/index.md:15: link: #nope not found in docs/setup/start.md",
|
|
"docs/index.md:17: link: ../modules/fixture/fixture.go does not resolve",
|
|
"docs/index.md:17: link: /abs.html does not resolve",
|
|
"modules/fixture/README.md:7: link: ../other/README.md does not resolve",
|
|
"modules/fixture/README.md:7: link: #missing not found in modules/fixture/README.md",
|
|
})
|
|
}
|
|
|
|
func TestCommandChecker(t *testing.T) {
|
|
example := "package main\n\nimport \"example.com/bonfire\"\n\n" +
|
|
"var one = bonfire.Command{Name: \"acme:greet\"}\n\n" +
|
|
"var many = []bonfire.Command{{Name: \"acme:list\"}, {Name: \"acme:sync\"}}\n"
|
|
good := "Run `summer docs:build` or `$ summer make:plugin acme.blog`.\n\n" +
|
|
"```sh\n$ summer docs:build --out site\nsummer --help\ncd app && summer make:plugin acme.blog\n" +
|
|
"./bin/acme serve --addr :8080\n./bin/acme acme:greet blog\n./bin/acme acme:sync\n```\n\n" +
|
|
"```text\nsummer not:checked\n```\n\n`summer.yaml` and `go install ./cmd/summer` are not commands.\n"
|
|
files := map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, good),
|
|
"docs/examples/greet/main.go": example,
|
|
"examples/app/plugins/p/plugin.go": example,
|
|
}
|
|
assertProblems(t, checkFixture(t, fixtureCommands, files), nil)
|
|
|
|
files["docs/index.md"] = page("Acme docs", "index", 0, good+
|
|
"\nThen `summer no:such`.\n\n```bash\n./bin/acme docs:build\nsummer migrate\n```\n")
|
|
files["modules/fixture/README.md"] = "# fixture\n\nFixture does one thing.\n\n## Usage\n\n```sh\n./bin/acme fixture:run\n```\n"
|
|
assertProblems(t, checkFixture(t, fixtureCommands, files), []string{
|
|
`docs/index.md:26: command: "no:such" is not a summer or application command`,
|
|
`docs/index.md:29: command: "docs:build" is not a summer or application command`,
|
|
`docs/index.md:30: command: "migrate" is not a summer or application command`,
|
|
`modules/fixture/README.md:8: command: "fixture:run" is not a summer or application command`,
|
|
})
|
|
|
|
assertProblems(t, checkFixture(t, nil, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
|
}), []string{"docs: command: no command set supplied"})
|
|
}
|
|
|
|
func TestForbiddenChecker(t *testing.T) {
|
|
// The forbidden words are built at run time so no test source names a
|
|
// consuming application.
|
|
name := "Fono" + "teka"
|
|
accented := "P" + "Ł" + "Ý" + "tarium"
|
|
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, "The host application.\n"),
|
|
}), nil)
|
|
|
|
got := checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, "The "+name+" app.\n"),
|
|
"docs/setup/start.md": page("Start", "setup", 10, "## First steps\n\nSee "+accented+".\n"),
|
|
})
|
|
assertProblems(t, got, []string{
|
|
"docs/index.md:9: forbidden: consuming-application name in output",
|
|
"docs/setup/start.md:11: forbidden: consuming-application name in output",
|
|
})
|
|
|
|
site := strings.Replace(fixtureSite, "description: Acme docs.", "description: Docs for "+strings.ToLower(name)+".", 1)
|
|
got = checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/site.yaml": site,
|
|
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
|
})
|
|
if !slices.Contains(got, "llms.txt:3: forbidden: consuming-application name in output") {
|
|
t.Fatalf("output check missed llms.txt: %q", got)
|
|
}
|
|
for _, line := range got {
|
|
if !strings.HasSuffix(line, ": forbidden: consuming-application name in output") ||
|
|
strings.Contains(strings.ToLower(line), strings.ToLower(name)) {
|
|
t.Fatalf("unexpected problem line %q", line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFencePolicy(t *testing.T) {
|
|
readme := "# fixture\n\nFixture does one thing.\n\n## Usage of `fixture`\n\n```go\nfixture.Run()\n```\n\n> [!TIP]\n> Fine.\n"
|
|
good := "## Plain heading\n\n> [!NOTE]\n> A note.\n\n> [!WARNING]\n> Careful.\n\n```text\nplain\n```\n\n" +
|
|
"```yaml\nkey: value\n```\n\n```md\n> [!DANGER]\n```\n"
|
|
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, good),
|
|
"modules/fixture/README.md": readme,
|
|
}), nil)
|
|
|
|
bad := good + "\n```go\nfmt.Println()\n```\n\n> [!DANGER]\n> Boom.\n\n## Use `fixture`\n\n## Zażółć\n\n## See [start](setup/start.md)\n"
|
|
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, bad),
|
|
"modules/fixture/README.md": readme + "\n> [!CAUTION]\n> No.\n",
|
|
}), []string{
|
|
"docs/index.md:29: snippet: go code block has no src= reference",
|
|
"docs/index.md:33: callout: unknown type DANGER (use NOTE, TIP or WARNING)",
|
|
"docs/index.md:36: heading: headings must be plain ASCII text without links or code",
|
|
"docs/index.md:38: heading: headings must be plain ASCII text without links or code",
|
|
"docs/index.md:40: heading: headings must be plain ASCII text without links or code",
|
|
"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 TestNestedFenceChecks(t *testing.T) {
|
|
body := strings.Join([]string{
|
|
"> [!NOTE]",
|
|
"> ```go",
|
|
"> x := 1",
|
|
"> ```",
|
|
"",
|
|
"- item",
|
|
"",
|
|
" ```go",
|
|
" x := 1",
|
|
" ```",
|
|
"",
|
|
"```GO",
|
|
"x := 1",
|
|
"```",
|
|
"",
|
|
"```main.go",
|
|
"x := 1",
|
|
"```",
|
|
"",
|
|
" ```go",
|
|
" x := 1",
|
|
" ```",
|
|
"",
|
|
"> [!NOTE]",
|
|
"> ```sh",
|
|
"> summer bogus:nested",
|
|
"> ```",
|
|
"",
|
|
"> ```yaml src=config/app.yaml",
|
|
"> app:",
|
|
"> ```",
|
|
"",
|
|
}, "\n")
|
|
readme := strings.Join([]string{
|
|
"# fixture",
|
|
"",
|
|
"Fixture does one thing.",
|
|
"",
|
|
"## Usage",
|
|
"",
|
|
"```go",
|
|
"x := 1",
|
|
"```",
|
|
"",
|
|
"```go src=modules/fixture/fixture.go",
|
|
"package fixture",
|
|
"```",
|
|
"",
|
|
}, "\n")
|
|
assertProblems(t, checkFixture(t, fixtureCommands, map[string]string{
|
|
"docs/index.md": page("Acme docs", "index", 0, "Text.\n"),
|
|
"docs/setup/start.md": page("Start", "setup", 10, body),
|
|
"modules/fixture/README.md": readme,
|
|
}), []string{
|
|
"docs/setup/start.md:10: snippet: go code block has no src= reference",
|
|
"docs/setup/start.md:16: snippet: go code block has no src= reference",
|
|
"docs/setup/start.md:20: snippet: GO code block has no src= reference",
|
|
"docs/setup/start.md:24: snippet: main.go code block has no src= reference",
|
|
`docs/setup/start.md:34: command: "bogus:nested" is not a summer or application command`,
|
|
"docs/setup/start.md:37: snippet: config/app.yaml: " + nestedSrcMessage,
|
|
"modules/fixture/README.md:11: snippet: modules/fixture/fixture.go: " + readmeSrcMessage,
|
|
})
|
|
}
|
|
|
|
func TestIdentifierGoDocCaseSensitive(t *testing.T) {
|
|
root := writeTree(t, map[string]string{
|
|
"go.mod": "module example.com/forms\n\ngo 1.27\n",
|
|
"modules/forms/forms.go": "package forms\n\n" +
|
|
"// OpenFromApp is a fixture.\nfunc OpenFromApp() {}\n\n" +
|
|
"// Outer holds a method.\ntype Outer struct{}\n\n" +
|
|
"// HelloWorld is a method.\nfunc (Outer) HelloWorld() {}\n",
|
|
})
|
|
idx, problems, err := buildIdentIndex(root)
|
|
if err != nil || len(problems) > 0 {
|
|
t.Fatalf("buildIdentIndex: %v %q", err, problemLines(problems))
|
|
}
|
|
for _, q := range []string{"OpenFromApp", "Outer.HelloWorld"} {
|
|
if !idx.goDoc("modules/forms", q) {
|
|
t.Errorf("goDoc(%q) = false, want true", q)
|
|
}
|
|
}
|
|
for _, q := range []string{"Openfromapp", "Outer.Helloworld"} {
|
|
if idx.goDoc("modules/forms", q) {
|
|
t.Errorf("goDoc(%q) = true, want false", q)
|
|
}
|
|
}
|
|
if got := idx.checkSpan("forms.Openfromapp"); got != "forms.Openfromapp does not exist in modules/forms" {
|
|
t.Fatalf("checkSpan = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCommandWord(t *testing.T) {
|
|
tool, app := true, false
|
|
cases := []struct {
|
|
in string
|
|
name string
|
|
tool, ok bool
|
|
}{
|
|
{"summer docs:build", "docs:build", tool, true},
|
|
{"$ summer docs:build --out site", "docs:build", tool, true},
|
|
{"FOO=1 summer no:such", "no:such", tool, true},
|
|
{"FOO=1 BAR=x ./bin/acme serve", "serve", app, true},
|
|
{"summer --root . no:such", "no:such", tool, true},
|
|
{"summer --root=. docs:build", "docs:build", tool, true},
|
|
{"summer -r . docs:build", "docs:build", tool, true},
|
|
{"summer --help", "", tool, false},
|
|
{"summer --help docs:build", "docs:build", tool, true},
|
|
{"summer -h docs:build", "docs:build", tool, true},
|
|
{"summer --root", "", tool, false},
|
|
{"summer --root .", "", tool, false},
|
|
{"summer -- docs:build", "", tool, false},
|
|
{"go run ./cmd/summer no:such", "no:such", tool, true},
|
|
{"go run ./cmd/summer --root . docs:build", "docs:build", tool, true},
|
|
{"bin/acme migrate", "migrate", app, true},
|
|
{"./bin/acme migrate", "migrate", app, true},
|
|
{"echo summer x", "", false, false},
|
|
{"# summer x", "", false, false},
|
|
{"go test ./modules/demo", "", false, false},
|
|
{"go install ./cmd/summer", "", false, false},
|
|
{"summer", "", tool, false},
|
|
{"FOO=1", "", false, false},
|
|
{"summer --version", "", tool, false},
|
|
}
|
|
for _, tc := range cases {
|
|
name, isTool, ok := commandWord(tc.in)
|
|
if name != tc.name || isTool != tc.tool || ok != tc.ok {
|
|
t.Errorf("commandWord(%q) = %q, %v, %v; want %q, %v, %v",
|
|
tc.in, name, isTool, ok, tc.name, tc.tool, tc.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|