- kiosk.ReadIdentity: ID()/Requires() as literals or constants across packages and modules, read with go/parser, never run - plugin minimums from go.mod requires; v0.0.0 is an error, a pseudo-version warns - kiosk.Validate, kiosk.CheckT (no testing import) - plugin:add reads IDs through kiosk.ReadIdentity, so const-ID plugins register
364 lines
13 KiB
Go
364 lines
13 KiB
Go
package kiosk
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// copyTestdata copies testdata/<names...> into a fresh temp dir, keeping
|
|
// the relative layout, and returns the temp dir.
|
|
func copyTestdata(t *testing.T, names ...string) string {
|
|
t.Helper()
|
|
dst := t.TempDir()
|
|
for _, name := range names {
|
|
src := filepath.Join("testdata", name)
|
|
err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel("testdata", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target := filepath.Join(dst, rel)
|
|
if d.IsDir() {
|
|
return os.MkdirAll(target, 0o755)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(target, data, 0o644)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("copy %s: %v", src, err)
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// TestManifestSmoke drives plugin.yaml generation end to end on the
|
|
// fixture plugins in testdata.
|
|
func TestManifestSmoke(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("literal", func(t *testing.T) {
|
|
dir := filepath.Join(copyTestdata(t, "literal"), "literal")
|
|
path, warnings, err := Write(ctx, dir, DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
if len(warnings) != 0 {
|
|
t.Errorf("warnings = %v", warnings)
|
|
}
|
|
if path != filepath.Join(dir, FileName) {
|
|
t.Errorf("path = %s", path)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(string(raw), Header+"\n") {
|
|
t.Errorf("plugin.yaml does not start with the header:\n%s", raw)
|
|
}
|
|
m, err := Parse(raw)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
want := Manifest{
|
|
Schema: SchemaVersion,
|
|
ID: "acme.blog",
|
|
Module: "example.test/sm-acme-plugin",
|
|
Package: "example.test/sm-acme-plugin",
|
|
Name: "Blog",
|
|
Description: "Posts and categories for an acme application, with a feed.",
|
|
Framework: "v0.5.3",
|
|
}
|
|
if m.Schema != want.Schema || m.ID != want.ID || m.Module != want.Module || m.Package != want.Package ||
|
|
m.Name != want.Name || m.Description != want.Description || m.Framework != want.Framework ||
|
|
len(m.Requires) != 0 || m.Overrides != nil {
|
|
t.Errorf("manifest = %+v, want %+v", m, want)
|
|
}
|
|
wantOrder := []string{"schema:", "id:", "module:", "package:", "name:", "description:", "framework:"}
|
|
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")[1:]
|
|
if len(lines) != len(wantOrder) {
|
|
t.Fatalf("lines = %q", lines)
|
|
}
|
|
for i, prefix := range wantOrder {
|
|
if !strings.HasPrefix(lines[i], prefix) {
|
|
t.Errorf("line %d = %q, want prefix %q", i+2, lines[i], prefix)
|
|
}
|
|
}
|
|
|
|
if err := Check(ctx, dir); err != nil {
|
|
t.Fatalf("Check on a fresh plugin.yaml: %v", err)
|
|
}
|
|
|
|
readme := filepath.Join(dir, "README.md")
|
|
src, _ := os.ReadFile(readme)
|
|
edited := strings.Replace(string(src), "with a feed.", "with an Atom feed.", 1)
|
|
if err := os.WriteFile(readme, []byte(edited), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stale *StaleError
|
|
if err := Check(ctx, dir); !errors.As(err, &stale) || stale.Missing {
|
|
t.Fatalf("Check after a README edit = %v, want a stale *StaleError", err)
|
|
}
|
|
if !strings.Contains(stale.Error(), path) || !strings.Contains(stale.Error(), "line ") {
|
|
t.Errorf("StaleError text = %q", stale.Error())
|
|
}
|
|
|
|
if err := os.Remove(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := Check(ctx, dir); !errors.As(err, &stale) || !stale.Missing {
|
|
t.Fatalf("Check without plugin.yaml = %v, want Missing", err)
|
|
}
|
|
if len(stale.Want) == 0 {
|
|
t.Error("a missing StaleError carries no Want bytes")
|
|
}
|
|
})
|
|
|
|
t.Run("consts", func(t *testing.T) {
|
|
base := copyTestdata(t, "framework", "other", "consts")
|
|
dir := filepath.Join(base, "consts")
|
|
id, err := ReadIdentity(ctx, dir)
|
|
if err != nil {
|
|
t.Fatalf("ReadIdentity: %v", err)
|
|
}
|
|
if id.ID != "acme.consts" || strings.Join(id.Requires, ",") != "acme.translate,acme.other" {
|
|
t.Fatalf("identity = %+v", id)
|
|
}
|
|
m, warnings, err := Derive(ctx, dir, DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Derive: %v", err)
|
|
}
|
|
if len(warnings) != 0 {
|
|
t.Errorf("warnings = %v", warnings)
|
|
}
|
|
if m.Requires["acme.other"] != "v0.2.0" {
|
|
t.Errorf("acme.other minimum = %q, want the go.mod require v0.2.0", m.Requires["acme.other"])
|
|
}
|
|
if v, ok := m.Requires["acme.translate"]; !ok || v != "" {
|
|
t.Errorf("acme.translate minimum = %q (present %v), want empty", v, ok)
|
|
}
|
|
if m.Package != "example.test/sm-consts-plugin" || m.Framework != "v0.5.3" {
|
|
t.Errorf("manifest = %+v", m)
|
|
}
|
|
})
|
|
|
|
t.Run("nested", func(t *testing.T) {
|
|
base := copyTestdata(t, "framework", "other", "consts")
|
|
m, _, err := Derive(ctx, filepath.Join(base, "consts", "nested"), DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Derive: %v", err)
|
|
}
|
|
if m.ID != "acme.constsnested" || m.Module != "example.test/sm-consts-plugin" || m.Package != "example.test/sm-consts-plugin/nested" {
|
|
t.Errorf("manifest = %+v", m)
|
|
}
|
|
if v, ok := m.Requires["acme.consts"]; !ok || v != "" || len(m.Requires) != 1 {
|
|
t.Errorf("requires = %v, want acme.consts with the empty minimum", m.Requires)
|
|
}
|
|
})
|
|
|
|
t.Run("errors", func(t *testing.T) {
|
|
cases := []struct {
|
|
name, src, want string
|
|
}{
|
|
{"id call", `func (*Plugin) ID() string { return id() }
|
|
func id() string { return "acme.x" }
|
|
func (*Plugin) Requires() []string { return nil }`, "ID()"},
|
|
{"id var", `var pluginID = "acme.x"
|
|
func (*Plugin) ID() string { return pluginID }
|
|
func (*Plugin) Requires() []string { return nil }`, "ID()"},
|
|
{"id concat", `func suffix() string { return "x" }
|
|
func (*Plugin) ID() string { return "acme." + suffix() }
|
|
func (*Plugin) Requires() []string { return nil }`, "ID()"},
|
|
{"requires var", `var deps = []string{"acme.y"}
|
|
func (*Plugin) ID() string { return "acme.x" }
|
|
func (*Plugin) Requires() []string { return deps }`, "Requires()"},
|
|
{"requires call", `func (*Plugin) ID() string { return "acme.x" }
|
|
func (*Plugin) Requires() []string { return append([]string(nil), "acme.y") }`, "Requires()"},
|
|
{"requires element", `var dep = "acme.y"
|
|
func (*Plugin) ID() string { return "acme.x" }
|
|
func (*Plugin) Requires() []string { return []string{dep} }`, "Requires()"},
|
|
}
|
|
for _, tc := range cases {
|
|
dir := t.TempDir()
|
|
src := "package p\n\ntype Plugin struct{}\n\n" + tc.src + "\n"
|
|
if err := os.WriteFile(filepath.Join(dir, "plugin.go"), []byte(src), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.test/p\n\ngo 1.27.0\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := ReadIdentity(ctx, dir)
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Errorf("%s: err = %v, want one naming %s", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
|
|
// A plugin requirement at v0.0.0 is refused; a pseudo-version warns.
|
|
base := copyTestdata(t, "framework", "other", "consts")
|
|
dir := filepath.Join(base, "consts")
|
|
gomod := filepath.Join(dir, "go.mod")
|
|
orig, _ := os.ReadFile(gomod)
|
|
setOther := func(v string) {
|
|
t.Helper()
|
|
data := strings.Replace(string(orig), "example.test/sm-other-plugin v0.2.0", "example.test/sm-other-plugin "+v, 1)
|
|
if err := os.WriteFile(gomod, []byte(data), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
setOther("v0.0.0")
|
|
if _, _, err := Derive(ctx, dir, DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "not a released version") {
|
|
t.Errorf("v0.0.0 requirement: err = %v", err)
|
|
}
|
|
pseudo := "v0.2.1-0.20261029101500-abcdefabcdef"
|
|
setOther(pseudo)
|
|
m, warnings, err := Derive(ctx, dir, DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("pseudo-version requirement: %v", err)
|
|
}
|
|
if m.Requires["acme.other"] != pseudo || len(warnings) != 1 || !strings.Contains(warnings[0], "pseudo-version") {
|
|
t.Errorf("pseudo-version: requires = %v, warnings = %v", m.Requires, warnings)
|
|
}
|
|
|
|
// The framework require is mandatory and must be released.
|
|
lit := filepath.Join(copyTestdata(t, "literal"), "literal")
|
|
litMod := filepath.Join(lit, "go.mod")
|
|
if err := os.WriteFile(litMod, []byte("module example.test/sm-acme-plugin\n\ngo 1.27.0\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := Derive(ctx, lit, DeriveOptions{}); err == nil || !strings.Contains(err.Error(), FrameworkModule) {
|
|
t.Errorf("missing framework require: err = %v", err)
|
|
}
|
|
if err := os.WriteFile(litMod, []byte("module example.test/sm-acme-plugin\n\ngo 1.27.0\n\nrequire "+FrameworkModule+" v0.0.0\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := Derive(ctx, lit, DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "not a released version") {
|
|
t.Errorf("framework v0.0.0: err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("overrides", func(t *testing.T) {
|
|
dir := filepath.Join(copyTestdata(t, "literal"), "literal")
|
|
readme := filepath.Join(dir, "README.md")
|
|
src, _ := os.ReadFile(readme)
|
|
if err := os.WriteFile(readme, []byte(strings.Replace(string(src), "# Blog", "# acme.blog", 1)), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "--name") {
|
|
t.Fatalf("H1 equal to the ID: err = %v, want one naming --name", err)
|
|
}
|
|
name, desc := "Acme Blog", "A blog."
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{Name: &name, Description: &desc}); err != nil {
|
|
t.Fatalf("Write with overrides: %v", err)
|
|
}
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{}); err != nil {
|
|
t.Fatalf("regeneration without flags: %v", err)
|
|
}
|
|
raw, _ := os.ReadFile(filepath.Join(dir, FileName))
|
|
m, err := Parse(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m.Name != name || m.Description != desc || m.Overrides == nil || m.Overrides.Name != name || m.Overrides.Description != desc {
|
|
t.Fatalf("after regeneration = %+v (overrides %+v)", m, m.Overrides)
|
|
}
|
|
if err := Check(ctx, dir); err != nil {
|
|
t.Fatalf("Check keeps the overrides: %v", err)
|
|
}
|
|
empty := ""
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{Description: &empty}); err != nil {
|
|
t.Fatalf("clearing the description: %v", err)
|
|
}
|
|
raw, _ = os.ReadFile(filepath.Join(dir, FileName))
|
|
if m, _ = Parse(raw); m.Description != "Posts and categories for an acme application, with a feed." || m.Overrides.Description != "" || m.Overrides.Name != name {
|
|
t.Fatalf("after clearing the description = %+v (overrides %+v)", m, m.Overrides)
|
|
}
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{Name: &empty}); err == nil || !strings.Contains(err.Error(), "--name") {
|
|
t.Fatalf("clearing the name with H1 == ID: err = %v, want one naming --name", err)
|
|
}
|
|
})
|
|
|
|
t.Run("checkt", func(t *testing.T) {
|
|
dir := filepath.Join(copyTestdata(t, "literal"), "literal")
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fresh fakeTB
|
|
CheckT(&fresh, dir)
|
|
if len(fresh.fatals) != 0 {
|
|
t.Fatalf("CheckT on a fresh manifest failed: %v", fresh.fatals)
|
|
}
|
|
readme := filepath.Join(dir, "README.md")
|
|
src, _ := os.ReadFile(readme)
|
|
if err := os.WriteFile(readme, []byte(strings.Replace(string(src), "# Blog", "# Weblog", 1)), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stale fakeTB
|
|
CheckT(&stale, dir)
|
|
if len(stale.fatals) != 1 || !strings.Contains(stale.fatals[0], filepath.Join(dir, FileName)) || !strings.Contains(stale.fatals[0], "summer plugin:manifest") {
|
|
t.Fatalf("CheckT on a stale manifest: %q", stale.fatals)
|
|
}
|
|
if stale.helpers == 0 {
|
|
t.Error("CheckT did not call Helper")
|
|
}
|
|
})
|
|
|
|
t.Run("validate", func(t *testing.T) {
|
|
good := Manifest{Schema: 1, ID: "acme.blog", Module: "example.test/m", Package: "example.test/m/sub",
|
|
Name: "Blog", Description: "A blog.", Framework: "v0.6.0", Requires: Requires{"acme.x": "", "acme.y": "v0.1.0"}}
|
|
if err := Validate(good); err != nil {
|
|
t.Fatalf("Validate(good) = %v", err)
|
|
}
|
|
bad := map[string]func(*Manifest){
|
|
"id": func(m *Manifest) { m.ID = "Acme.Blog" },
|
|
"module dash": func(m *Manifest) { m.Module = "-example.test/m"; m.Package = m.Module },
|
|
"package outside": func(m *Manifest) { m.Package = "example.test/other" },
|
|
"framework": func(m *Manifest) { m.Framework = "0.6.0" },
|
|
"framework rc": func(m *Manifest) { m.Framework = "v0.6.0-rc1" },
|
|
"schema": func(m *Manifest) { m.Schema = SchemaVersion + 1 },
|
|
"schema zero": func(m *Manifest) { m.Schema = 0 },
|
|
"required id": func(m *Manifest) { m.Requires = Requires{"bad": ""} },
|
|
"minimum": func(m *Manifest) { m.Requires = Requires{"acme.x": "latest"} },
|
|
"name": func(m *Manifest) { m.Name = " " },
|
|
}
|
|
for name, mutate := range bad {
|
|
m := good
|
|
m.Requires = Requires{}
|
|
mutate(&m)
|
|
if err := Validate(m); err == nil {
|
|
t.Errorf("Validate accepted a bad %s: %+v", name, m)
|
|
}
|
|
}
|
|
raw, err := Marshal(good)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
newer := strings.Replace(string(raw), "schema: 1", "schema: 2", 1)
|
|
if _, err := Parse([]byte(newer)); err == nil || !strings.Contains(err.Error(), "update summer") {
|
|
t.Errorf("Parse of a newer schema: err = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// fakeTB records CheckT's calls.
|
|
type fakeTB struct {
|
|
helpers int
|
|
fatals []string
|
|
}
|
|
|
|
func (f *fakeTB) Helper() { f.helpers++ }
|
|
|
|
func (f *fakeTB) Fatalf(format string, args ...any) {
|
|
f.fatals = append(f.fatals, fmt.Sprintf(format, args...))
|
|
}
|