- TestCheckSiteURL (21 accepted and rejected values), TestSiteLabel and TestSiteURLPrecedence (option over site.yaml, label-without-URL and invalid option errors); new TestParseSite rows for blank and two-line labels - TestSiteLink: escaped label, section page and 404 page, exact unset header bytes - TestDocsSiteFlagsInHelp and the --site-label-without-URL error - check-phase11.2.sh --framework, --app and --site
635 lines
20 KiB
Go
635 lines
20 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDocsBuildSiteFlags checks that --site-url and --site-label reach the
|
|
// header and that an unsafe --site-url is refused.
|
|
func TestDocsBuildSiteFlags(t *testing.T) {
|
|
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, "--site-url", "/", "--site-label", "example.org"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("docs:build: %v\n%s", err, buf.String())
|
|
}
|
|
index, err := os.ReadFile(filepath.Join(out, "index.html"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{`class="site-link" href="/"`, "example.org"} {
|
|
if !bytes.Contains(index, []byte(want)) {
|
|
t.Errorf("index.html lacks %s", want)
|
|
}
|
|
}
|
|
|
|
buf.Reset()
|
|
root, err = bonfire.NewRoot("summer", toolCommands(), &buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", filepath.Join(t.TempDir(), "bad"), "--site-url", "javascript:alert(1)"})
|
|
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-url") {
|
|
t.Fatalf("docs:build --site-url javascript:alert(1): err = %v, want a --site-url error", err)
|
|
}
|
|
|
|
// The framework's own site.yaml sets no site_url, so a label alone fails.
|
|
buf.Reset()
|
|
root, err = bonfire.NewRoot("summer", toolCommands(), &buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", filepath.Join(t.TempDir(), "nolabel"), "--site-label", "x"})
|
|
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-label needs --site-url") {
|
|
t.Fatalf("docs:build --site-label x: err = %v, want a --site-label needs --site-url error", err)
|
|
}
|
|
}
|
|
|
|
// TestDocsSiteFlagsInHelp checks that docs:build and docs:serve list the
|
|
// --site-url and --site-label flags in their help.
|
|
func TestDocsSiteFlagsInHelp(t *testing.T) {
|
|
for _, cmd := range []string{"docs:build", "docs:serve"} {
|
|
var buf bytes.Buffer
|
|
root, err := bonfire.NewRoot("summer", toolCommands(), &buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root.SetArgs([]string{cmd, "--help"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("%s --help: %v", cmd, err)
|
|
}
|
|
for _, want := range []string{"--site-url", "--site-label", "overrides site.yaml site_url", "overrides site.yaml site_label"} {
|
|
if !strings.Contains(buf.String(), want) {
|
|
t.Errorf("%s help lacks %q:\n%s", cmd, want, buf.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|