A failure after the first change left the application half-modified: a checkout moved into plugins/<vendor>/<name> but not registered, go.mod edited, submodules without summer.yaml entries (and the next install refused the leftover directory), or some submodules updated and others not. Both commands now snapshot go.mod, go.sum, go.work, go.work.sum, summer.yaml and .gitmodules (with its index entry) before applying; a failure restores them, removes the submodules the run added (index entry, submodule config, absorbed git dir, checkout; registered right after the move) and checks moved submodules out where they were. What cannot be undone is named in the error.
1049 lines
44 KiB
Go
1049 lines
44 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/kiosk"
|
|
)
|
|
|
|
const (
|
|
blogModule = "example.com/acme/blog"
|
|
// plantedComment is written into summer.yaml before an install; the
|
|
// install must keep it.
|
|
plantedComment = "# planted: plugin:install keeps this comment"
|
|
)
|
|
|
|
// plantComment puts a comment line and a blank line above the plugins
|
|
// list of the app's summer.yaml.
|
|
func plantComment(t *testing.T, appDir string) {
|
|
t.Helper()
|
|
path := filepath.Join(appDir, manifestName)
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := strings.Replace(string(raw), "plugins:\n", "\n"+plantedComment+"\nplugins:\n", 1)
|
|
write(t, path, s)
|
|
}
|
|
|
|
func assertManifestKeepsComment(t *testing.T, appDir string) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join(appDir, manifestName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(raw, []byte(plantedComment)) {
|
|
t.Fatalf("summer.yaml lost the planted comment:\n%s", raw)
|
|
}
|
|
}
|
|
|
|
func manifestIDs(t *testing.T, appDir string) []string {
|
|
t.Helper()
|
|
m, err := LoadManifest(filepath.Join(appDir, manifestName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids := make([]string, len(m.Plugins))
|
|
for i, p := range m.Plugins {
|
|
ids[i] = p.ID + "=" + p.Module
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// fullError is the error text plus a bonfire hint's explanation and Try
|
|
// line, where the hints name commands and flags.
|
|
func fullError(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
text := err.Error()
|
|
var h *bonfire.HintError
|
|
if errors.As(err, &h) {
|
|
text += "\n" + h.Explanation + "\n" + h.Try
|
|
}
|
|
return text
|
|
}
|
|
|
|
func TestInstallSmoke(t *testing.T) {
|
|
blogV1 := regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)
|
|
blogV2 := regVersion("v0.2.0", "v9.0.0", blogModule, blogModule, nil)
|
|
|
|
t.Run("module", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("builds an app against a file:// module proxy")
|
|
}
|
|
app := helloCopy(t)
|
|
plantComment(t, app)
|
|
proxy := t.TempDir()
|
|
writeProxyModule(t, proxy, blogModule, "v0.1.0", fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0"))
|
|
offlineGoEnv(t, proxy)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git", blogV2, blogV1),
|
|
})
|
|
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if len(plan.Steps) != 1 || plan.Steps[0].Version != "v0.1.0" || plan.Steps[0].Mode != ModeModule {
|
|
t.Fatalf("plan steps = %+v", plan.Steps)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog@v0.1.0 (module)") {
|
|
t.Fatalf("plan output lacks the step:\n%s", out.String())
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "run summer build") {
|
|
t.Fatalf("apply output:\n%s", out.String())
|
|
}
|
|
gomod, err := os.ReadFile(filepath.Join(app, "go.mod"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(gomod, []byte(blogModule+" v0.1.0")) {
|
|
t.Fatalf("go.mod does not require %s v0.1.0:\n%s", blogModule, gomod)
|
|
}
|
|
ids := manifestIDs(t, app)
|
|
if last := ids[len(ids)-1]; last != "acme.blog="+blogModule {
|
|
t.Fatalf("summer.yaml plugins = %v", ids)
|
|
}
|
|
assertManifestKeepsComment(t, app)
|
|
|
|
var log bytes.Buffer
|
|
if err := App(t.Context(), app, &log); err != nil {
|
|
t.Fatalf("build: %v\n%s", err, log.String())
|
|
}
|
|
gen, err := os.ReadFile(filepath.Join(app, "plugins.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(gen, []byte(`_ "`+blogModule+`"`)) || !bytes.Contains(gen, []byte(`Version: "v0.1.0"`)) {
|
|
t.Fatalf("plugins.gen.go does not import %s at v0.1.0:\n%s", blogModule, gen)
|
|
}
|
|
})
|
|
|
|
t.Run("module_pin_framework", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git", blogV2, blogV1),
|
|
})
|
|
before := manifestIDs(t, app)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog@v0.2.0", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "v9.0.0") {
|
|
t.Fatalf("pin to v0.2.0: err = %v, want one naming framework v9.0.0", err)
|
|
}
|
|
if got := manifestIDs(t, app); strings.Join(got, ",") != strings.Join(before, ",") {
|
|
t.Fatalf("summer.yaml changed: %v", got)
|
|
}
|
|
})
|
|
|
|
translateModule := "example.com/acme/translate"
|
|
depsRegistry := func(t *testing.T) *fakeReg {
|
|
return fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git",
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, kiosk.Requires{"acme.translate": "v0.1.0"})),
|
|
"acme.translate": regPlugin("acme.translate", "https://example.com/acme/translate.git",
|
|
regVersion("v0.2.0", "v9.0.0", translateModule, translateModule, nil),
|
|
regVersion("v0.1.1", "v0.5.0", translateModule, translateModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", translateModule, translateModule, nil),
|
|
regVersion("v0.0.9", "v0.5.0", translateModule, translateModule, nil)),
|
|
})
|
|
}
|
|
stepList := func(p *InstallPlan) string {
|
|
var parts []string
|
|
for _, s := range p.Steps {
|
|
parts = append(parts, s.ID+"@"+s.Version+"/"+s.Mode)
|
|
}
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
t.Run("deps", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.1/module,acme.blog@v0.1.0/module" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.translate@v0.1.1 (module)\n acme.blog@v0.1.0 (module)") {
|
|
t.Fatalf("summary:\n%s", out.String())
|
|
}
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
proxy := t.TempDir()
|
|
writeProxyModule(t, proxy, blogModule, "v0.1.0", fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0", translateModule+" v0.1.0"))
|
|
writeProxyModule(t, proxy, translateModule, "v0.1.0", fixturePluginFiles(translateModule, "translate", "acme.translate", "v0.5.0"))
|
|
writeProxyModule(t, proxy, translateModule, "v0.1.1", fixturePluginFiles(translateModule, "translate", "acme.translate", "v0.5.0"))
|
|
offlineGoEnv(t, proxy)
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
if n := strings.Count(out.String(), "go get "); n != 1 {
|
|
t.Fatalf("want one go get, got %d:\n%s", n, out.String())
|
|
}
|
|
gomod, _ := os.ReadFile(filepath.Join(app, "go.mod"))
|
|
for _, want := range []string{blogModule + " v0.1.0", translateModule + " v0.1.1"} {
|
|
if !bytes.Contains(gomod, []byte(want)) {
|
|
t.Fatalf("go.mod lacks %s:\n%s", want, gomod)
|
|
}
|
|
}
|
|
ids := manifestIDs(t, app)
|
|
if got := strings.Join(ids[len(ids)-2:], ","); got != "acme.translate="+translateModule+",acme.blog="+blogModule {
|
|
t.Fatalf("summer.yaml order = %v", ids)
|
|
}
|
|
})
|
|
|
|
t.Run("installed_old", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.translate", Module: translateModule}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.0.9"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "v0.0.9") || !strings.Contains(fullError(err), "summer plugin:update acme.translate") {
|
|
t.Fatalf("err = %v (%s), want installed v0.0.9 too old and a plugin:update hint", err, fullError(err))
|
|
}
|
|
// At the minimum it is fine, and only blog is planned.
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := stepList(plan); got != "acme.blog@v0.1.0/module" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("cycle", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.one": regPlugin("acme.one", "", regVersion("v0.1.0", "v0.5.0", "example.com/acme/one", "example.com/acme/one", kiosk.Requires{"acme.two": "v0.1.0"})),
|
|
"acme.two": regPlugin("acme.two", "", regVersion("v0.1.0", "v0.5.0", "example.com/acme/two", "example.com/acme/two", kiosk.Requires{"acme.one": "v0.1.0"})),
|
|
})
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.one", Yes: true, Registry: reg.Client})
|
|
done <- err
|
|
}()
|
|
err := <-done
|
|
if err == nil || !strings.Contains(err.Error(), "cycle") || !strings.Contains(err.Error(), "acme.one -> acme.two -> acme.one") {
|
|
t.Fatalf("err = %v, want a reported cycle", err)
|
|
}
|
|
})
|
|
|
|
t.Run("pin", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.translate@v0.1.0", Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.0/module" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.translate@v0.7.0", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "v0.7.0") || !strings.Contains(err.Error(), "v0.2.0, v0.1.1, v0.1.0, v0.0.9") {
|
|
t.Fatalf("unknown pin: err = %v, want the available versions listed", err)
|
|
}
|
|
for _, bad := range []string{"acme.translate@latest", "acme.translate@v1.0", "Acme.Translate", "acme.translate@-v1.0.0"} {
|
|
if _, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: bad, Yes: true, Registry: reg.Client}); err == nil {
|
|
t.Fatalf("%s: want an error", bad)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("unknown", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.nope", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "not in the registry") || !strings.Contains(err.Error(), reg.URL) {
|
|
t.Fatalf("err = %v, want not in the registry at %s", err, reg.URL)
|
|
}
|
|
})
|
|
|
|
t.Run("prompt", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
before, _ := os.ReadFile(filepath.Join(app, manifestName))
|
|
var out bytes.Buffer
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "--yes") {
|
|
t.Fatalf("non-interactive without --yes: err = %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog@v0.1.0 (module)") {
|
|
t.Fatalf("plan not printed before the error:\n%s", out.String())
|
|
}
|
|
asked := ""
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Registry: reg.Client, Interactive: true,
|
|
Confirm: func(q string) (bool, error) { asked = q; return false, nil }})
|
|
if !errors.Is(err, ErrCancelled) || asked == "" {
|
|
t.Fatalf("declined prompt: err = %v, asked %q", err, asked)
|
|
}
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Registry: reg.Client, Interactive: true,
|
|
Confirm: func(string) (bool, error) { return true, nil }})
|
|
if err != nil || len(plan.Steps) != 2 {
|
|
t.Fatalf("accepted prompt: plan %+v, err %v", plan, err)
|
|
}
|
|
after, _ := os.ReadFile(filepath.Join(app, manifestName))
|
|
if !bytes.Equal(before, after) {
|
|
t.Fatal("planning changed summer.yaml")
|
|
}
|
|
})
|
|
|
|
t.Run("offline", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Offline: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "--offline") || !strings.Contains(fullError(err), "git") {
|
|
t.Fatalf("offline: err = %v (%s)", err, fullError(err))
|
|
}
|
|
if n := len(reg.Requests()); n != 0 {
|
|
t.Fatalf("offline made %d registry requests", n)
|
|
}
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true})
|
|
if err == nil || !strings.Contains(err.Error(), "plugins.registry_url") || !strings.Contains(fullError(err), "git") {
|
|
t.Fatalf("disabled registry: err = %v (%s)", err, fullError(err))
|
|
}
|
|
})
|
|
|
|
t.Run("invalid", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
for name, v := range map[string]kiosk.RegistryVersion{
|
|
"dash module": regVersion("v0.1.0", "v0.5.0", "-modfile=/tmp/x", "-modfile=/tmp/x", nil),
|
|
"bad version": regVersion("v0.1", "v0.5.0", blogModule, blogModule, nil),
|
|
"outside": regVersion("v0.1.0", "v0.5.0", blogModule, "example.com/other/pkg", nil),
|
|
"bad requireid": regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, kiosk.Requires{"-x": "v0.1.0"}),
|
|
} {
|
|
reg := fakeRegistry(t, map[string]fakePlugin{"acme.blog": regPlugin("acme.blog", "", v)})
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err == nil {
|
|
t.Fatalf("%s: accepted, plan %+v", name, plan.Steps)
|
|
}
|
|
if name == "bad version" {
|
|
if !strings.Contains(err.Error(), "no release") && !strings.Contains(err.Error(), "refused") {
|
|
t.Fatalf("%s: err = %v", name, err)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), "refused") {
|
|
t.Fatalf("%s: err = %v, want refused", name, err)
|
|
}
|
|
}
|
|
// ApplyInstall checks again, so a hand-built plan cannot reach go.
|
|
bad := &InstallPlan{AppDir: app, Steps: []InstallStep{{ID: "acme.blog", Version: "v0.1.0", Module: "-x", Package: "-x", Mode: ModeModule}}}
|
|
if err := ApplyInstall(t.Context(), bad, nil); err == nil || !strings.Contains(err.Error(), "refused") {
|
|
t.Fatalf("apply: err = %v", err)
|
|
}
|
|
})
|
|
|
|
// Source installs: fixture repositories over file://, submodules in a
|
|
// git copy of examples/hello, builds with GOPROXY=off.
|
|
translateReq := map[string][2]string{"acme.translate": {translateModule, "v0.1.0"}}
|
|
sourceRegistry := func(t *testing.T, blogRepo, translateRepo string) *fakeReg {
|
|
return fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.2.0", "v9.0.0", blogModule, blogModule, kiosk.Requires{"acme.translate": "v0.1.0"}),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, kiosk.Requires{"acme.translate": "v0.1.0"})),
|
|
"acme.translate": regPlugin("acme.translate", translateRepo,
|
|
regVersion("v0.1.0", "v0.5.0", translateModule, translateModule, nil)),
|
|
})
|
|
}
|
|
assertSubmodule := func(t *testing.T, app, rel, tag string) {
|
|
t.Helper()
|
|
gm, err := os.ReadFile(filepath.Join(app, ".gitmodules"))
|
|
if err != nil || !bytes.Contains(gm, []byte("path = "+rel)) {
|
|
t.Fatalf(".gitmodules lacks %s: %v\n%s", rel, err, gm)
|
|
}
|
|
if status := gitOut(t, app, "submodule", "status", "--", rel); !strings.Contains(status, "("+tag+")") {
|
|
t.Fatalf("submodule status = %q, want %s", status, tag)
|
|
}
|
|
if st, err := os.Stat(filepath.Join(app, rel, ".git")); err != nil || st.IsDir() {
|
|
t.Fatalf("%s/.git should be a file after absorbgitdirs: %v", rel, err)
|
|
}
|
|
}
|
|
buildApp := func(t *testing.T, app string) {
|
|
t.Helper()
|
|
var log bytes.Buffer
|
|
if err := App(t.Context(), app, &log); err != nil {
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
t.Fatalf("build: %v\n%s\ngo.work:\n%s", err, log.String(), work)
|
|
}
|
|
}
|
|
|
|
t.Run("source", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}, {Tag: "v0.2.0", Framework: "v9.0.0"}}, nil)
|
|
app := gitApp(t)
|
|
plantComment(t, app)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.2.0", "v9.0.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)),
|
|
})
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.blog@v0.1.0/submodule" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
gomod, _ := os.ReadFile(filepath.Join(app, "go.mod"))
|
|
if !bytes.Contains(gomod, []byte(blogModule+" v0.1.0")) || !bytes.Contains(gomod, []byte(blogModule+" => ./plugins/acme/blog")) {
|
|
t.Fatalf("go.mod lacks the require at the tag or the local replace:\n%s", gomod)
|
|
}
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte("./plugins/acme/blog")) || !bytes.Contains(work, []byte("replace "+frameworkModule)) {
|
|
t.Fatalf("go.work lacks the use or the framework override:\n%s", work)
|
|
}
|
|
assertManifestKeepsComment(t, app)
|
|
if len(installTemps(t, app)) != 0 {
|
|
t.Fatalf("leftover checkouts: %v", installTemps(t, app))
|
|
}
|
|
buildApp(t, app)
|
|
})
|
|
|
|
t.Run("source_deps", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0", Requires: translateReq}}, nil)
|
|
app := gitApp(t)
|
|
reg := sourceRegistry(t, blogRepo, translateRepo)
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.0/submodule,acme.blog@v0.1.0/submodule" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/translate", "v0.1.0")
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte(translateModule+" v0.1.0 => ./plugins/acme/translate")) {
|
|
t.Fatalf("go.work lacks the versioned override for the workspace module:\n%s", work)
|
|
}
|
|
buildApp(t, app)
|
|
})
|
|
|
|
t.Run("giturl", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{
|
|
{Tag: "v0.1.0", Framework: "v0.5.0", Requires: translateReq},
|
|
{Tag: "v0.2.0", Framework: "v9.0.0", Requires: translateReq},
|
|
}, nil)
|
|
app := gitApp(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.translate": regPlugin("acme.translate", translateRepo, regVersion("v0.1.0", "v0.5.0", translateModule, translateModule, nil)),
|
|
})
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.0/submodule,acme.blog@v0.1.0/submodule" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
for _, r := range reg.Requests() {
|
|
if strings.Contains(r, "acme.blog") {
|
|
t.Fatalf("the git URL plugin was looked up in the registry: %v", reg.Requests())
|
|
}
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
buildApp(t, app)
|
|
|
|
// No tag fits: the default branch head is planned with a warning,
|
|
// and a declined prompt leaves no checkout behind.
|
|
headRepo := pluginRepo(t, "acme.news", "example.com/acme/news", []fixtureRelease{{Tag: "v0.1.0", Framework: "v9.0.0"}}, &fixtureRelease{Tag: "head", Framework: "v9.0.0"})
|
|
out.Reset()
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: headRepo, Registry: reg.Client, Out: &out, Interactive: true,
|
|
Confirm: func(string) (bool, error) { return false, nil }})
|
|
if !errors.Is(err, ErrCancelled) {
|
|
t.Fatalf("head plan: err = %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "warning: no release tag fits framework") || !strings.Contains(out.String(), "acme.news@default branch head (submodule)") {
|
|
t.Fatalf("head plan output:\n%s", out.String())
|
|
}
|
|
if left := installTemps(t, app); len(left) != 0 {
|
|
t.Fatalf("declined plan left checkouts: %v", left)
|
|
}
|
|
})
|
|
|
|
t.Run("giturl_offline", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0", Requires: translateReq}}, nil)
|
|
app := gitApp(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{})
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo, Yes: true, Offline: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "acme.translate") || !strings.Contains(err.Error(), "--offline") {
|
|
t.Fatalf("missing requirement offline: err = %v", err)
|
|
}
|
|
if left := installTemps(t, app); len(left) != 0 {
|
|
t.Fatalf("failed plan left checkouts: %v", left)
|
|
}
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.translate", Module: translateModule}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo, Yes: true, Offline: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
if n := len(reg.Requests()); n != 0 {
|
|
t.Fatalf("offline install made %d registry requests: %v", n, reg.Requests())
|
|
}
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte(translateModule+" => "+translateModule+" v0.1.0")) {
|
|
t.Fatalf("go.work lacks the version pin for the replaced requirement:\n%s", work)
|
|
}
|
|
})
|
|
|
|
t.Run("nogit", func(t *testing.T) {
|
|
gitTestEnv(t)
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "not a git repository") || !strings.Contains(err.Error(), "submodule") {
|
|
t.Fatalf("--source outside git: err = %v", err)
|
|
}
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "https://git.example.com/acme/sm-blog-plugin.git", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "not a git repository") {
|
|
t.Fatalf("git URL outside git: err = %v", err)
|
|
}
|
|
if left := installTemps(t, app); len(left) != 0 {
|
|
t.Fatalf("refused install left checkouts: %v", left)
|
|
}
|
|
})
|
|
|
|
t.Run("urls", func(t *testing.T) {
|
|
accept := []string{
|
|
"https://git.example.com/acme/sm-blog-plugin.git",
|
|
"ssh://git@git.example.com/acme/blog.git",
|
|
"git@git.example.com:acme/blog.git",
|
|
}
|
|
refuse := []string{
|
|
"-uhttps://git.example.com/x.git",
|
|
"--upload-pack=touch /tmp/x",
|
|
"ext::sh -c touch% /tmp/pwned",
|
|
"file::/tmp/x",
|
|
"file:///tmp/repo",
|
|
"http://git.example.com/acme/blog.git",
|
|
"git://git.example.com/acme/blog.git",
|
|
"https://user:secret@git.example.com/acme/blog.git",
|
|
"https://git.example.com/-x",
|
|
"git@git.example.com:-oProxyCommand=x",
|
|
"https://git.example.com/a b.git",
|
|
}
|
|
for _, u := range accept {
|
|
if !isGitURL(u) {
|
|
t.Errorf("isGitURL(%q) = false", u)
|
|
}
|
|
if err := validateGitURL(u); err != nil {
|
|
t.Errorf("validateGitURL(%q) = %v", u, err)
|
|
}
|
|
}
|
|
for _, u := range refuse {
|
|
if err := validateGitURL(u); err == nil {
|
|
t.Errorf("validateGitURL(%q) accepted", u)
|
|
}
|
|
}
|
|
for _, id := range []string{"acme.blog", "acme.blog@v0.1.0"} {
|
|
if isGitURL(id) {
|
|
t.Errorf("isGitURL(%q) = true", id)
|
|
}
|
|
}
|
|
if err := validateRepositoryURL("acme.blog", "ssh://git@git.example.com/acme/blog.git"); err == nil {
|
|
t.Error("registry repository over ssh accepted")
|
|
}
|
|
if err := validateRepositoryURL("acme.blog", "https://git.example.com/acme/blog.git"); err != nil {
|
|
t.Errorf("registry https repository: %v", err)
|
|
}
|
|
old := cliGitProtocols
|
|
cliGitProtocols = "https:ssh:file"
|
|
defer func() { cliGitProtocols = old }()
|
|
if err := validateGitURL("file:///tmp/repo"); err != nil {
|
|
t.Errorf("file:// with the test protocols: %v", err)
|
|
}
|
|
if err := validateGitURL("file::/tmp/repo"); err == nil {
|
|
t.Error("file:: accepted with the test protocols")
|
|
}
|
|
})
|
|
|
|
// plugin:update. tagRelease adds a tagged release to a fixture repo.
|
|
tagRelease := func(t *testing.T, repoURL, id, module string, rel fixtureRelease) {
|
|
t.Helper()
|
|
dir := strings.TrimPrefix(repoURL, "file://")
|
|
for name, body := range pluginRepoFiles(t, id, module, rel) {
|
|
write(t, filepath.Join(dir, name), body)
|
|
}
|
|
write(t, filepath.Join(dir, "CHANGES"), "release "+rel.Tag+"\n")
|
|
runTestGit(t, dir, "add", "-A")
|
|
runTestGit(t, dir, "commit", "--quiet", "-m", "release "+rel.Tag)
|
|
runTestGit(t, dir, "tag", rel.Tag)
|
|
}
|
|
requireOf := func(t *testing.T, app, module string) string {
|
|
t.Helper()
|
|
gomod, err := editJSON(t.Context(), app, "mod", "edit", "-json", filepath.Join(app, "go.mod"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range gomod.Require {
|
|
if r.Path == module {
|
|
return r.Version
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
// sourceApp is a git app with acme.blog v0.1.0 installed with --source.
|
|
sourceApp := func(t *testing.T) (app, blogRepo string, reg *fakeReg) {
|
|
t.Helper()
|
|
blogRepo = pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
app = gitApp(t)
|
|
reg = fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo, regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)),
|
|
})
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return app, blogRepo, reg
|
|
}
|
|
|
|
t.Run("source_shared_module", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
blogDir := filepath.Join(app, "plugins", "acme", "blog")
|
|
reg.set("acme.blogextra", regPlugin("acme.blogextra", blogRepo,
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule+"/extra", nil)))
|
|
// The checkout has no extra package: refused, nothing cloned.
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blogextra", Source: true, Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "that checkout has no package "+blogModule+"/extra") {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(blogDir, "extra"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blogextra", Source: true, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.blogextra@v0.1.0/submodule" || !plan.Steps[0].existing {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
gitmodules, _ := os.ReadFile(filepath.Join(app, ".gitmodules"))
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
if after, _ := os.ReadFile(filepath.Join(app, ".gitmodules")); !bytes.Equal(gitmodules, after) || len(installTemps(t, app)) != 0 {
|
|
t.Fatalf("a second submodule or checkout was added:\n%s", after)
|
|
}
|
|
if strings.Contains(out.String(), "Cloning") {
|
|
t.Fatalf("cloned again:\n%s", out.String())
|
|
}
|
|
// A git URL install of the same repository is refused before cloning.
|
|
out.Reset()
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo + ".git", Yes: true, Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "already checked out at plugins/acme/blog") || strings.Contains(out.String(), "Cloning") {
|
|
t.Fatalf("git URL of an installed submodule: %v\n%s", err, out.String())
|
|
}
|
|
})
|
|
|
|
t.Run("update", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("installs from a file:// module proxy")
|
|
}
|
|
app := helloCopy(t)
|
|
proxy := t.TempDir()
|
|
for _, v := range []string{"v0.1.0", "v0.1.1"} {
|
|
writeProxyModule(t, proxy, blogModule, v, fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0"))
|
|
}
|
|
offlineGoEnv(t, proxy)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{"acme.blog": regPlugin("acme.blog", "", blogV1)})
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg.set("acme.blog", regPlugin("acme.blog", "",
|
|
regVersion("v0.3.0", "v9.0.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
blogV1))
|
|
var out bytes.Buffer
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("update: %v\n%s", err, out.String())
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
|
t.Fatalf("go.mod requires %s %s, want v0.1.1\n%s", blogModule, got, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1 (module)") {
|
|
t.Fatalf("update output:\n%s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("second update: %v\n%s", err, out.String())
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
|
t.Fatalf("second update moved to %s", got)
|
|
}
|
|
if !strings.Contains(out.String(), "v0.3.0 needs framework v9.0.0") {
|
|
t.Fatalf("second update does not report the incompatible release:\n%s", out.String())
|
|
}
|
|
})
|
|
|
|
t.Run("update_submodule", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
var out bytes.Buffer
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("update: %v\n%s", err, out.String())
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.1" {
|
|
t.Fatalf("submodule at %s, want v0.1.1\n%s", got, out.String())
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
|
t.Fatalf("go.mod requires %s, want v0.1.1", got)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1 (submodule)") {
|
|
t.Fatalf("update output:\n%s", out.String())
|
|
}
|
|
buildApp(t, app)
|
|
|
|
// A go.work pin of a module to one of its own versions follows the
|
|
// app's require when that moves.
|
|
if err := runGo(t.Context(), app, "work", "edit", "-replace="+translateModule+"="+translateModule+"@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.2.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := refreshWorkOverrides(t.Context(), app, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte(translateModule+" => "+translateModule+" v0.2.0")) {
|
|
t.Fatalf("pin not refreshed:\n%s", work)
|
|
}
|
|
})
|
|
|
|
t.Run("update_all", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
// acme.translate from a git URL: not in the registry, updated from
|
|
// its own tags.
|
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: translateRepo, Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// acme.ghost: a module install the registry does not list.
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.ghost", Module: "example.com/acme/ghost"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require=example.com/acme/ghost@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
tagRelease(t, translateRepo, "acme.translate", translateModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
tagRelease(t, translateRepo, "acme.translate", translateModule, fixtureRelease{Tag: "v0.2.0", Framework: "v9.0.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
|
|
var out bytes.Buffer
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("update all: %v\n%s", err, out.String())
|
|
}
|
|
for _, want := range []string{
|
|
"acme.blog v0.1.0 -> v0.1.1 (submodule)",
|
|
"acme.translate v0.1.0 -> v0.1.1 (submodule)",
|
|
"golem15.hello: ",
|
|
"acme.ghost: ",
|
|
"v0.2.0 needs framework v9.0.0",
|
|
} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Fatalf("update all output lacks %q:\n%s", want, out.String())
|
|
}
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "translate"), "describe", "--tags"); got != "v0.1.1" {
|
|
t.Fatalf("translate at %s, want v0.1.1", got)
|
|
}
|
|
|
|
// A release the registry lists but the repository lacks makes the
|
|
// run fail, naming the plugin; the other plugins are reported.
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.2", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil)))
|
|
out.Reset()
|
|
err = Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "acme.blog") {
|
|
t.Fatalf("failed update: err = %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "acme.ghost: ") {
|
|
t.Fatalf("skipped plugins not reported:\n%s", out.String())
|
|
}
|
|
})
|
|
|
|
t.Run("rollback", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
app := gitApp(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo, regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)),
|
|
})
|
|
files := []string{"go.mod", "go.sum", "go.work", manifestName, ".gitmodules"}
|
|
read := func() map[string]string {
|
|
got := map[string]string{}
|
|
for _, f := range files {
|
|
raw, err := os.ReadFile(filepath.Join(app, f))
|
|
if err != nil {
|
|
got[f] = "<absent>"
|
|
continue
|
|
}
|
|
got[f] = string(raw)
|
|
}
|
|
got["status"] = gitOut(t, app, "status", "--porcelain")
|
|
return got
|
|
}
|
|
before := read()
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A module step whose go get fails (GOPROXY=off) after the
|
|
// submodule was added.
|
|
plan.Steps = append(plan.Steps, InstallStep{ID: "acme.gone", Version: "v0.1.0", Module: "example.com/acme/gone", Package: "example.com/acme/gone", Mode: ModeModule})
|
|
var out bytes.Buffer
|
|
err = ApplyInstall(t.Context(), plan, &out)
|
|
if err == nil || !strings.Contains(err.Error(), "go get") || !strings.Contains(out.String(), "Undid the changes of this run.") {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
after := read()
|
|
for k, v := range before {
|
|
if after[k] != v {
|
|
t.Errorf("%s not restored:\nbefore %q\nafter %q", k, v, after[k])
|
|
}
|
|
}
|
|
if dirExists(filepath.Join(app, "plugins", "acme", "blog")) || dirExists(filepath.Join(app, ".git", "modules", "plugins", "acme", "blog")) {
|
|
t.Error("the added submodule is still there")
|
|
}
|
|
// The next run is not refused by leftovers.
|
|
plan, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatalf("plan after the rollback: %v", err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatalf("apply after the rollback: %v", err)
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
|
|
// plugin:update: the submodule moves, then go get of a module
|
|
// plugin fails; both go back.
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
reg.set("acme.gone", regPlugin("acme.gone", "", regVersion("v0.2.0", "v0.5.0", "example.com/acme/gone", "example.com/acme/gone", nil)))
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require=example.com/acme/gone@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.gone", Module: "example.com/acme/gone"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gomod, _ := os.ReadFile(filepath.Join(app, "go.mod"))
|
|
out.Reset()
|
|
err = Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(out.String(), "Undid the changes of this run.") {
|
|
t.Fatalf("update: %v\n%s", err, out.String())
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.0" {
|
|
t.Errorf("submodule at %s after the rollback, want v0.1.0", got)
|
|
}
|
|
if after, _ := os.ReadFile(filepath.Join(app, "go.mod")); !bytes.Equal(gomod, after) {
|
|
t.Errorf("go.mod not restored:\n%s", after)
|
|
}
|
|
})
|
|
|
|
t.Run("update_prompt", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
var out bytes.Buffer
|
|
err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "--yes") || !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1") {
|
|
t.Fatalf("non-interactive update: err = %v\n%s", err, out.String())
|
|
}
|
|
err = Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Registry: reg.Client, Interactive: true,
|
|
Confirm: func(string) (bool, error) { return false, nil }})
|
|
if !errors.Is(err, ErrCancelled) {
|
|
t.Fatalf("declined update: err = %v", err)
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.0" {
|
|
t.Fatalf("declined update moved the submodule to %s", got)
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.0" {
|
|
t.Fatalf("declined update changed go.mod to %s", got)
|
|
}
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.nope", Yes: true, Registry: reg.Client}); err == nil || !strings.Contains(err.Error(), "summer.yaml") {
|
|
t.Fatalf("unknown ID: err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("append_manifest", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, manifestName)
|
|
cases := []struct {
|
|
name, in, want string
|
|
}{
|
|
{
|
|
name: "indented",
|
|
in: "# app\nmodule: example.com/app\nbinary: app\n\n# the plugins\nplugins:\n - id: a.one\n module: example.com/one # first\n\n # trailing note\nbuild:\n tags: [x]\n",
|
|
want: "# app\nmodule: example.com/app\nbinary: app\n\n# the plugins\nplugins:\n - id: a.one\n module: example.com/one # first\n - id: acme.blog\n module: example.com/acme/blog\n\n # trailing note\nbuild:\n tags: [x]\n",
|
|
},
|
|
{
|
|
name: "flush",
|
|
in: "module: example.com/app\nbinary: app\nplugins:\n- id: a.one\n module: example.com/one\n",
|
|
want: "module: example.com/app\nbinary: app\nplugins:\n- id: a.one\n module: example.com/one\n- id: acme.blog\n module: example.com/acme/blog\n",
|
|
},
|
|
{
|
|
name: "empty",
|
|
in: "module: example.com/app\nbinary: app\nplugins: []\n# end\n",
|
|
want: "module: example.com/app\nbinary: app\nplugins:\n - id: acme.blog\n module: example.com/acme/blog\n# end\n",
|
|
},
|
|
{
|
|
name: "absent",
|
|
in: "module: example.com/app\nbinary: app",
|
|
want: "module: example.com/app\nbinary: app\nplugins:\n - id: acme.blog\n module: example.com/acme/blog\n",
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
write(t, path, c.in)
|
|
if err := appendManifestPlugins(path, []Plugin{{ID: "acme.blog", Module: blogModule}}); err != nil {
|
|
t.Fatalf("%s: %v", c.name, err)
|
|
}
|
|
got, _ := os.ReadFile(path)
|
|
if string(got) != c.want {
|
|
t.Fatalf("%s:\n got %q\nwant %q", c.name, got, c.want)
|
|
}
|
|
}
|
|
inline := "module: example.com/app\nbinary: app\nplugins: [{id: a.one, module: example.com/one}]\n"
|
|
write(t, path, inline)
|
|
if err := appendManifestPlugins(path, []Plugin{{ID: "acme.blog", Module: blogModule}}); err == nil {
|
|
t.Fatal("inline plugins list: want an error")
|
|
}
|
|
if got, _ := os.ReadFile(path); string(got) != inline {
|
|
t.Fatalf("inline list was changed: %q", got)
|
|
}
|
|
dup := "module: example.com/app\nbinary: app\nplugins:\n - id: acme.blog\n module: example.com/other\n"
|
|
write(t, path, dup)
|
|
if err := appendManifestPlugins(path, []Plugin{{ID: "acme.blog", Module: blogModule}}); err == nil {
|
|
t.Fatal("duplicate ID: want an error")
|
|
}
|
|
if got, _ := os.ReadFile(path); string(got) != dup {
|
|
t.Fatalf("summer.yaml not restored: %q", got)
|
|
}
|
|
})
|
|
|
|
t.Run("registry_client", func(t *testing.T) {
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git", blogV2, blogV1),
|
|
})
|
|
vs, err := reg.Client.Versions(t.Context(), "acme.blog")
|
|
if err != nil || len(vs) != 2 || vs[0].Version != "v0.2.0" {
|
|
t.Fatalf("versions = %+v, %v", vs, err)
|
|
}
|
|
_, err = reg.Client.Versions(t.Context(), "acme.nope")
|
|
if !errors.Is(err, kiosk.ErrNotFound) || !strings.Contains(err.Error(), reg.URL) {
|
|
t.Fatalf("unknown id: err = %v", err)
|
|
}
|
|
list, err := reg.Client.List(t.Context(), "blog", "")
|
|
if err != nil || list.Meta.Total != 1 {
|
|
t.Fatalf("list = %+v, %v", list, err)
|
|
}
|
|
if _, err := kiosk.NewClient("", nil); !errors.Is(err, kiosk.ErrRegistryDisabled) {
|
|
t.Fatalf("empty URL: err = %v", err)
|
|
}
|
|
for _, bad := range []string{"ftp://x", "https://user:pw@x", "https://x/?q=1", "x"} {
|
|
if _, err := kiosk.NewClient(bad, nil); err == nil {
|
|
t.Fatalf("NewClient(%q) accepted", bad)
|
|
}
|
|
}
|
|
})
|
|
}
|