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) } }