- 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)
531 lines
21 KiB
Go
531 lines
21 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 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)
|
|
}
|
|
}
|