- 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
144 lines
4.2 KiB
Go
144 lines
4.2 KiB
Go
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
|
|
}
|