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 }