Files
summercms/internal/build/plugininfo_smoke_test.go
Jakub Zych e8d3d724f5 feat(build): bake plugin metadata and the framework version into plugins.gen.go
- party.Info, party.Installed, party.Describe (additive; Plugin and Activate unchanged)
- summer build resolves plugin dirs with one go list, reads plugin.yaml, versions per D-07 (module version, git describe without --dirty, none for in-app plugins)
- build.EffectiveFramework: newest tag of a locally replaced or used framework checkout, else the go.mod require
- generated main calls party.Describe(app, PluginInfo, FrameworkVersion) before Activate; metadata failures warn and fall back to IDs
- summer new goldens and examples/hello regenerated
2026-10-29 17:12:13 +01:00

243 lines
9.8 KiB
Go

package build
import (
"bytes"
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/party"
)
// TestPluginInfoSmoke covers the build-time plugin metadata (D-06, D-07):
// generation, plugin directory resolution, versions, the effective
// framework version, the fallback and the runtime publication.
func TestPluginInfoSmoke(t *testing.T) {
m := Manifest{
Module: "example.com/app",
Binary: "app",
Plugins: []Plugin{
{ID: "acme.blog", Module: "example.com/sm-blog-plugin"},
{ID: "acme.local", Module: "example.com/app/plugins/acme/local"},
},
}
t.Run("generate", func(t *testing.T) {
meta := buildMeta{
Framework: "v0.6.0",
Plugins: []party.Info{
{ID: "acme.blog", Module: "example.com/sm-blog-plugin", Package: "example.com/sm-blog-plugin",
Name: "Blog", Description: "Posts with \"quotes\",\nnewlines and `ticks`.", Version: "v0.2.5", Source: party.SourceModule},
{ID: "acme.local", Package: "example.com/app/plugins/acme/local", Source: party.SourceLocal},
},
}
first, err := generatePluginsGen(m, meta)
if err != nil {
t.Fatal(err)
}
second, err := generatePluginsGen(m, meta)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(first, second) {
t.Fatal("plugins.gen.go is not byte-stable")
}
src := string(first)
for _, want := range []string{
`"git.golem15.com/golem15/summercms/modules/party"`,
"var PluginInfo = []party.Info{",
`{ID: "acme.blog", Module: "example.com/sm-blog-plugin", Package: "example.com/sm-blog-plugin", Name: "Blog", Description: "Posts with \"quotes\",\nnewlines and ` + "`ticks`" + `.", Version: "v0.2.5", Source: party.SourceModule},`,
`{ID: "acme.local", Package: "example.com/app/plugins/acme/local", Source: party.SourceLocal},`,
`const FrameworkVersion = "v0.6.0"`,
} {
if !strings.Contains(src, want) {
t.Errorf("plugins.gen.go lacks %s\n%s", want, src)
}
}
if strings.Index(src, `ID: "acme.blog"`) > strings.Index(src, `ID: "acme.local"`) {
t.Error("PluginInfo is not in summer.yaml order")
}
if strings.Index(src, "var PluginIDs") > strings.Index(src, "var PluginInfo") {
t.Error("PluginInfo comes before PluginIDs")
}
// Missing metadata degrades to ID-only entries.
idOnly, err := generatePluginsGen(m, buildMeta{})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(idOnly), `{ID: "acme.blog"},`) || !strings.Contains(string(idOnly), `const FrameworkVersion = ""`) {
t.Errorf("ID-only plugins.gen.go:\n%s", idOnly)
}
})
t.Run("main", func(t *testing.T) {
src, err := generateMain(m)
if err != nil {
t.Fatal(err)
}
describe := strings.Index(string(src), "if err := party.Describe(app, PluginInfo, FrameworkVersion); err != nil {")
activate := strings.Index(string(src), "party.Activate(app, PluginIDs)")
if describe < 0 || activate < 0 || describe > activate {
t.Fatalf("Describe must come before Activate:\n%s", src)
}
})
t.Run("dirs", func(t *testing.T) {
requireGit(t)
app := t.TempDir()
write(t, filepath.Join(app, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\n"+
"require (\n\texample.test/acme/inapp v0.0.0\n\texample.test/acme/sub v0.0.0\n)\n\n"+
"replace example.test/acme/inapp => ./plugins/acme/inapp\n\nreplace example.test/acme/sub => ./plugins/acme/sub\n")
write(t, filepath.Join(app, "main.go"), "package main\n\nimport (\n\t_ \"example.test/acme/inapp\"\n\t_ \"example.test/acme/sub\"\n)\n\nfunc main() {}\n")
for _, name := range []string{"inapp", "sub"} {
dir := filepath.Join(app, "plugins", "acme", name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
write(t, filepath.Join(dir, "go.mod"), "module example.test/acme/"+name+"\n\ngo 1.27.0\n")
write(t, filepath.Join(dir, "plugin.go"), "package "+name+"\n")
}
inapp := filepath.Join(app, "plugins", "acme", "inapp")
sub := filepath.Join(app, "plugins", "acme", "sub")
write(t, filepath.Join(inapp, "plugin.yaml"), "schema: [not, a, manifest\n")
write(t, filepath.Join(sub, "plugin.yaml"), "# Generated by summer plugin:manifest. Do not edit; rerun the command.\n"+
"schema: 1\nid: acme.sub\nmodule: example.test/acme/sub\npackage: example.test/acme/sub\n"+
"name: Sub\ndescription: A plugin in its own repository.\nframework: v0.6.0\n")
runTestGit(t, app, "init", "-q")
runTestGit(t, sub, "init", "-q")
runTestGit(t, sub, "add", ".")
runTestGit(t, sub, "commit", "-q", "-m", "first")
runTestGit(t, sub, "tag", "v0.1.0")
write(t, filepath.Join(sub, "extra.go"), "package sub\n")
runTestGit(t, sub, "add", ".")
runTestGit(t, sub, "commit", "-q", "-m", "second")
// Uncommitted change: the version must not carry -dirty.
write(t, filepath.Join(sub, "plugin.go"), "package sub\n\n// changed\n")
manifest := Manifest{Module: "example.test/app", Binary: "app", Plugins: []Plugin{
{ID: "acme.inapp", Module: "example.test/acme/inapp"},
{ID: "acme.sub", Module: "example.test/acme/sub"},
}}
infos, warnings := resolvePluginInfo(t.Context(), app, manifest)
if len(infos) != 2 {
t.Fatalf("infos = %+v", infos)
}
in, sb := infos[0], infos[1]
if in.ID != "acme.inapp" || in.Source != party.SourceLocal || in.Version != "" || in.Name != "" ||
in.Module != "example.test/acme/inapp" || in.Package != "example.test/acme/inapp" {
t.Errorf("in-app plugin = %+v", in)
}
if sb.ID != "acme.sub" || sb.Source != party.SourceSubmodule || !strings.HasPrefix(sb.Version, "v0.1.0-1-g") ||
strings.Contains(sb.Version, "dirty") || sb.Name != "Sub" || sb.Description != "A plugin in its own repository." {
t.Errorf("own-repository plugin = %+v", sb)
}
if len(warnings) != 1 || !strings.Contains(warnings[0], "plugin.yaml") {
t.Errorf("warnings = %q, want one about the malformed plugin.yaml", warnings)
}
for _, info := range infos {
for _, s := range []string{info.Module, info.Package, info.Version, info.Name, info.Description} {
if strings.Contains(s, app) {
t.Errorf("a filesystem path leaked into %+v", info)
}
}
}
})
t.Run("framework", func(t *testing.T) {
requireGit(t)
ctx := t.Context()
fw := t.TempDir()
write(t, filepath.Join(fw, "go.mod"), "module "+frameworkModule+"\n\ngo 1.27.0\n")
runTestGit(t, fw, "init", "-q")
runTestGit(t, fw, "add", ".")
runTestGit(t, fw, "commit", "-q", "-m", "one")
runTestGit(t, fw, "tag", "v0.5.3")
write(t, filepath.Join(fw, "a.go"), "package summercms\n")
runTestGit(t, fw, "add", ".")
runTestGit(t, fw, "commit", "-q", "-m", "two")
runTestGit(t, fw, "tag", "v0.6.0")
write(t, filepath.Join(fw, "b.go"), "package summercms\n")
runTestGit(t, fw, "add", ".")
runTestGit(t, fw, "commit", "-q", "-m", "three")
replaced := t.TempDir()
write(t, filepath.Join(replaced, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.0.0\n\nreplace "+frameworkModule+" => "+fw+"\n")
if v, err := EffectiveFramework(ctx, replaced); err != nil || v != "v0.6.0" {
t.Errorf("replaced framework = %q, %v; want v0.6.0", v, err)
}
required := t.TempDir()
write(t, filepath.Join(required, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.5.3\n")
if v, err := EffectiveFramework(ctx, required); err != nil || v != "v0.5.3" {
t.Errorf("required framework = %q, %v; want v0.5.3", v, err)
}
notGit := t.TempDir()
noRepo := t.TempDir()
write(t, filepath.Join(noRepo, "go.mod"), "module "+frameworkModule+"\n\ngo 1.27.0\n")
write(t, filepath.Join(notGit, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.5.1\n\nreplace "+frameworkModule+" => "+noRepo+"\n")
if v, err := EffectiveFramework(ctx, notGit); err == nil || v != "v0.5.1" {
t.Errorf("framework without git = %q, %v; want v0.5.1 and a warning", v, err)
}
})
t.Run("fallback", func(t *testing.T) {
dir := copyHelloApp(t)
orig := listPackages
listPackages = func(context.Context, string, []string) ([]listedPackage, error) {
return nil, errors.New("go list failed")
}
t.Cleanup(func() { listPackages = orig })
var out bytes.Buffer
if err := App(t.Context(), dir, &out); err != nil {
t.Fatalf("build with failing metadata: %v\n%s", err, out.String())
}
if n := strings.Count(out.String(), "warning:"); n != 1 {
t.Errorf("warnings = %d, want 1:\n%s", n, out.String())
}
gen, err := os.ReadFile(filepath.Join(dir, "plugins.gen.go"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(gen), `{ID: "golem15.hello"},`) || !strings.Contains(string(gen), "const FrameworkVersion = ") {
t.Errorf("plugins.gen.go after a metadata failure:\n%s", gen)
}
})
t.Run("describe", func(t *testing.T) {
app := backpack.New(&compass.Config{})
plugins := []party.Info{{ID: "acme.blog", Name: "Blog", Version: "v0.2.5", Source: party.SourceModule}}
if err := party.Describe(app, plugins, "v0.6.0"); err != nil {
t.Fatal(err)
}
got, ok := app.Lookup[party.Installed]()
if !ok || got.Framework != "v0.6.0" || len(got.Plugins) != 1 || got.Plugins[0] != plugins[0] {
t.Fatalf("Installed = %+v, %v", got, ok)
}
if err := party.Describe(nil, plugins, "v0.6.0"); err == nil {
t.Fatal("Describe(nil app) succeeded")
}
})
}
// git runs git in dir with a test-local identity and no signing.
func runTestGit(t *testing.T, dir string, args ...string) {
t.Helper()
full := append([]string{"-c", "user.name=Test", "-c", "user.email=test@example.test",
"-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", "-c", "init.defaultBranch=master"}, args...)
cmd := exec.CommandContext(t.Context(), "git", full...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
}