- TestPluginDirs and TestEffectiveFramework over go list JSON fixtures and git checkouts; TestResolveInstall against an in-memory registry; TestAppendManifestPlugins for the in-place summer.yaml edit - fix: a package of the application's own module was reported as a submodule with the application's git describe; it is in-app (local) - fix: summer.yaml items written as '- id:' keep their spacing when plugin:add or plugin:install appends to them
304 lines
13 KiB
Go
304 lines
13 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
// fakeListPackages replaces listPackages with a decoder of the given
|
|
// `go list -json` stream for the test.
|
|
func fakeListPackages(t *testing.T, stream string) *[]string {
|
|
t.Helper()
|
|
var asked []string
|
|
orig := listPackages
|
|
listPackages = func(_ context.Context, _ string, paths []string) ([]listedPackage, error) {
|
|
asked = append(asked, paths...)
|
|
var out []listedPackage
|
|
dec := json.NewDecoder(strings.NewReader(stream))
|
|
for dec.More() {
|
|
var p listedPackage
|
|
if err := dec.Decode(&p); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, nil
|
|
}
|
|
t.Cleanup(func() { listPackages = orig })
|
|
return &asked
|
|
}
|
|
|
|
// writeAll writes body to path, creating the parent directories.
|
|
func writeAll(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
write(t, path, body)
|
|
}
|
|
|
|
func pluginYAML(id, module, pkg, name string) string {
|
|
return "# Generated by summer plugin:manifest. Do not edit; rerun the command.\n" +
|
|
"schema: 1\nid: " + id + "\nmodule: " + module + "\npackage: " + pkg + "\n" +
|
|
"name: " + name + "\ndescription: " + name + " plugin.\nframework: v0.6.0\n"
|
|
}
|
|
|
|
// TestPluginDirs resolves sources and versions from go list output (as
|
|
// JSON fixtures) and from git for plugin directories that are their own
|
|
// repositories.
|
|
func TestPluginDirs(t *testing.T) {
|
|
requireGit(t)
|
|
ctx := t.Context()
|
|
root := t.TempDir()
|
|
dir := func(rel string) string { return filepath.Join(root, filepath.FromSlash(rel)) }
|
|
|
|
// A plugin from the module cache with a plugin.yaml.
|
|
writeAll(t, filepath.Join(dir("cache/user"), "plugin.yaml"), pluginYAML("acme.user", "example.test/sm-user-plugin", "example.test/sm-user-plugin", "User"))
|
|
// A nested plugin package of a cached module.
|
|
writeAll(t, filepath.Join(dir("cache/pg/subs"), "plugin.yaml"), pluginYAML("acme.subs", "example.test/sm-pg-plugin", "example.test/sm-pg-plugin/subs", "Subs"))
|
|
// A submodule (own repository) tagged v0.2.0 and one commit after.
|
|
sub := dir("app/plugins/acme/sub")
|
|
writeAll(t, filepath.Join(sub, "go.mod"), "module example.test/acme/sub\n\ngo 1.27.0\n")
|
|
writeAll(t, filepath.Join(sub, "plugin.yaml"), pluginYAML("acme.sub", "example.test/acme/sub", "example.test/acme/sub", "Sub"))
|
|
runTestGit(t, dir("app"), "init", "-q")
|
|
runTestGit(t, sub, "init", "-q")
|
|
runTestGit(t, sub, "add", ".")
|
|
runTestGit(t, sub, "commit", "-q", "-m", "one")
|
|
runTestGit(t, sub, "tag", "v0.2.0")
|
|
writeAll(t, filepath.Join(sub, "a.go"), "package sub\n")
|
|
runTestGit(t, sub, "add", ".")
|
|
runTestGit(t, sub, "commit", "-q", "-m", "two")
|
|
writeAll(t, filepath.Join(sub, "a.go"), "package sub\n\n// dirty\n")
|
|
// An untagged own repository: describe falls back to the commit.
|
|
untagged := dir("app/plugins/acme/untagged")
|
|
writeAll(t, filepath.Join(untagged, "go.mod"), "module example.test/acme/untagged\n\ngo 1.27.0\n")
|
|
runTestGit(t, untagged, "init", "-q")
|
|
runTestGit(t, untagged, "add", ".")
|
|
runTestGit(t, untagged, "commit", "-q", "-m", "one")
|
|
// A directory inside the app repository (not its own top level), with
|
|
// a plugin.yaml for another ID.
|
|
inapp := dir("app/plugins/acme/inapp")
|
|
writeAll(t, filepath.Join(inapp, "plugin.yaml"), pluginYAML("acme.other", "example.test/app", "example.test/app/plugins/acme/inapp", "Other"))
|
|
|
|
stream := `{"ImportPath":"example.test/sm-user-plugin","Dir":"` + dir("cache/user") + `","Module":{"Path":"example.test/sm-user-plugin","Version":"v0.1.2","Dir":"` + dir("cache/user") + `"}}
|
|
{"ImportPath":"example.test/sm-pg-plugin/subs","Dir":"` + dir("cache/pg/subs") + `","Module":{"Path":"example.test/sm-pg-plugin","Version":"v0.3.0","Dir":"` + dir("cache/pg") + `"}}
|
|
{"ImportPath":"example.test/replaced","Dir":"` + dir("cache/user") + `","Module":{"Path":"example.test/replaced","Version":"v1.0.0","Replace":{"Path":"example.test/fork","Version":"v1.0.1"}}}
|
|
{"ImportPath":"example.test/acme/sub","Dir":"` + sub + `","Module":{"Path":"example.test/acme/sub","Version":"v0.0.0","Dir":"` + sub + `","Replace":{"Path":"./plugins/acme/sub","Dir":"` + sub + `"}}}
|
|
{"ImportPath":"example.test/acme/untagged","Dir":"` + untagged + `","Module":{"Path":"example.test/acme/untagged","Main":true,"Dir":"` + untagged + `"}}
|
|
{"ImportPath":"example.test/app/plugins/acme/inapp","Dir":"` + inapp + `","Module":{"Path":"example.test/app","Main":true,"Dir":"` + dir("app") + `"}}
|
|
{"ImportPath":"example.test/gopath","Dir":"` + inapp + `"}
|
|
{"ImportPath":"example.test/broken","Error":{"Err":"cannot find module providing package example.test/broken"}}
|
|
`
|
|
asked := fakeListPackages(t, stream)
|
|
manifest := Manifest{Module: "example.test/app", Binary: "app", Plugins: []Plugin{
|
|
{ID: "acme.user", Module: "example.test/sm-user-plugin"},
|
|
{ID: "acme.subs", Module: "example.test/sm-pg-plugin/subs"},
|
|
{ID: "acme.replaced", Module: "example.test/replaced"},
|
|
{ID: "acme.sub", Module: "example.test/acme/sub"},
|
|
{ID: "acme.untagged", Module: "example.test/acme/untagged"},
|
|
{ID: "acme.inapp", Module: "example.test/app/plugins/acme/inapp"},
|
|
{ID: "acme.gopath", Module: "example.test/gopath"},
|
|
{ID: "acme.broken", Module: "example.test/broken"},
|
|
{ID: "acme.absent", Module: "example.test/absent"},
|
|
}}
|
|
infos, warnings := resolvePluginInfo(ctx, dir("app"), manifest)
|
|
if len(*asked) != len(manifest.Plugins) {
|
|
t.Errorf("go list asked for %v", *asked)
|
|
}
|
|
if len(infos) != len(manifest.Plugins) {
|
|
t.Fatalf("infos = %+v", infos)
|
|
}
|
|
byID := map[string]party.Info{}
|
|
for i, info := range infos {
|
|
if info.ID != manifest.Plugins[i].ID {
|
|
t.Errorf("info %d is %s, want manifest order", i, info.ID)
|
|
}
|
|
byID[info.ID] = info
|
|
}
|
|
check := func(id, module, pkg, name, version, source string) {
|
|
t.Helper()
|
|
got := byID[id]
|
|
if got.Module != module || got.Package != pkg || got.Name != name || got.Version != version || got.Source != source {
|
|
t.Errorf("%s = %+v, want module %q package %q name %q version %q source %q", id, got, module, pkg, name, version, source)
|
|
}
|
|
}
|
|
check("acme.user", "example.test/sm-user-plugin", "example.test/sm-user-plugin", "User", "v0.1.2", party.SourceModule)
|
|
check("acme.subs", "example.test/sm-pg-plugin", "example.test/sm-pg-plugin/subs", "Subs", "v0.3.0", party.SourceModule)
|
|
check("acme.replaced", "example.test/replaced", "example.test/replaced", "", "v1.0.1", party.SourceModule)
|
|
check("acme.inapp", "example.test/app", "example.test/app/plugins/acme/inapp", "", "", party.SourceLocal)
|
|
check("acme.gopath", "", "example.test/gopath", "", "", party.SourceLocal)
|
|
check("acme.broken", "", "", "", "", "")
|
|
check("acme.absent", "", "", "", "", "")
|
|
|
|
s := byID["acme.sub"]
|
|
if s.Source != party.SourceSubmodule || !strings.HasPrefix(s.Version, "v0.2.0-1-g") || strings.Contains(s.Version, "dirty") || s.Name != "Sub" {
|
|
t.Errorf("submodule = %+v, want git describe v0.2.0-1-g… without -dirty", s)
|
|
}
|
|
u := byID["acme.untagged"]
|
|
if u.Source != party.SourceSubmodule || len(u.Version) < 7 || strings.HasPrefix(u.Version, "v") {
|
|
t.Errorf("untagged own repository = %+v, want the abbreviated commit", u)
|
|
}
|
|
|
|
joined := strings.Join(warnings, "\n")
|
|
for _, want := range []string{
|
|
"acme.inapp: plugin.yaml describes acme.other, not acme.inapp; ignored",
|
|
"acme.broken: cannot find module providing package example.test/broken",
|
|
"acme.absent: package not found",
|
|
"acme.replaced: plugin.yaml describes acme.user, not acme.replaced; ignored",
|
|
"acme.gopath: plugin.yaml describes acme.other, not acme.gopath; ignored",
|
|
} {
|
|
if !strings.Contains(joined, want) {
|
|
t.Errorf("warnings = %q, want %q", warnings, want)
|
|
}
|
|
}
|
|
if len(warnings) != 5 {
|
|
t.Errorf("warnings = %q, want 5", warnings)
|
|
}
|
|
for _, info := range infos {
|
|
for _, v := range []string{info.Module, info.Package, info.Version, info.Name, info.Description} {
|
|
if strings.Contains(v, root) {
|
|
t.Errorf("a filesystem path leaked into %+v", info)
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Run("go list failure keeps the IDs", func(t *testing.T) {
|
|
orig := listPackages
|
|
listPackages = func(context.Context, string, []string) ([]listedPackage, error) {
|
|
return nil, os.ErrPermission
|
|
}
|
|
t.Cleanup(func() { listPackages = orig })
|
|
infos, warnings := resolvePluginInfo(ctx, dir("app"), manifest)
|
|
if len(infos) != len(manifest.Plugins) || infos[0].ID != "acme.user" || infos[0].Version != "" || len(warnings) != 1 ||
|
|
!strings.Contains(warnings[0], "IDs only") {
|
|
t.Errorf("infos %+v, warnings %q", infos, warnings)
|
|
}
|
|
})
|
|
|
|
t.Run("no plugins", func(t *testing.T) {
|
|
infos, warnings := resolvePluginInfo(ctx, dir("app"), Manifest{})
|
|
if len(infos) != 0 || len(warnings) != 0 {
|
|
t.Errorf("infos %+v, warnings %q", infos, warnings)
|
|
}
|
|
})
|
|
|
|
t.Run("own repository", func(t *testing.T) {
|
|
if !isOwnRepository(ctx, sub) {
|
|
t.Error("a submodule-like checkout is not its own repository")
|
|
}
|
|
if isOwnRepository(ctx, inapp) {
|
|
t.Error("a directory inside the app repository counts as its own repository")
|
|
}
|
|
if isOwnRepository(ctx, t.TempDir()) {
|
|
t.Error("a directory outside any repository counts as its own repository")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestEffectiveFramework covers where the framework version comes from:
|
|
// go.mod require, go.mod replace (version or directory), go.work use and
|
|
// replace, and the warning when git cannot describe the checkout.
|
|
func TestEffectiveFramework(t *testing.T) {
|
|
requireGit(t)
|
|
ctx := t.Context()
|
|
|
|
tagged := func(tags ...string) string {
|
|
t.Helper()
|
|
fw := t.TempDir()
|
|
writeAll(t, filepath.Join(fw, "go.mod"), "module "+frameworkModule+"\n\ngo 1.27.0\n")
|
|
runTestGit(t, fw, "init", "-q")
|
|
for i, tag := range tags {
|
|
writeAll(t, filepath.Join(fw, "f.go"), "package summercms\n\n// "+tag+"\n")
|
|
runTestGit(t, fw, "add", ".")
|
|
runTestGit(t, fw, "commit", "-q", "-m", "c"+string(rune('0'+i)))
|
|
runTestGit(t, fw, "tag", tag)
|
|
}
|
|
return fw
|
|
}
|
|
fwOld := tagged("v0.5.3")
|
|
fwNew := tagged("v0.5.3", "v0.6.0")
|
|
|
|
app := func(gomod, gowork string) string {
|
|
t.Helper()
|
|
parent := t.TempDir()
|
|
dir := filepath.Join(parent, "app")
|
|
writeAll(t, filepath.Join(dir, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\n"+gomod)
|
|
if gowork != "" {
|
|
writeAll(t, filepath.Join(dir, "go.work"), "go 1.27.0\n\nuse .\n\n"+gowork)
|
|
}
|
|
return dir
|
|
}
|
|
req := "require " + frameworkModule + " v0.5.0\n"
|
|
|
|
cases := []struct {
|
|
name, gomod, gowork, want string
|
|
}{
|
|
{"require only", req, "", "v0.5.0"},
|
|
{"go.mod replace with a version", req + "\nreplace " + frameworkModule + " => example.test/fork v0.5.9\n", "", "v0.5.9"},
|
|
{"go.mod replace with a directory", req + "\nreplace " + frameworkModule + " => " + fwOld + "\n", "", "v0.5.3"},
|
|
{"go.work replace wins over go.mod", req + "\nreplace " + frameworkModule + " => " + fwOld + "\n", "replace " + frameworkModule + " => " + fwNew + "\n", "v0.6.0"},
|
|
{"go.work use of the framework", req, "use " + fwNew + "\n", "v0.6.0"},
|
|
{"go.work without the framework", req, "", "v0.5.0"},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := EffectiveFramework(ctx, app(c.gomod, c.gowork))
|
|
if err != nil || got != c.want {
|
|
t.Errorf("%s: EffectiveFramework = %q, %v; want %q", c.name, got, err, c.want)
|
|
}
|
|
}
|
|
|
|
t.Run("relative replace", func(t *testing.T) {
|
|
parent := t.TempDir()
|
|
fw := filepath.Join(parent, "framework")
|
|
if err := os.Rename(fwNew, fw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := filepath.Join(parent, "app")
|
|
writeAll(t, filepath.Join(dir, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\n"+req+"\nreplace "+frameworkModule+" => ../framework\n")
|
|
if got, err := EffectiveFramework(ctx, dir); err != nil || got != "v0.6.0" {
|
|
t.Errorf("EffectiveFramework = %q, %v", got, err)
|
|
}
|
|
})
|
|
|
|
t.Run("untagged checkout warns", func(t *testing.T) {
|
|
fw := tagged()
|
|
writeAll(t, filepath.Join(fw, "f.go"), "package summercms\n")
|
|
runTestGit(t, fw, "add", ".")
|
|
runTestGit(t, fw, "commit", "-q", "-m", "untagged")
|
|
got, err := EffectiveFramework(ctx, app(req+"\nreplace "+frameworkModule+" => "+fw+"\n", ""))
|
|
if got != "v0.5.0" || err == nil || !strings.Contains(err.Error(), "using the go.mod require v0.5.0") {
|
|
t.Errorf("EffectiveFramework = %q, %v; want the require and a warning", got, err)
|
|
}
|
|
})
|
|
|
|
t.Run("git missing", func(t *testing.T) {
|
|
goBin, err := exec.LookPath("go")
|
|
if err != nil {
|
|
t.Skip("go not found")
|
|
}
|
|
bin := t.TempDir()
|
|
if err := os.Symlink(goBin, filepath.Join(bin, "go")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := app(req+"\nreplace "+frameworkModule+" => "+fwOld+"\n", "")
|
|
t.Setenv("PATH", bin)
|
|
got, err := EffectiveFramework(ctx, dir)
|
|
if got != "v0.5.0" || err == nil || !strings.Contains(err.Error(), "git describe") {
|
|
t.Errorf("EffectiveFramework without git = %q, %v; want the require and a warning", got, err)
|
|
}
|
|
})
|
|
|
|
t.Run("no go.mod", func(t *testing.T) {
|
|
if _, err := EffectiveFramework(ctx, t.TempDir()); err == nil {
|
|
t.Error("EffectiveFramework without go.mod succeeded")
|
|
}
|
|
})
|
|
}
|