test(build): plugin dirs, install resolution and summer.yaml edits
- 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
This commit is contained in:
379
internal/build/install_test.go
Normal file
379
internal/build/install_test.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/kiosk"
|
||||
)
|
||||
|
||||
// memRegistry is an in-memory Registry that counts its calls.
|
||||
type memRegistry struct {
|
||||
mu sync.Mutex
|
||||
versions map[string][]kiosk.RegistryVersion
|
||||
details map[string]kiosk.RegistryPluginDetail
|
||||
calls []string
|
||||
fail error
|
||||
}
|
||||
|
||||
func (r *memRegistry) List(context.Context, string, string) (kiosk.ListResponse, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.calls = append(r.calls, "list")
|
||||
return kiosk.ListResponse{}, r.fail
|
||||
}
|
||||
|
||||
func (r *memRegistry) Plugin(_ context.Context, id string) (kiosk.RegistryPluginDetail, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.calls = append(r.calls, "plugin "+id)
|
||||
if r.fail != nil {
|
||||
return kiosk.RegistryPluginDetail{}, r.fail
|
||||
}
|
||||
d, ok := r.details[id]
|
||||
if !ok {
|
||||
return d, fmt.Errorf("%s is %w at mem", id, kiosk.ErrNotFound)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (r *memRegistry) Versions(_ context.Context, id string) ([]kiosk.RegistryVersion, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.calls = append(r.calls, "versions "+id)
|
||||
if r.fail != nil {
|
||||
return nil, r.fail
|
||||
}
|
||||
v, ok := r.versions[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is %w at mem", id, kiosk.ErrNotFound)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// add lists id with releases given as version:framework pairs, all in
|
||||
// module example.test/<name>, requiring reqs at every release.
|
||||
func (r *memRegistry) add(id string, reqs kiosk.Requires, releases ...string) {
|
||||
mod := "example.test/" + pluginName(id)
|
||||
r.addIn(id, mod, mod, reqs, releases...)
|
||||
}
|
||||
|
||||
func (r *memRegistry) addIn(id, mod, pkg string, reqs kiosk.Requires, releases ...string) {
|
||||
if r.versions == nil {
|
||||
r.versions = map[string][]kiosk.RegistryVersion{}
|
||||
r.details = map[string]kiosk.RegistryPluginDetail{}
|
||||
}
|
||||
if _, ok := r.versions[id]; !ok {
|
||||
r.versions[id] = []kiosk.RegistryVersion{}
|
||||
}
|
||||
for _, rel := range releases {
|
||||
v, fw, _ := strings.Cut(rel, ":")
|
||||
r.versions[id] = append(r.versions[id], kiosk.RegistryVersion{Version: v, Framework: fw, Requires: reqs, Module: mod, Package: pkg})
|
||||
}
|
||||
d := kiosk.RegistryPluginDetail{}
|
||||
d.ID, d.Module, d.Package = id, mod, pkg
|
||||
d.Repository = "https://example.test/sm-" + pluginName(id) + "-plugin.git"
|
||||
r.details[id] = d
|
||||
}
|
||||
|
||||
// installApp writes a minimal application (summer.yaml and a go.mod that
|
||||
// requires the framework at framework, without a replace) and returns its
|
||||
// directory. extra are summer.yaml plugins and go.mod requires to add.
|
||||
func installApp(t *testing.T, framework string, installed map[string]string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
yaml := "# The test application.\nmodule: example.test/app\nbinary: app\nplugins:\n"
|
||||
gomod := "module example.test/app\n\ngo 1.27.0\n\nrequire " + frameworkModule + " " + framework + "\n"
|
||||
if len(installed) == 0 {
|
||||
yaml = "module: example.test/app\nbinary: app\n"
|
||||
}
|
||||
for _, id := range sortedKeys(kiosk.Requires(installed)) {
|
||||
mod := "example.test/" + pluginName(id)
|
||||
yaml += " - id: " + id + "\n module: " + mod + "\n"
|
||||
gomod += "require " + mod + " " + installed[id] + "\n"
|
||||
}
|
||||
write(t, filepath.Join(dir, manifestName), yaml)
|
||||
write(t, filepath.Join(dir, "go.mod"), gomod)
|
||||
return dir
|
||||
}
|
||||
|
||||
func planSteps(p *InstallPlan) string {
|
||||
var parts []string
|
||||
for _, s := range p.Steps {
|
||||
parts = append(parts, s.ID+"@"+s.Version)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
// TestResolveInstall pins the resolution rules of plugin:install against
|
||||
// an in-memory registry: newest compatible release, pins, framework
|
||||
// errors, requirement closure and order, installed requirements, cycles,
|
||||
// offline and disabled registries and refused registry strings.
|
||||
func TestResolveInstall(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
plan := func(app, arg string, reg Registry, mutate ...func(*InstallOptions)) (*InstallPlan, string, error) {
|
||||
t.Helper()
|
||||
var out bytes.Buffer
|
||||
opts := InstallOptions{StartDir: app, Arg: arg, Yes: true, Registry: reg, Out: &out}
|
||||
for _, m := range mutate {
|
||||
m(&opts)
|
||||
}
|
||||
p, err := PlanInstall(ctx, opts)
|
||||
return p, out.String(), err
|
||||
}
|
||||
|
||||
t.Run("newest compatible", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", nil, "v0.3.0:v0.7.0", "v0.10.0:v0.6.0", "v0.2.0:v0.6.0", "v0.1.0:v0.5.0", "v0.4.0-rc.1:v0.5.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
p, out, err := plan(app, "acme.shop", reg)
|
||||
if err != nil {
|
||||
t.Fatalf("plan: %v", err)
|
||||
}
|
||||
if got := planSteps(p); got != "acme.shop@v0.10.0" {
|
||||
t.Errorf("steps = %s", got)
|
||||
}
|
||||
if p.Framework != "v0.6.0" || p.AppDir != app || p.Steps[0].Mode != ModeModule || p.Steps[0].Module != "example.test/shop" {
|
||||
t.Errorf("plan = %+v", p)
|
||||
}
|
||||
if !strings.Contains(out, "Installing 1 plugin into "+app+" (framework v0.6.0):\n acme.shop@v0.10.0 (module)\n") {
|
||||
t.Errorf("summary:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pin", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", nil, "v0.3.0:v0.7.0", "v0.2.0:v0.6.0", "v0.1.0:v0.5.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
if p, _, err := plan(app, "acme.shop@v0.1.0", reg); err != nil || planSteps(p) != "acme.shop@v0.1.0" {
|
||||
t.Errorf("compatible pin: %v, %v", p, err)
|
||||
}
|
||||
_, _, err := plan(app, "acme.shop@v0.3.0", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.shop v0.3.0 needs framework v0.7.0 or newer; this application builds against v0.6.0") ||
|
||||
!strings.Contains(fullError(err), "go get "+frameworkModule+"@v0.7.0") {
|
||||
t.Errorf("incompatible pin: %v (%s)", err, fullError(err))
|
||||
}
|
||||
_, _, err = plan(app, "acme.shop@v0.9.0", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "no release v0.9.0; available: v0.3.0, v0.2.0, v0.1.0") {
|
||||
t.Errorf("unknown pin: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no compatible release names the minimum framework", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", nil, "v0.2.0:v0.9.0", "v0.3.0:v0.8.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
_, _, err := plan(app, "acme.shop", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.shop v0.3.0 needs framework v0.8.0 or newer") {
|
||||
t.Errorf("err = %v, want the newest release's framework named", err)
|
||||
}
|
||||
empty := &memRegistry{}
|
||||
empty.add("acme.shop", nil)
|
||||
if _, _, err := plan(app, "acme.shop", empty); err == nil || !strings.Contains(err.Error(), "has no release in the registry") {
|
||||
t.Errorf("no releases: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("requirement closure in dependency order", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", kiosk.Requires{"acme.user": "v0.1.0", "acme.payment": "v0.2.0"}, "v0.1.0:v0.6.0")
|
||||
reg.add("acme.payment", kiosk.Requires{"acme.user": "v0.1.2"}, "v0.3.0:v0.6.0", "v0.2.0:v0.6.0")
|
||||
reg.add("acme.user", nil, "v0.1.5:v0.7.0", "v0.1.3:v0.6.0", "v0.1.0:v0.5.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
p, out, err := plan(app, "acme.shop", reg)
|
||||
if err != nil {
|
||||
t.Fatalf("plan: %v", err)
|
||||
}
|
||||
if got := planSteps(p); got != "acme.user@v0.1.3,acme.payment@v0.3.0,acme.shop@v0.1.0" {
|
||||
t.Errorf("steps = %s", got)
|
||||
}
|
||||
if !strings.Contains(out, "Installing 3 plugins") {
|
||||
t.Errorf("summary:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("requirement minimum above the newest compatible release", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", kiosk.Requires{"acme.user": "v0.2.0"}, "v0.1.0:v0.6.0")
|
||||
reg.add("acme.user", nil, "v0.2.0:v0.7.0", "v0.1.9:v0.6.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
_, _, err := plan(app, "acme.shop", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.user v0.2.0 needs framework v0.7.0") {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
reg.add("acme.lonely", kiosk.Requires{"acme.user": "v0.5.0"}, "v0.1.0:v0.6.0")
|
||||
_, _, err = plan(app, "acme.lonely", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.user has no release at or above the required minimum v0.5.0") {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plugins of one module share a release", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.addIn("acme.pg", "example.test/pg", "example.test/pg", nil, "v0.2.0:v0.6.0", "v0.1.0:v0.6.0")
|
||||
reg.addIn("acme.pgsubs", "example.test/pg", "example.test/pg/subs", kiosk.Requires{"acme.pg": ""}, "v0.2.0:v0.6.0", "v0.1.0:v0.6.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
p, _, err := plan(app, "acme.pgsubs@v0.1.0", reg)
|
||||
if err != nil {
|
||||
t.Fatalf("plan: %v", err)
|
||||
}
|
||||
if got := planSteps(p); got != "acme.pg@v0.1.0,acme.pgsubs@v0.1.0" {
|
||||
t.Errorf("steps = %s, want both at the pinned module release", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("installed requirements", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", kiosk.Requires{"acme.user": "v0.1.2"}, "v0.1.0:v0.6.0")
|
||||
reg.add("acme.user", nil, "v0.1.3:v0.6.0")
|
||||
old := installApp(t, "v0.6.0", map[string]string{"acme.user": "v0.1.1"})
|
||||
_, _, err := plan(old, "acme.shop", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "acme.shop needs acme.user v0.1.2 or newer, but the application has v0.1.1") ||
|
||||
!strings.Contains(fullError(err), "plugin:update acme.user") {
|
||||
t.Errorf("too old: %v (%s)", err, fullError(err))
|
||||
}
|
||||
fresh := installApp(t, "v0.6.0", map[string]string{"acme.user": "v0.1.2"})
|
||||
p, _, err := plan(fresh, "acme.shop", reg)
|
||||
if err != nil || planSteps(p) != "acme.shop@v0.1.0" {
|
||||
t.Errorf("installed at the minimum: %v, %v", p, err)
|
||||
}
|
||||
_, _, err = plan(fresh, "acme.user", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "already in summer.yaml") || !strings.Contains(fullError(err), "plugin:update acme.user") {
|
||||
t.Errorf("installing an installed plugin: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cycle", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.a", kiosk.Requires{"acme.b": ""}, "v0.1.0:v0.6.0")
|
||||
reg.add("acme.b", kiosk.Requires{"acme.c": ""}, "v0.1.0:v0.6.0")
|
||||
reg.add("acme.c", kiosk.Requires{"acme.a": ""}, "v0.1.0:v0.6.0")
|
||||
_, _, err := plan(installApp(t, "v0.6.0", nil), "acme.a", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "dependency cycle in the registry data: acme.a -> acme.b -> acme.c -> acme.a") {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("offline and disabled", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", nil, "v0.1.0:v0.6.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
_, _, err := plan(app, "acme.shop", reg, func(o *InstallOptions) { o.Offline = true })
|
||||
if err == nil || !strings.Contains(err.Error(), "--offline makes no registry calls") || !strings.Contains(fullError(err), "plugin:install <git-url>") {
|
||||
t.Errorf("offline: %v (%s)", err, fullError(err))
|
||||
}
|
||||
if len(reg.calls) != 0 {
|
||||
t.Errorf("offline made registry calls: %v", reg.calls)
|
||||
}
|
||||
_, _, err = plan(app, "acme.shop", nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "plugins.registry_url is empty") {
|
||||
t.Errorf("disabled: %v", err)
|
||||
}
|
||||
reg.fail = errors.New("connection refused")
|
||||
_, _, err = plan(app, "acme.shop", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), "connection refused") {
|
||||
t.Errorf("registry down: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refused registry strings", func(t *testing.T) {
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
cases := map[string]struct {
|
||||
mod, pkg string
|
||||
reqs kiosk.Requires
|
||||
release string
|
||||
want string
|
||||
}{
|
||||
"module flag": {"-modfile=/tmp/x", "-modfile=/tmp/x", nil, "v0.1.0:v0.6.0", "module"},
|
||||
"module shell": {"example.test/x;rm", "example.test/x;rm", nil, "v0.1.0:v0.6.0", "module"},
|
||||
"package outside": {"example.test/x", "example.test/y", nil, "v0.1.0:v0.6.0", "not inside module"},
|
||||
"required id": {"example.test/x", "example.test/x", kiosk.Requires{"Bad ID": ""}, "v0.1.0:v0.6.0", "refused"},
|
||||
"version": {"example.test/x", "example.test/x", nil, "v0.1:v0.6.0", "has no release in the registry"},
|
||||
}
|
||||
for name, c := range cases {
|
||||
reg := &memRegistry{}
|
||||
reg.addIn("acme.x", c.mod, c.pkg, c.reqs, c.release)
|
||||
_, _, err := plan(app, "acme.x", reg)
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: err = %v, want %q", name, err, c.want)
|
||||
}
|
||||
}
|
||||
for _, arg := range []string{"", "acme", "Acme.Shop", "acme.shop@latest", "acme.shop@v1", "acme.shop@v1.0.0-rc.1", "-x.y"} {
|
||||
if _, _, err := plan(app, arg, &memRegistry{}); err == nil {
|
||||
t.Errorf("arg %q accepted", arg)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prompt", func(t *testing.T) {
|
||||
reg := &memRegistry{}
|
||||
reg.add("acme.shop", nil, "v0.1.0:v0.6.0")
|
||||
app := installApp(t, "v0.6.0", nil)
|
||||
before, _ := os.ReadFile(filepath.Join(app, manifestName))
|
||||
_, out, err := plan(app, "acme.shop", reg, func(o *InstallOptions) { o.Yes = false })
|
||||
if err == nil || !strings.Contains(err.Error(), "pass --yes") || !strings.Contains(out, "acme.shop@v0.1.0 (module)") {
|
||||
t.Errorf("non-interactive: %v\n%s", err, out)
|
||||
}
|
||||
var question string
|
||||
_, _, err = plan(app, "acme.shop", reg, func(o *InstallOptions) {
|
||||
o.Yes, o.Interactive = false, true
|
||||
o.Confirm = func(q string) (bool, error) { question = q; return false, nil }
|
||||
})
|
||||
if !errors.Is(err, ErrCancelled) || question != "Install 1 plugin?" {
|
||||
t.Errorf("declined: %v, asked %q", err, question)
|
||||
}
|
||||
_, _, err = plan(app, "acme.shop", reg, func(o *InstallOptions) {
|
||||
o.Yes, o.Interactive = false, true
|
||||
o.Confirm = func(string) (bool, error) { return false, errors.New("tty closed") }
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "tty closed") {
|
||||
t.Errorf("prompt error: %v", err)
|
||||
}
|
||||
if after, _ := os.ReadFile(filepath.Join(app, manifestName)); !bytes.Equal(before, after) {
|
||||
t.Error("planning changed summer.yaml")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not an application", func(t *testing.T) {
|
||||
if _, _, err := plan(t.TempDir(), "acme.shop", &memRegistry{}); err == nil {
|
||||
t.Error("PlanInstall outside an application succeeded")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("apply refuses an empty or tampered plan", func(t *testing.T) {
|
||||
if err := ApplyInstall(ctx, nil, nil); err == nil {
|
||||
t.Error("ApplyInstall(nil) succeeded")
|
||||
}
|
||||
bad := &InstallPlan{AppDir: t.TempDir(), Steps: []InstallStep{{ID: "acme.x", Version: "v0.1.0", Module: "-x", Package: "-x", Mode: ModeModule}}}
|
||||
if err := ApplyInstall(ctx, bad, nil); err == nil || !strings.Contains(err.Error(), "refused") {
|
||||
t.Errorf("tampered plan: %v", err)
|
||||
}
|
||||
mode := &InstallPlan{AppDir: t.TempDir(), Steps: []InstallStep{{ID: "acme.x", Version: "v0.1.0", Module: "example.test/x", Package: "example.test/x", Mode: "zip"}}}
|
||||
if err := ApplyInstall(ctx, mode, nil); err == nil || !strings.Contains(err.Error(), "unknown install mode") {
|
||||
t.Errorf("unknown mode: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAskToProceed(t *testing.T) {
|
||||
yes := func(string) (bool, error) { return true, nil }
|
||||
if err := askToProceed(true, false, nil, "q"); err != nil {
|
||||
t.Errorf("--yes: %v", err)
|
||||
}
|
||||
if err := askToProceed(false, false, yes, "q"); err == nil || !strings.Contains(err.Error(), "--yes") {
|
||||
t.Errorf("non-interactive: %v", err)
|
||||
}
|
||||
if err := askToProceed(false, true, nil, "q"); err == nil || !strings.Contains(err.Error(), "--yes") {
|
||||
t.Errorf("interactive without a prompt: %v", err)
|
||||
}
|
||||
if err := askToProceed(false, true, yes, "q"); err != nil {
|
||||
t.Errorf("accepted: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -249,10 +249,11 @@ func insertManifestPlugins(src string, plugins []Plugin) (string, error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
render := func(dash, keyIndent string) string {
|
||||
// dash is the indentation before "-", gap the spacing after it.
|
||||
render := func(dash, gap, keyIndent string) string {
|
||||
var b strings.Builder
|
||||
for _, p := range plugins {
|
||||
fmt.Fprintf(&b, "%s- id: %s\n%smodule: %s\n", dash, p.ID, keyIndent, p.Module)
|
||||
fmt.Fprintf(&b, "%s-%sid: %s\n%smodule: %s\n", dash, gap, p.ID, keyIndent, p.Module)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -261,7 +262,7 @@ func insertManifestPlugins(src string, plugins []Plugin) (string, error) {
|
||||
if out != "" && !strings.HasSuffix(out, "\n") {
|
||||
out += "\n"
|
||||
}
|
||||
return out + "plugins:\n" + render(" ", " "), nil
|
||||
return out + "plugins:\n" + render(" ", " ", " "), nil
|
||||
}
|
||||
rest := strings.TrimSpace(stripYAMLComment(strings.TrimPrefix(lines[key], "plugins:")))
|
||||
switch rest {
|
||||
@@ -272,7 +273,7 @@ func insertManifestPlugins(src string, plugins []Plugin) (string, error) {
|
||||
// The block runs until the next line that starts a top-level key.
|
||||
end := key + 1
|
||||
last := key
|
||||
dash, keyIndent := "", ""
|
||||
dash, gap, keyIndent := "", " ", ""
|
||||
for ; end < len(lines); end++ {
|
||||
l := strings.TrimRight(lines[end], "\r\n")
|
||||
trimmed := strings.TrimSpace(l)
|
||||
@@ -287,7 +288,10 @@ func insertManifestPlugins(src string, plugins []Plugin) (string, error) {
|
||||
if dash == "" && strings.HasPrefix(trimmed, "-") {
|
||||
dash = l[:len(l)-len(strings.TrimLeft(l, " \t"))]
|
||||
afterDash := strings.TrimLeft(trimmed[1:], " \t")
|
||||
keyIndent = strings.Repeat(" ", len(dash)+len(trimmed)-len(afterDash))
|
||||
if g := trimmed[1 : len(trimmed)-len(afterDash)]; g != "" && !strings.Contains(g, "\t") {
|
||||
gap = g
|
||||
}
|
||||
keyIndent = strings.Repeat(" ", len(dash)+1+len(gap))
|
||||
}
|
||||
}
|
||||
if dash == "" && keyIndent == "" {
|
||||
@@ -303,7 +307,7 @@ func insertManifestPlugins(src string, plugins []Plugin) (string, error) {
|
||||
if !strings.HasSuffix(out, "\n") {
|
||||
out += "\n"
|
||||
}
|
||||
return out + render(dash, keyIndent) + strings.Join(tail, ""), nil
|
||||
return out + render(dash, gap, keyIndent) + strings.Join(tail, ""), nil
|
||||
}
|
||||
|
||||
// stripYAMLComment drops a trailing " # comment" from a YAML value.
|
||||
|
||||
129
internal/build/manifest_append_test.go
Normal file
129
internal/build/manifest_append_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAppendManifestPlugins pins the in-place summer.yaml edit of
|
||||
// plugin:add and plugin:install: comments, blank lines and key order stay,
|
||||
// new items take the existing indentation, a missing plugins block is
|
||||
// appended, and an edit that does not parse back leaves the file as it was.
|
||||
func TestAppendManifestPlugins(t *testing.T) {
|
||||
add := []Plugin{{ID: "acme.shop", Module: "example.test/sm-shop-plugin"}, {ID: "acme.pgsubs", Module: "example.test/pg/subs"}}
|
||||
cases := []struct {
|
||||
name, src, want string
|
||||
}{
|
||||
{
|
||||
name: "comments, blank lines and later keys stay",
|
||||
src: "# The application.\nmodule: example.test/app # app module\nbinary: app\n\nplugins:\n" +
|
||||
" # Core plugins.\n - id: acme.user\n module: example.test/sm-user-plugin # pinned\n\n" +
|
||||
" - id: acme.blog\n module: example.test/sm-blog-plugin\n\n# Build tags.\ntags:\n - summercms_mcp\n",
|
||||
want: "# The application.\nmodule: example.test/app # app module\nbinary: app\n\nplugins:\n" +
|
||||
" # Core plugins.\n - id: acme.user\n module: example.test/sm-user-plugin # pinned\n\n" +
|
||||
" - id: acme.blog\n module: example.test/sm-blog-plugin\n" +
|
||||
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" +
|
||||
"\n# Build tags.\ntags:\n - summercms_mcp\n",
|
||||
},
|
||||
{
|
||||
name: "indentation is matched",
|
||||
src: "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n",
|
||||
want: "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n" +
|
||||
"- id: acme.shop\n module: example.test/sm-shop-plugin\n- id: acme.pgsubs\n module: example.test/pg/subs\n",
|
||||
},
|
||||
{
|
||||
name: "four-space indentation",
|
||||
src: "module: example.test/app\nbinary: app\nplugins:\n - id: acme.user\n module: example.test/sm-user-plugin\n",
|
||||
want: "module: example.test/app\nbinary: app\nplugins:\n - id: acme.user\n module: example.test/sm-user-plugin\n" +
|
||||
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n",
|
||||
},
|
||||
{
|
||||
name: "dash alone on its line",
|
||||
src: "module: example.test/app\nbinary: app\nplugins:\n -\n id: acme.user\n module: example.test/sm-user-plugin\n",
|
||||
want: "module: example.test/app\nbinary: app\nplugins:\n -\n id: acme.user\n module: example.test/sm-user-plugin\n" +
|
||||
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n",
|
||||
},
|
||||
{
|
||||
name: "missing plugins block is appended",
|
||||
src: "# App.\nmodule: example.test/app\nbinary: app",
|
||||
want: "# App.\nmodule: example.test/app\nbinary: app\nplugins:\n" +
|
||||
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n",
|
||||
},
|
||||
{
|
||||
name: "empty inline list becomes a block",
|
||||
src: "module: example.test/app\nbinary: app\nplugins: [] # none yet\ntags: []\n",
|
||||
want: "module: example.test/app\nbinary: app\nplugins:\n" +
|
||||
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" +
|
||||
"tags: []\n",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), manifestName)
|
||||
write(t, path, c.src)
|
||||
if err := appendManifestPlugins(path, add); err != nil {
|
||||
t.Fatalf("appendManifestPlugins: %v", err)
|
||||
}
|
||||
got, _ := os.ReadFile(path)
|
||||
if string(got) != c.want {
|
||||
t.Errorf("summer.yaml =\n%s\nwant\n%s", got, c.want)
|
||||
}
|
||||
m, err := LoadManifest(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := len(m.Plugins)
|
||||
if n < 2 || m.Plugins[n-2] != add[0] || m.Plugins[n-1] != add[1] {
|
||||
t.Errorf("plugins = %+v", m.Plugins)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("refusals leave the file unchanged", func(t *testing.T) {
|
||||
refusals := []struct {
|
||||
name, src string
|
||||
plugins []Plugin
|
||||
want string
|
||||
}{
|
||||
{"inline list", "module: example.test/app\nbinary: app\nplugins: [{id: acme.user, module: example.test/u}]\n", add, "inline"},
|
||||
{"bad id", "module: example.test/app\nbinary: app\n", []Plugin{{ID: "Acme", Module: "example.test/x"}}, "plugins[0]"},
|
||||
{"bad module", "module: example.test/app\nbinary: app\n", []Plugin{{ID: "acme.x", Module: "-x"}}, "module"},
|
||||
{"broken file", "module: [\n", add, ""},
|
||||
// A second YAML document after the plugins block: the items
|
||||
// land in it, the first document does not gain them, and the
|
||||
// original bytes are restored.
|
||||
{"edit does not parse back", "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n---\nother: 1\n", add, "left unchanged"},
|
||||
}
|
||||
for _, r := range refusals {
|
||||
path := filepath.Join(t.TempDir(), manifestName)
|
||||
write(t, path, r.src)
|
||||
err := appendManifestPlugins(path, r.plugins)
|
||||
if err == nil || !strings.Contains(err.Error(), r.want) {
|
||||
t.Errorf("%s: err = %v, want %q", r.name, err, r.want)
|
||||
}
|
||||
if got, _ := os.ReadFile(path); string(got) != r.src {
|
||||
t.Errorf("%s: file changed:\n%s", r.name, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nothing to add", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), manifestName)
|
||||
src := "module: example.test/app\nbinary: app\n"
|
||||
write(t, path, src)
|
||||
if err := appendManifestPlugins(path, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := os.ReadFile(path); string(got) != src {
|
||||
t.Errorf("file changed:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing file", func(t *testing.T) {
|
||||
if err := appendManifestPlugins(filepath.Join(t.TempDir(), manifestName), add); err == nil {
|
||||
t.Error("appendManifestPlugins on a missing file succeeded")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func resolvePluginInfo(ctx context.Context, appDir string, m Manifest) ([]party.
|
||||
if w := readPluginYAML(pkg.Dir, info); w != "" {
|
||||
warnings = append(warnings, fmt.Sprintf("plugin metadata: %s: %s", p.ID, w))
|
||||
}
|
||||
pluginVersion(ctx, pkg, info)
|
||||
pluginVersion(ctx, appDir, pkg, info)
|
||||
}
|
||||
return infos, warnings
|
||||
}
|
||||
@@ -163,8 +163,10 @@ func readPluginYAML(dir string, info *party.Info) string {
|
||||
|
||||
// pluginVersion sets info.Source and info.Version (D-07): the module
|
||||
// version for a plugin from the module cache, git describe for a plugin
|
||||
// directory that is its own repository, nothing for an in-app plugin.
|
||||
func pluginVersion(ctx context.Context, pkg listedPackage, info *party.Info) {
|
||||
// directory that is its own repository, nothing for an in-app plugin. A
|
||||
// package of the application's own module is in-app even though the
|
||||
// application directory is the top level of a repository.
|
||||
func pluginVersion(ctx context.Context, appDir string, pkg listedPackage, info *party.Info) {
|
||||
mod := pkg.Module
|
||||
switch {
|
||||
case mod == nil:
|
||||
@@ -178,7 +180,10 @@ func pluginVersion(ctx context.Context, pkg listedPackage, info *party.Info) {
|
||||
if root == "" {
|
||||
root = pkg.Dir
|
||||
}
|
||||
if isOwnRepository(ctx, root) {
|
||||
if abs, err := filepath.Abs(appDir); err == nil {
|
||||
appDir = abs
|
||||
}
|
||||
if !samePath(root, appDir) && isOwnRepository(ctx, root) {
|
||||
info.Source = party.SourceSubmodule
|
||||
info.Version = gitDescribe(ctx, root)
|
||||
} else {
|
||||
|
||||
303
internal/build/plugindirs_test.go
Normal file
303
internal/build/plugindirs_test.go
Normal file
@@ -0,0 +1,303 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user