- 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)
569 lines
18 KiB
Go
569 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/internal/docsite"
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
)
|
|
|
|
const repoRoot = "../.."
|
|
|
|
// TestDocsTree fails with every problem line in the real docs tree.
|
|
func TestDocsTree(t *testing.T) {
|
|
problems, err := docsite.Check(docsite.Options{Root: repoRoot, Commands: docsCommands()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, p := range problems {
|
|
t.Error(p.String())
|
|
}
|
|
}
|
|
|
|
// buildRealTree runs docs:build on the real tree into a temp dir and returns
|
|
// the output dir and the command output.
|
|
func buildRealTree(t *testing.T) (string, string) {
|
|
t.Helper()
|
|
out := filepath.Join(t.TempDir(), "site")
|
|
var buf bytes.Buffer
|
|
root, err := bonfire.NewRoot("summer", toolCommands(), &buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", out})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("docs:build: %v\n%s", err, buf.String())
|
|
}
|
|
return out, buf.String()
|
|
}
|
|
|
|
func TestDocsBuildRealTree(t *testing.T) {
|
|
out, stdout := buildRealTree(t)
|
|
if !regexp.MustCompile(`docs:build: wrote \d+ pages to `).MatchString(stdout) {
|
|
t.Fatalf("output = %q, want a wrote-pages line", stdout)
|
|
}
|
|
for _, name := range []string{
|
|
"index.html", "index.md",
|
|
"setup/installation.html", "setup/installation.md",
|
|
"llms.txt", "llms-full.txt", "search-index.json",
|
|
"assets/site.css", docsite.MarkerFile,
|
|
"404.html", "assets/site.js", "assets/search.js", "assets/theme-init.js",
|
|
"assets/LICENSE-lucide.txt", "assets/fonts/LICENSE-dm-sans.txt", "assets/fonts/LICENSE-dm-mono.txt",
|
|
"assets/fonts/dm-sans-latin-400-normal.woff2", "assets/fonts/dm-sans-latin-600-normal.woff2",
|
|
"assets/fonts/dm-sans-latin-ext-400-normal.woff2", "assets/fonts/dm-sans-latin-ext-600-normal.woff2",
|
|
"assets/fonts/dm-sans-latin-400-italic.woff2", "assets/fonts/dm-sans-latin-ext-400-italic.woff2",
|
|
"assets/fonts/dm-mono-latin-400-normal.woff2", "assets/fonts/dm-mono-latin-ext-400-normal.woff2",
|
|
} {
|
|
if _, err := os.Stat(filepath.Join(out, name)); err != nil {
|
|
t.Errorf("missing %s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// requiredPages lists the guide pages the docs must keep. Each content plan
|
|
// appends the pages it writes.
|
|
var requiredPages = []string{
|
|
"index",
|
|
"setup/installation",
|
|
"setup/coming-from-wintercms",
|
|
"architecture/introduction",
|
|
"architecture/go-modules-and-workspaces",
|
|
"architecture/application-lifecycle",
|
|
"architecture/request-lifecycle",
|
|
"plugins/registration",
|
|
"plugins/scheduling",
|
|
"plugins/extending",
|
|
"plugins/testing",
|
|
"setup/introduction",
|
|
"setup/configuration",
|
|
"console/introduction",
|
|
"console/setup-and-maintenance",
|
|
"console/scaffolding",
|
|
"console/writing-commands",
|
|
"console/utilities",
|
|
"services/jobs",
|
|
"database/models",
|
|
"database/migrations",
|
|
"database/queries-and-pagination",
|
|
"database/relations",
|
|
"database/casts-and-validation",
|
|
"database/attachments",
|
|
"database/transactions",
|
|
"services/configuration",
|
|
"services/events",
|
|
"services/routing",
|
|
"services/rate-limiting",
|
|
"services/authentication",
|
|
"services/oauth-server",
|
|
"services/mail",
|
|
"services/localization",
|
|
"backend/admin-controllers",
|
|
"backend/forms",
|
|
"backend/lists-and-filters",
|
|
"backend/relation-manager",
|
|
"backend/users-and-permissions",
|
|
"backend/settings",
|
|
"backend/partials-and-widgets",
|
|
"backend/admin-spa",
|
|
"services/storage",
|
|
"services/outbound-http",
|
|
"services/realtime",
|
|
"services/push",
|
|
"services/search",
|
|
"services/parity-testing",
|
|
"services/frontend-and-ajax",
|
|
"setup/porting-a-plugin",
|
|
}
|
|
|
|
// sectionOrder is the D-08 sidebar order: WinterCMS's documentation order,
|
|
// with the API reference last.
|
|
var sectionOrder = []string{"setup", "architecture", "plugins", "backend", "database", "services", "console", "api"}
|
|
|
|
// TestDocsRequiredPages asserts that every required page is in the loaded
|
|
// tree and is built as both .html and .md on the real tree, and that
|
|
// site.yaml lists the sections in sectionOrder.
|
|
func TestDocsRequiredPages(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(repoRoot, "docs", "site.yaml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sections []string
|
|
for _, m := range regexp.MustCompile(`(?m)^ - name: (\S+)$`).FindAllStringSubmatch(string(raw), -1) {
|
|
sections = append(sections, m[1])
|
|
}
|
|
if !slices.Equal(sections, sectionOrder) {
|
|
t.Errorf("site.yaml sections = %v, want %v", sections, sectionOrder)
|
|
}
|
|
|
|
pages, problems, err := docsite.Pages(docsite.Options{Root: repoRoot, Commands: docsCommands()})
|
|
if err != nil || len(problems) > 0 {
|
|
t.Fatalf("Pages: %v %v", err, problems)
|
|
}
|
|
loaded := map[string]bool{}
|
|
for _, p := range pages {
|
|
loaded[p.URL] = true
|
|
}
|
|
out, _ := buildRealTree(t)
|
|
for _, url := range requiredPages {
|
|
if !loaded[url] {
|
|
t.Errorf("page %s is not in the docs tree", url)
|
|
}
|
|
for _, ext := range []string{".html", ".md"} {
|
|
if _, err := os.Stat(filepath.Join(out, filepath.FromSlash(url)+ext)); err != nil {
|
|
t.Errorf("page %s has no %s output: %v", url, ext, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// frameworkModules lists modules/<m> directories that hold a non-test Go
|
|
// file, discovered independently of docsite.
|
|
func frameworkModules(t *testing.T) []string {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(filepath.Join(repoRoot, "modules"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var names []string
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
goFiles, err := filepath.Glob(filepath.Join(repoRoot, "modules", e.Name(), "*.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slices.ContainsFunc(goFiles, func(f string) bool { return !strings.HasSuffix(f, "_test.go") }) {
|
|
names = append(names, e.Name())
|
|
}
|
|
}
|
|
if len(names) == 0 {
|
|
t.Fatal("no framework modules found")
|
|
}
|
|
return names
|
|
}
|
|
|
|
func TestEveryModuleInSidebar(t *testing.T) {
|
|
out, _ := buildRealTree(t)
|
|
index, err := os.ReadFile(filepath.Join(out, "index.html"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
start := bytes.Index(index, []byte(`<nav class="sidebar" aria-label="Documentation">`))
|
|
end := bytes.Index(index[max(start, 0):], []byte("</nav>"))
|
|
if start < 0 || end < 0 {
|
|
t.Fatal("index.html has no sidebar nav")
|
|
}
|
|
sidebar := string(index[start : start+end])
|
|
for _, m := range frameworkModules(t) {
|
|
if _, err := os.Stat(filepath.Join(out, "api", m+".html")); err != nil {
|
|
t.Errorf("module %s has no API page: %v", m, err)
|
|
}
|
|
if !strings.Contains(sidebar, `href="/api/`+m+`.html"`) {
|
|
t.Errorf("sidebar does not link api/%s.html", m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDocsAIOutputsInSync asserts that the page tree, the .html pages, the
|
|
// .md siblings, llms.txt and llms-full.txt all list the same pages in the
|
|
// same reading order.
|
|
func TestDocsAIOutputsInSync(t *testing.T) {
|
|
pages, problems, err := docsite.Pages(docsite.Options{Root: repoRoot, Commands: docsCommands()})
|
|
if err != nil || len(problems) > 0 {
|
|
t.Fatalf("Pages: %v %v", err, problems)
|
|
}
|
|
var want []string
|
|
for _, p := range pages {
|
|
want = append(want, p.URL)
|
|
}
|
|
out, _ := buildRealTree(t)
|
|
|
|
var htmlFiles, mdFiles []string
|
|
if 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/") {
|
|
return nil
|
|
}
|
|
if rel == "404.html" {
|
|
return nil
|
|
}
|
|
switch filepath.Ext(rel) {
|
|
case ".html":
|
|
htmlFiles = append(htmlFiles, strings.TrimSuffix(rel, ".html"))
|
|
case ".md":
|
|
mdFiles = append(mdFiles, strings.TrimSuffix(rel, ".md"))
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sorted := slices.Sorted(slices.Values(want))
|
|
slices.Sort(htmlFiles)
|
|
slices.Sort(mdFiles)
|
|
if !slices.Equal(htmlFiles, sorted) {
|
|
t.Errorf(".html pages = %v, want %v", htmlFiles, sorted)
|
|
}
|
|
if !slices.Equal(mdFiles, sorted) {
|
|
t.Errorf(".md pages = %v, want %v", mdFiles, sorted)
|
|
}
|
|
|
|
llms := readLines(t, filepath.Join(out, "llms.txt"))
|
|
if len(llms) == 0 || llms[0] != "# SummerCMS" {
|
|
t.Fatalf("llms.txt line 1 = %q, want # SummerCMS", first(llms))
|
|
}
|
|
if next := nextNonEmpty(llms, 1); next < 0 || !strings.HasPrefix(llms[next], "> ") {
|
|
t.Errorf("llms.txt: the line after the H1 must be a > summary")
|
|
}
|
|
item := regexp.MustCompile(`^- \[[^\]]+\]\(/([^)]+)\.md\): \S`)
|
|
var linked []string
|
|
for i, line := range llms {
|
|
if strings.HasPrefix(line, "## ") {
|
|
if next := nextNonEmpty(llms, i+1); next < 0 || !strings.HasPrefix(llms[next], "- [") {
|
|
t.Errorf("llms.txt: %q is not followed by a link list", line)
|
|
}
|
|
}
|
|
if m := item.FindStringSubmatch(line); m != nil {
|
|
linked = append(linked, m[1])
|
|
}
|
|
}
|
|
if !slices.Equal(linked, want) {
|
|
t.Errorf("llms.txt links = %v, want %v", linked, want)
|
|
}
|
|
|
|
var sources []string
|
|
for _, line := range readLines(t, filepath.Join(out, "llms-full.txt")) {
|
|
if rest, ok := strings.CutPrefix(line, "Source: /"); ok {
|
|
sources = append(sources, strings.TrimSuffix(rest, ".html"))
|
|
}
|
|
}
|
|
if !slices.Equal(sources, want) {
|
|
t.Errorf("llms-full.txt sources = %v, want %v", sources, want)
|
|
}
|
|
}
|
|
|
|
func readLines(t *testing.T, path string) []string {
|
|
t.Helper()
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
var lines []string
|
|
sc := bufio.NewScanner(f)
|
|
sc.Buffer(make([]byte, 0, 1<<20), 1<<24)
|
|
for sc.Scan() {
|
|
lines = append(lines, sc.Text())
|
|
}
|
|
if err := sc.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func nextNonEmpty(lines []string, from int) int {
|
|
for i := from; i < len(lines); i++ {
|
|
if strings.TrimSpace(lines[i]) != "" {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func first(lines []string) string {
|
|
if len(lines) == 0 {
|
|
return ""
|
|
}
|
|
return lines[0]
|
|
}
|
|
|
|
func TestDocsCommandNames(t *testing.T) {
|
|
cmds := docsCommands()
|
|
for _, want := range []string{"docs:build", "make:plugin", "migrate:status"} {
|
|
if !slices.Contains(cmds.Tool, want) {
|
|
t.Errorf("Tool is missing %s: %v", want, cmds.Tool)
|
|
}
|
|
}
|
|
for _, want := range []string{"key:generate", "route:list", "admin:create", "queue:clear", "websockets:health"} {
|
|
if !slices.Contains(cmds.App, want) {
|
|
t.Errorf("App is missing %s: %v", want, cmds.App)
|
|
}
|
|
}
|
|
}
|
|
|
|
// generatedConstructor matches a command constructor the generated app main
|
|
// appends to its command list.
|
|
var generatedConstructor = regexp.MustCompile(`(?:commands :=|append\(commands,)\s*([a-z]+)\.([A-Z][A-Za-z0-9]*)\(app\b`)
|
|
|
|
// TestDocsCommandsMirrorGeneratedMain keeps docsCommands in step with the
|
|
// application main internal/build generates: every command constructor
|
|
// written there must also be called in docs.go.
|
|
func TestDocsCommandsMirrorGeneratedMain(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
buildFile, err := parser.ParseFile(fset, filepath.Join(repoRoot, "internal", "build", "build.go"), nil, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var generated []string
|
|
ast.Inspect(buildFile, func(n ast.Node) bool {
|
|
lit, ok := n.(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
return true
|
|
}
|
|
v, err := strconv.Unquote(lit.Value)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
for _, m := range generatedConstructor.FindAllStringSubmatch(v, -1) {
|
|
generated = append(generated, m[1]+"."+m[2])
|
|
}
|
|
return true
|
|
})
|
|
if len(generated) < 5 {
|
|
t.Fatalf("found %d constructors in internal/build/build.go (%v), want at least 5", len(generated), generated)
|
|
}
|
|
|
|
docsFile, err := parser.ParseFile(fset, "docs.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var called []string
|
|
ast.Inspect(docsFile, func(n ast.Node) bool {
|
|
fn, ok := n.(*ast.FuncDecl)
|
|
if !ok || fn.Name.Name != "docsCommands" {
|
|
return true
|
|
}
|
|
ast.Inspect(fn, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
if pkg, ok := sel.X.(*ast.Ident); ok {
|
|
called = append(called, pkg.Name+"."+sel.Sel.Name)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return false
|
|
})
|
|
for _, c := range generated {
|
|
if !slices.Contains(called, c) {
|
|
t.Errorf("the generated main calls %s but docsCommands does not", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// cleanDocsFixture is the internal/docsite fixture that passes every check.
|
|
const cleanDocsFixture = "../../internal/docsite/testdata/clean"
|
|
|
|
// scratchDocsRoot copies the clean docs fixture into a temp dir.
|
|
func scratchDocsRoot(t *testing.T) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
err := filepath.WalkDir(cleanDocsFixture, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, _ := filepath.Rel(cleanDocsFixture, p)
|
|
target := filepath.Join(root, rel)
|
|
if d.IsDir() {
|
|
return os.MkdirAll(target, 0o755)
|
|
}
|
|
data, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(target, data, 0o644)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
// runSummer runs the summer tool with args and returns its output.
|
|
func runSummer(t *testing.T, args ...string) (string, error) {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
root, err := bonfire.NewRoot("summer", toolCommands(), &buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root.SetArgs(args)
|
|
err = root.Execute()
|
|
return buf.String(), err
|
|
}
|
|
|
|
func appendDoc(t *testing.T, root, name, text string) {
|
|
t.Helper()
|
|
f, err := os.OpenFile(filepath.Join(root, filepath.FromSlash(name)), os.O_APPEND|os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
if _, err := f.WriteString(text); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestDocsBuildCheckOutput(t *testing.T) {
|
|
root := scratchDocsRoot(t)
|
|
out, err := runSummer(t, "docs:build", "--check", "--root", root)
|
|
if err != nil || out != "docs:build: no problems found\n" {
|
|
t.Fatalf("docs:build --check on the clean fixture = %q, %v", out, err)
|
|
}
|
|
|
|
appendDoc(t, root, "docs/extras/faq.md", "\nSee `demo.Missing` and `summer no:such`.\n")
|
|
site := filepath.Join(t.TempDir(), "site")
|
|
for _, args := range [][]string{
|
|
{"docs:build", "--check", "--root", root},
|
|
{"docs:build", "--root", root, "--out", site},
|
|
} {
|
|
out, err := runSummer(t, args...)
|
|
if err == nil || err.Error() != "docs:build failed" {
|
|
t.Fatalf("%v = %v, want docs:build failed", args, err)
|
|
}
|
|
want := "docs/extras/faq.md:17: identifier: demo.Missing does not exist in modules/demo\n" +
|
|
"docs/extras/faq.md:17: command: \"no:such\" is not a summer or application command\n" +
|
|
"docs:build: 2 problems, nothing written\n"
|
|
if out != want {
|
|
t.Fatalf("%v output =\n%s\nwant\n%s", args, out, want)
|
|
}
|
|
}
|
|
if _, err := os.Stat(site); err == nil {
|
|
t.Fatal("docs:build wrote output despite problems")
|
|
}
|
|
|
|
// A refused --out is an error without problem lines.
|
|
if _, err := runSummer(t, "docs:build", "--root", root, "--out", filepath.Join(root, "docs", "site")); err == nil ||
|
|
!strings.Contains(err.Error(), "--out must not be inside --src") {
|
|
t.Fatalf("docs:build --out inside docs = %v", err)
|
|
}
|
|
// A missing site.yaml is an error for --check too.
|
|
if _, err := runSummer(t, "docs:build", "--check", "--root", t.TempDir()); err == nil || !strings.Contains(err.Error(), "read site config") {
|
|
t.Fatalf("docs:build --check without site.yaml = %v", err)
|
|
}
|
|
|
|
// --src and --base-url are honoured.
|
|
clean := scratchDocsRoot(t)
|
|
out, err = runSummer(t, "docs:build", "--root", clean, "--src", filepath.Join(clean, "docs"), "--out", site, "--base-url", "/v2/")
|
|
if err != nil || !strings.HasPrefix(out, "docs:build: wrote 6 pages to ") {
|
|
t.Fatalf("docs:build --src --base-url = %q, %v", out, err)
|
|
}
|
|
if html, err := os.ReadFile(filepath.Join(site, "index.html")); err != nil || !strings.Contains(string(html), `href="/v2/assets/site.css"`) {
|
|
t.Fatalf("--base-url not applied: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDocsSyncOutput(t *testing.T) {
|
|
root := scratchDocsRoot(t)
|
|
out, err := runSummer(t, "docs:sync", "--root", root)
|
|
if err != nil || out != "docs:sync: all snippets up to date\n" {
|
|
t.Fatalf("docs:sync on the clean fixture = %q, %v", out, err)
|
|
}
|
|
|
|
start := filepath.Join(root, "docs", "guide", "start.md")
|
|
raw, err := os.ReadFile(start)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
drifted := strings.Replace(string(raw), `demo.Hello("blog")`, `demo.Hello("stale")`, 1)
|
|
if err := os.WriteFile(start, []byte(drifted), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err = runSummer(t, "docs:sync", "--root", root)
|
|
if err != nil || out != "docs:sync: updated 1 snippets in 1 files\n" {
|
|
t.Fatalf("docs:sync with drift = %q, %v", out, err)
|
|
}
|
|
if got, _ := os.ReadFile(start); string(got) != string(raw) {
|
|
t.Fatal("docs:sync did not restore the source text")
|
|
}
|
|
|
|
appendDoc(t, root, "docs/extras/faq.md", "\n```go src=modules/demo/demo.go#Nope\n```\n")
|
|
out, err = runSummer(t, "docs:sync", "--root", root)
|
|
if err == nil || err.Error() != "docs:sync failed" {
|
|
t.Fatalf("docs:sync with a broken ref = %v", err)
|
|
}
|
|
if want := "docs/extras/faq.md:17: snippet: modules/demo/demo.go#Nope not found\ndocs:sync: 1 problems, nothing written\n"; out != want {
|
|
t.Fatalf("docs:sync output =\n%s\nwant\n%s", out, want)
|
|
}
|
|
}
|
|
|
|
func TestDocsServeRefusal(t *testing.T) {
|
|
root := scratchDocsRoot(t)
|
|
for _, addr := range []string{"0.0.0.0:8088", "[::]:8088", "192.0.2.10:8088"} {
|
|
_, err := runSummer(t, "docs:serve", "--root", root, "--addr", addr)
|
|
want := "docs:serve: refusing to listen on " + addr + ": not a loopback address. Pass --allow-remote to serve on the network."
|
|
if err == nil || err.Error() != want {
|
|
t.Errorf("docs:serve --addr %s = %v, want %q", addr, err, want)
|
|
}
|
|
}
|
|
// --allow-remote passes the address check; the planted problem then
|
|
// stops the first build before anything listens.
|
|
appendDoc(t, root, "docs/extras/faq.md", "\n`summer no:such`\n")
|
|
out, err := runSummer(t, "docs:serve", "--root", root, "--addr", "0.0.0.0:0", "--allow-remote")
|
|
if err == nil || err.Error() != "docs:serve: build failed" || !strings.Contains(out, `"no:such"`) {
|
|
t.Fatalf("docs:serve --allow-remote with a problem = %q, %v", out, err)
|
|
}
|
|
if strings.Contains(out, "Serving docs") {
|
|
t.Fatal("docs:serve listened despite a failed build")
|
|
}
|
|
}
|