feat(11.1-05): pin the walkthrough to the scaffolder and link it from the concept map
- TestScaffoldLayout runs make:plugin, make:model, make:migration, make:admin-controller and make:command for acme.blog in a copy of examples/hello and compares the file set with docs/examples/blog - the page lists the exact make commands, the go.mod a scaffolded plugin gets, what the scaffolder leaves to the developer and a checklist - scaffolding.md no longer claims same-second migrations get consecutive timestamps; only same-name ones do - coming-from-wintercms.md and index.md link the walkthrough
This commit is contained in:
143
docs/examples/blog/scaffold_layout_test.go
Normal file
143
docs/examples/blog/scaffold_layout_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package blog_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/build"
|
||||
)
|
||||
|
||||
// TestScaffoldLayout pins the walkthrough to the scaffolder: running the
|
||||
// make commands the porting page lists for acme.blog, in its order, in a
|
||||
// copy of the examples/hello application, must produce exactly the files of
|
||||
// this plugin (without its tests, and with migration timestamps normalised). The
|
||||
// in-repository copy has no go.mod or go.sum because it lives in the
|
||||
// framework module; those two are left out of the scaffolder's set.
|
||||
func TestScaffoldLayout(t *testing.T) {
|
||||
// The make commands run go mod tidy; keep it on the module cache.
|
||||
t.Setenv("GOPROXY", "off")
|
||||
root, err := filepath.Abs(filepath.Join("..", "..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := copyApp(t, filepath.Join(root, "examples", "hello"), root)
|
||||
|
||||
ctx := t.Context()
|
||||
if _, err := build.MakePlugin(ctx, app, "acme.blog"); err != nil {
|
||||
t.Fatalf("MakePlugin: %v", err)
|
||||
}
|
||||
if _, err := build.MakeModel(ctx, app, "acme.blog", "Post", false); err != nil {
|
||||
t.Fatalf("MakeModel: %v", err)
|
||||
}
|
||||
if _, err := build.MakeMigration(ctx, app, "acme.blog", "AddPublishedAt"); err != nil {
|
||||
t.Fatalf("MakeMigration: %v", err)
|
||||
}
|
||||
if _, err := build.MakeAdminController(ctx, app, "acme.blog", "Posts"); err != nil {
|
||||
t.Fatalf("MakeAdminController: %v", err)
|
||||
}
|
||||
if _, err := build.MakeCommand(ctx, app, "acme.blog", "Publish"); err != nil {
|
||||
t.Fatalf("MakeCommand: %v", err)
|
||||
}
|
||||
|
||||
scaffolded := fileSet(t, filepath.Join(app, "plugins", "blog"), func(rel string) bool {
|
||||
return rel != "go.mod" && rel != "go.sum"
|
||||
})
|
||||
walkthrough := fileSet(t, ".", func(rel string) bool {
|
||||
return !strings.HasSuffix(rel, "_test.go")
|
||||
})
|
||||
var missing, extra []string
|
||||
for _, f := range scaffolded {
|
||||
if !slices.Contains(walkthrough, f) {
|
||||
missing = append(missing, f)
|
||||
}
|
||||
}
|
||||
for _, f := range walkthrough {
|
||||
if !slices.Contains(scaffolded, f) {
|
||||
extra = append(extra, f)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 || len(extra) > 0 {
|
||||
t.Errorf("docs/examples/blog differs from the scaffolder output\nscaffolded but missing here: %v\nhere but not scaffolded: %v", missing, extra)
|
||||
}
|
||||
}
|
||||
|
||||
var migrationStamp = regexp.MustCompile(`^updates/\d{14}_`)
|
||||
|
||||
// fileSet returns the sorted relative paths of the files under dir that keep
|
||||
// accepts, with a leading 14-digit migration timestamp replaced by a token.
|
||||
func fileSet(t *testing.T, dir string, keep func(rel string) bool) []string {
|
||||
t.Helper()
|
||||
var out []string
|
||||
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(dir, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if keep(rel) {
|
||||
out = append(out, migrationStamp.ReplaceAllString(rel, "updates/TIMESTAMP_"))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// copyApp copies the application at src into a temporary directory, without
|
||||
// its bin/ directory, and points every go.mod replace of the framework
|
||||
// module at framework.
|
||||
func copyApp(t *testing.T, src, framework string) string {
|
||||
t.Helper()
|
||||
const module = "git.golem15.com/golem15/summercms"
|
||||
dst := t.TempDir()
|
||||
err := filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if rel == "bin" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return os.MkdirAll(filepath.Join(dst, rel), 0o755)
|
||||
}
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Name() == "go.mod" {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "replace "))
|
||||
if strings.HasPrefix(trimmed, module+" =>") {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
prefix := ""
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "replace ") {
|
||||
prefix = "replace "
|
||||
}
|
||||
lines[i] = indent + prefix + module + " => " + framework
|
||||
}
|
||||
}
|
||||
data = []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
return os.WriteFile(filepath.Join(dst, rel), data, 0o644)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
Reference in New Issue
Block a user