refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
220
modules/party/registry.go
Normal file
220
modules/party/registry.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
||||
"git.golem15.com/golem15/summercms/modules/postcard"
|
||||
)
|
||||
|
||||
// Plugin is the required descriptor every compiled plugin implements.
|
||||
type Plugin interface {
|
||||
ID() string
|
||||
Requires() []string
|
||||
Register(app *backpack.App) error
|
||||
Boot(app *backpack.App) error
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
plugins []Plugin
|
||||
)
|
||||
|
||||
// Register appends a plugin to the process-wide registry. Plugins call this
|
||||
// from init() after a blank import; Activate selects and orders them.
|
||||
func Register(p Plugin) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
|
||||
func snapshot() []Plugin {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
out := make([]Plugin, len(plugins))
|
||||
copy(out, plugins)
|
||||
return out
|
||||
}
|
||||
|
||||
// Activate resolves ids in manifest order, topologically sorts Requires(),
|
||||
// then runs every Register before any Boot.
|
||||
func Activate(app *backpack.App, ids []string) ([]Plugin, error) {
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("party: app is nil")
|
||||
}
|
||||
return activate(snapshot(), app, ids)
|
||||
}
|
||||
|
||||
func activate(registered []Plugin, app *backpack.App, ids []string) ([]Plugin, error) {
|
||||
byID := make(map[string]Plugin, len(registered))
|
||||
for _, p := range registered {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
id := p.ID()
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("party: registered plugin has empty id")
|
||||
}
|
||||
if _, exists := byID[id]; exists {
|
||||
return nil, fmt.Errorf("party: duplicate plugin id %q", id)
|
||||
}
|
||||
byID[id] = p
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(ids))
|
||||
selected := make([]Plugin, 0, len(ids))
|
||||
index := make(map[string]int, len(ids))
|
||||
for _, id := range ids {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("party: empty plugin id in manifest")
|
||||
}
|
||||
if _, dup := seen[id]; dup {
|
||||
return nil, fmt.Errorf("party: duplicate plugin id %q", id)
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
p, ok := byID[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("party: plugin %q is not registered", id)
|
||||
}
|
||||
index[id] = len(selected)
|
||||
selected = append(selected, p)
|
||||
}
|
||||
|
||||
for _, p := range selected {
|
||||
for _, req := range p.Requires() {
|
||||
if _, ok := seen[req]; !ok {
|
||||
return nil, fmt.Errorf("party: plugin %q requires %q, which is not registered", p.ID(), req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ordered, err := topoSort(selected, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderedIDs := make([]string, len(ordered))
|
||||
for i, p := range ordered {
|
||||
orderedIDs[i] = p.ID()
|
||||
}
|
||||
app.SetPlugins(orderedIDs)
|
||||
|
||||
for _, p := range ordered {
|
||||
if hc, ok := p.(pact.HasConfig); ok && app.Config != nil {
|
||||
if err := app.Config.MergePlugin(p.ID(), hc.ConfigFS()); err != nil {
|
||||
return nil, fmt.Errorf("party: config %s: %w", p.ID(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range ordered {
|
||||
if err := p.Register(app); err != nil {
|
||||
return nil, fmt.Errorf("party: register %s: %w", p.ID(), err)
|
||||
}
|
||||
}
|
||||
if err := phrasebook.Activate(app, ordered); err != nil {
|
||||
return nil, fmt.Errorf("party: lang: %w", err)
|
||||
}
|
||||
if err := postcard.Activate(app, ordered); err != nil {
|
||||
return nil, fmt.Errorf("party: mail: %w", err)
|
||||
}
|
||||
for _, p := range ordered {
|
||||
if err := postcard.BootPlugin(app, p); err != nil {
|
||||
return nil, fmt.Errorf("party: boot %s: %w", p.ID(), err)
|
||||
}
|
||||
if err := p.Boot(app); err != nil {
|
||||
return nil, fmt.Errorf("party: boot %s: %w", p.ID(), err)
|
||||
}
|
||||
}
|
||||
return ordered, nil
|
||||
}
|
||||
|
||||
func topoSort(plugins []Plugin, index map[string]int) ([]Plugin, error) {
|
||||
byID := make(map[string]Plugin, len(plugins))
|
||||
indeg := make(map[string]int, len(plugins))
|
||||
dependents := make(map[string][]string, len(plugins))
|
||||
for _, p := range plugins {
|
||||
byID[p.ID()] = p
|
||||
indeg[p.ID()] = 0
|
||||
}
|
||||
for _, p := range plugins {
|
||||
seenReq := make(map[string]struct{})
|
||||
for _, req := range p.Requires() {
|
||||
if _, ok := seenReq[req]; ok {
|
||||
continue
|
||||
}
|
||||
seenReq[req] = struct{}{}
|
||||
if _, ok := byID[req]; !ok {
|
||||
continue
|
||||
}
|
||||
indeg[p.ID()]++
|
||||
dependents[req] = append(dependents[req], p.ID())
|
||||
}
|
||||
}
|
||||
|
||||
ready := make([]string, 0, len(plugins))
|
||||
for _, p := range plugins {
|
||||
if indeg[p.ID()] == 0 {
|
||||
ready = append(ready, p.ID())
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]Plugin, 0, len(plugins))
|
||||
for len(ready) > 0 {
|
||||
id := ready[0]
|
||||
ready = ready[1:]
|
||||
out = append(out, byID[id])
|
||||
next := append([]string(nil), dependents[id]...)
|
||||
sortByIndex(next, index)
|
||||
for _, dep := range next {
|
||||
indeg[dep]--
|
||||
if indeg[dep] == 0 {
|
||||
ready = insertByIndex(ready, dep, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) != len(plugins) {
|
||||
leftover := make([]string, 0)
|
||||
done := make(map[string]struct{}, len(out))
|
||||
for _, p := range out {
|
||||
done[p.ID()] = struct{}{}
|
||||
}
|
||||
for _, p := range plugins {
|
||||
if _, ok := done[p.ID()]; !ok {
|
||||
leftover = append(leftover, p.ID())
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("party: dependency cycle involving %s", strings.Join(leftover, ", "))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func sortByIndex(ids []string, index map[string]int) {
|
||||
for i := 1; i < len(ids); i++ {
|
||||
j := i
|
||||
for j > 0 && index[ids[j]] < index[ids[j-1]] {
|
||||
ids[j], ids[j-1] = ids[j-1], ids[j]
|
||||
j--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func insertByIndex(ready []string, id string, index map[string]int) []string {
|
||||
at := len(ready)
|
||||
for i, existing := range ready {
|
||||
if index[id] < index[existing] {
|
||||
at = i
|
||||
break
|
||||
}
|
||||
}
|
||||
ready = append(ready, "")
|
||||
copy(ready[at+1:], ready[at:])
|
||||
ready[at] = id
|
||||
return ready
|
||||
}
|
||||
540
modules/party/registry_test.go
Normal file
540
modules/party/registry_test.go
Normal file
@@ -0,0 +1,540 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
||||
"git.golem15.com/golem15/summercms/modules/postcard"
|
||||
)
|
||||
|
||||
type tracePlugin struct {
|
||||
id string
|
||||
req []string
|
||||
log *[]string
|
||||
}
|
||||
|
||||
func (p *tracePlugin) ID() string { return p.id }
|
||||
func (p *tracePlugin) Requires() []string { return p.req }
|
||||
|
||||
func (p *tracePlugin) Register(*backpack.App) error {
|
||||
*p.log = append(*p.log, p.id+":register")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *tracePlugin) Boot(*backpack.App) error {
|
||||
*p.log = append(*p.log, p.id+":boot")
|
||||
return nil
|
||||
}
|
||||
|
||||
type configPlugin struct {
|
||||
id string
|
||||
fsys fs.FS
|
||||
got *int
|
||||
}
|
||||
|
||||
func (p *configPlugin) ID() string { return p.id }
|
||||
func (p *configPlugin) Requires() []string { return nil }
|
||||
func (p *configPlugin) ConfigFS() fs.FS { return p.fsys }
|
||||
|
||||
func (p *configPlugin) Register(app *backpack.App) error {
|
||||
if app != nil && app.Config != nil {
|
||||
*p.got = app.Config.Int("golem15.hello.posts_per_page")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *configPlugin) Boot(*backpack.App) error { return nil }
|
||||
|
||||
func TestActivateMergesHasConfigBeforeRegister(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: app\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := compass.Load(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
var got int
|
||||
p := &configPlugin{
|
||||
id: "golem15.hello",
|
||||
got: &got,
|
||||
fsys: fstest.MapFS{
|
||||
"config/config.yaml": {Data: []byte("posts_per_page: 10\n")},
|
||||
},
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
if _, err := activate([]Plugin{p}, app, []string{"golem15.hello"}); err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if got != 10 {
|
||||
t.Fatalf("posts_per_page during Register = %d, want 10 (HasConfig must merge before Register)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateMissingRequiresNamesIDsAndSkipsBoot(t *testing.T) {
|
||||
var log []string
|
||||
p := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{p}, app, []string{"golem15.greeter"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing dependency error")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "golem15.greeter") || !strings.Contains(msg, "golem15.hello") {
|
||||
t.Fatalf("error %q should name both plugin IDs", msg)
|
||||
}
|
||||
if strings.Contains(strings.Join(log, ","), "boot") {
|
||||
t.Fatalf("Boot ran despite missing dependency: %v", log)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateCycleNamesIDsAndSkipsBoot(t *testing.T) {
|
||||
var log []string
|
||||
a := &tracePlugin{id: "golem15.a", req: []string{"golem15.b"}, log: &log}
|
||||
b := &tracePlugin{id: "golem15.b", req: []string{"golem15.a"}, log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{a, b}, app, []string{"golem15.a", "golem15.b"})
|
||||
if err == nil {
|
||||
t.Fatal("expected cycle error")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "golem15.a") || !strings.Contains(msg, "golem15.b") {
|
||||
t.Fatalf("error %q should name cycle members", msg)
|
||||
}
|
||||
if strings.Contains(strings.Join(log, ","), "boot") || strings.Contains(strings.Join(log, ","), "register") {
|
||||
t.Fatalf("lifecycle ran despite cycle: %v", log)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateDuplicateIDRejected(t *testing.T) {
|
||||
var log []string
|
||||
a := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
dup := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{a, dup}, app, []string{"golem15.hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate id error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "golem15.hello") {
|
||||
t.Fatalf("error %q should name the duplicate id", err)
|
||||
}
|
||||
}
|
||||
|
||||
type bootProbe struct {
|
||||
id string
|
||||
req []string
|
||||
log *[]string
|
||||
sawOpt *bool
|
||||
booted *[]string
|
||||
}
|
||||
|
||||
func (p *bootProbe) ID() string { return p.id }
|
||||
func (p *bootProbe) Requires() []string { return p.req }
|
||||
|
||||
func (p *bootProbe) Register(*backpack.App) error {
|
||||
*p.log = append(*p.log, p.id+":register")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *bootProbe) Boot(app *backpack.App) error {
|
||||
if p.sawOpt != nil && p.id == "golem15.hello" {
|
||||
*p.sawOpt = app.HasPlugin("golem15.optional")
|
||||
}
|
||||
*p.log = append(*p.log, p.id+":boot")
|
||||
if p.booted != nil {
|
||||
*p.booted = append(*p.booted, p.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestActivateHasPluginSeesNotYetBootedPlugins(t *testing.T) {
|
||||
var log []string
|
||||
var saw bool
|
||||
hello := &bootProbe{id: "golem15.hello", log: &log, sawOpt: &saw}
|
||||
opt := &bootProbe{id: "golem15.optional", log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
if _, err := activate([]Plugin{opt, hello}, app, []string{"golem15.hello", "golem15.optional"}); err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if !saw {
|
||||
t.Fatal("HasPlugin(golem15.optional) was false during hello Boot; complete set must be visible before Boot")
|
||||
}
|
||||
want := []string{
|
||||
"golem15.hello:register",
|
||||
"golem15.optional:register",
|
||||
"golem15.hello:boot",
|
||||
"golem15.optional:boot",
|
||||
}
|
||||
if strings.Join(log, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("lifecycle = %v, want %v", log, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateIndependentPluginsKeepManifestOrder(t *testing.T) {
|
||||
var log []string
|
||||
a := &tracePlugin{id: "golem15.a", log: &log}
|
||||
b := &tracePlugin{id: "golem15.b", log: &log}
|
||||
c := &tracePlugin{id: "golem15.c", log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
got, err := activate([]Plugin{c, a, b}, app, []string{"golem15.c", "golem15.a", "golem15.b"})
|
||||
if err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if len(got) != 3 || got[0].ID() != "golem15.c" || got[1].ID() != "golem15.a" || got[2].ID() != "golem15.b" {
|
||||
t.Fatalf("order = %v", idsOf(got))
|
||||
}
|
||||
}
|
||||
|
||||
func idsOf(plugins []Plugin) []string {
|
||||
out := make([]string, len(plugins))
|
||||
for i, p := range plugins {
|
||||
out[i] = p.ID()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestActivateRunsAllRegisterBeforeAnyBoot(t *testing.T) {
|
||||
var log []string
|
||||
hello := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
greeter := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
|
||||
got, err := activate([]Plugin{greeter, hello}, app, []string{"golem15.greeter", "golem15.hello"})
|
||||
if err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d plugins, want 2", len(got))
|
||||
}
|
||||
if got[0].ID() != "golem15.hello" || got[1].ID() != "golem15.greeter" {
|
||||
t.Fatalf("order = %s, %s", got[0].ID(), got[1].ID())
|
||||
}
|
||||
|
||||
want := []string{
|
||||
"golem15.hello:register",
|
||||
"golem15.greeter:register",
|
||||
"golem15.hello:boot",
|
||||
"golem15.greeter:boot",
|
||||
}
|
||||
if strings.Join(log, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("lifecycle = %v, want %v", log, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateReorderedManifestStillTopoSorts(t *testing.T) {
|
||||
var log []string
|
||||
hello := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
greeter := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
|
||||
extra := &tracePlugin{id: "golem15.extra", req: []string{"golem15.greeter"}, log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
got, err := activate(
|
||||
[]Plugin{extra, greeter, hello},
|
||||
app,
|
||||
[]string{"golem15.extra", "golem15.hello", "golem15.greeter"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if len(got) != 3 || got[0].ID() != "golem15.hello" || got[1].ID() != "golem15.greeter" || got[2].ID() != "golem15.extra" {
|
||||
t.Fatalf("order = %v, want hello, greeter, extra", idsOf(got))
|
||||
}
|
||||
want := []string{
|
||||
"golem15.hello:register",
|
||||
"golem15.greeter:register",
|
||||
"golem15.extra:register",
|
||||
"golem15.hello:boot",
|
||||
"golem15.greeter:boot",
|
||||
"golem15.extra:boot",
|
||||
}
|
||||
if strings.Join(log, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("lifecycle = %v, want %v", log, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateDuplicateManifestIDRejected(t *testing.T) {
|
||||
var log []string
|
||||
p := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{p}, app, []string{"golem15.hello", "golem15.hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate manifest id error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "golem15.hello") {
|
||||
t.Fatalf("error %q should name the duplicate id", err)
|
||||
}
|
||||
if len(log) != 0 {
|
||||
t.Fatalf("lifecycle ran despite duplicate manifest: %v", log)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateUnregisteredPluginNamesID(t *testing.T) {
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate(nil, app, []string{"golem15.missing"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing registered plugin error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "golem15.missing") {
|
||||
t.Fatalf("error %q should name the missing id", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateEmptyManifestIDRejected(t *testing.T) {
|
||||
var log []string
|
||||
p := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{p}, app, []string{""})
|
||||
if err == nil {
|
||||
t.Fatal("expected empty plugin id error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateNilAppRejected(t *testing.T) {
|
||||
_, err := Activate(nil, []string{"golem15.hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected nil app error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterNilIsNoop(t *testing.T) {
|
||||
Register(nil)
|
||||
}
|
||||
|
||||
func TestActivateHasPluginFalseForUnselected(t *testing.T) {
|
||||
var log []string
|
||||
hello := &tracePlugin{id: "golem15.hello", log: &log}
|
||||
opt := &tracePlugin{id: "golem15.optional", log: &log}
|
||||
app := backpack.New(&compass.Config{})
|
||||
if _, err := activate([]Plugin{hello, opt}, app, []string{"golem15.hello"}); err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if !app.HasPlugin("golem15.hello") {
|
||||
t.Fatal("selected plugin must be present")
|
||||
}
|
||||
if app.HasPlugin("golem15.optional") {
|
||||
t.Fatal("unselected registered plugin must not appear in HasPlugin")
|
||||
}
|
||||
}
|
||||
|
||||
type commandsPlugin struct {
|
||||
tracePlugin
|
||||
}
|
||||
|
||||
func (p *commandsPlugin) Commands() []bonfire.Command {
|
||||
return []bonfire.Command{{
|
||||
Name: "demo:run",
|
||||
Description: "demo command",
|
||||
}}
|
||||
}
|
||||
|
||||
func TestActivateDiscoversHasConfigAndHasCommands(t *testing.T) {
|
||||
var log []string
|
||||
var got int
|
||||
cfgPlugin := &configPlugin{
|
||||
id: "golem15.hello",
|
||||
got: &got,
|
||||
fsys: fstest.MapFS{
|
||||
"config/config.yaml": {Data: []byte("posts_per_page: 4\n")},
|
||||
},
|
||||
}
|
||||
cmdPlugin := &commandsPlugin{tracePlugin: tracePlugin{id: "golem15.demo", log: &log}}
|
||||
plain := &tracePlugin{id: "golem15.plain", log: &log}
|
||||
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: app\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := compass.Load(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
gotPlugins, err := activate([]Plugin{cfgPlugin, cmdPlugin, plain}, app, []string{"golem15.hello", "golem15.demo", "golem15.plain"})
|
||||
if err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if got != 4 {
|
||||
t.Fatalf("HasConfig value during Register = %d, want 4", got)
|
||||
}
|
||||
|
||||
var sawConfig, sawCommands, sawPlainCommands bool
|
||||
for _, p := range gotPlugins {
|
||||
if _, ok := p.(pact.HasConfig); ok && p.ID() == "golem15.hello" {
|
||||
sawConfig = true
|
||||
}
|
||||
if hc, ok := p.(pact.HasCommands); ok {
|
||||
if p.ID() == "golem15.demo" {
|
||||
cmds := hc.Commands()
|
||||
if len(cmds) != 1 || cmds[0].Name != "demo:run" {
|
||||
t.Fatalf("HasCommands commands = %+v", cmds)
|
||||
}
|
||||
sawCommands = true
|
||||
}
|
||||
if p.ID() == "golem15.plain" {
|
||||
sawPlainCommands = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sawConfig {
|
||||
t.Fatal("golem15.hello should satisfy HasConfig")
|
||||
}
|
||||
if !sawCommands {
|
||||
t.Fatal("golem15.demo should satisfy HasCommands")
|
||||
}
|
||||
if sawPlainCommands {
|
||||
t.Fatal("plain plugin must not satisfy HasCommands")
|
||||
}
|
||||
if _, ok := any(plain).(pact.HasConfig); ok {
|
||||
t.Fatal("plain plugin must not satisfy HasConfig")
|
||||
}
|
||||
}
|
||||
|
||||
type langPlugin struct {
|
||||
id string
|
||||
fsys fs.FS
|
||||
got *string
|
||||
}
|
||||
|
||||
func (p *langPlugin) ID() string { return p.id }
|
||||
func (p *langPlugin) Requires() []string { return nil }
|
||||
func (p *langPlugin) Register(*backpack.App) error { return nil }
|
||||
func (p *langPlugin) LangFS() fs.FS { return p.fsys }
|
||||
|
||||
func (p *langPlugin) Boot(app *backpack.App) error {
|
||||
if p.got == nil {
|
||||
return nil
|
||||
}
|
||||
tr, ok := app.Lookup[*phrasebook.Translator]()
|
||||
if !ok {
|
||||
return fmt.Errorf("translator missing at boot")
|
||||
}
|
||||
*p.got = tr.GetIn("en", p.id+"::lang.greeting", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestActivatePublishesTranslatorBeforeBoot(t *testing.T) {
|
||||
var got string
|
||||
p := &langPlugin{
|
||||
id: "golem15.hello",
|
||||
got: &got,
|
||||
fsys: fstest.MapFS{
|
||||
"lang/en/lang.yaml": {Data: []byte("greeting: Hello\n")},
|
||||
},
|
||||
}
|
||||
app := backpack.New(&compass.Config{})
|
||||
if _, err := activate([]Plugin{p}, app, []string{"golem15.hello"}); err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if got != "Hello" {
|
||||
t.Fatalf("boot lookup = %q, want Hello (translator must be published before Boot)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateMalformedLangFailsWithPluginContext(t *testing.T) {
|
||||
p := &langPlugin{
|
||||
id: "golem15.hello",
|
||||
fsys: fstest.MapFS{
|
||||
"lang/en.yaml": {Data: []byte("greeting: Hello\n")},
|
||||
},
|
||||
}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{p}, app, []string{"golem15.hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected malformed catalog error")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "golem15.hello") || !strings.Contains(msg, "malformed path") {
|
||||
t.Fatalf("error %q should name the plugin and path problem", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateMalformedLangLeafNamesFileAndKey(t *testing.T) {
|
||||
p := &langPlugin{
|
||||
id: "golem15.hello",
|
||||
fsys: fstest.MapFS{
|
||||
"lang/en/lang.yaml": {Data: []byte("labels:\n count: 1\n")},
|
||||
},
|
||||
}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{p}, app, []string{"golem15.hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected non-string leaf error")
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, want := range []string{"golem15.hello", "lang/en/lang.yaml", "labels.count", "non-string leaf"} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Fatalf("error %q missing %s", msg, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type mailPlugin struct {
|
||||
id string
|
||||
fsys fs.FS
|
||||
templates []string
|
||||
layouts map[string]string
|
||||
sawMailer *bool
|
||||
}
|
||||
|
||||
func (p *mailPlugin) ID() string { return p.id }
|
||||
func (p *mailPlugin) Requires() []string { return nil }
|
||||
func (p *mailPlugin) Register(*backpack.App) error { return nil }
|
||||
func (p *mailPlugin) MailTemplatesFS() fs.FS { return p.fsys }
|
||||
func (p *mailPlugin) MailTemplates() []string { return p.templates }
|
||||
func (p *mailPlugin) MailLayouts() map[string]string { return p.layouts }
|
||||
|
||||
func (p *mailPlugin) Boot(app *backpack.App) error {
|
||||
if p.sawMailer == nil {
|
||||
return nil
|
||||
}
|
||||
_, ok := app.Lookup[postcard.Mailer]()
|
||||
*p.sawMailer = ok
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestActivatePublishesMailerBeforeBoot(t *testing.T) {
|
||||
var saw bool
|
||||
p := &mailPlugin{
|
||||
id: "golem15.hello",
|
||||
sawMailer: &saw,
|
||||
templates: []string{"golem15.hello::mail.hello"},
|
||||
layouts: map[string]string{"hello": "golem15.hello::mail.layouts.hello"},
|
||||
fsys: fstest.MapFS{
|
||||
"views/mail/hello.htm": {Data: []byte("subject = \"Hi\"\nlayout = \"hello\"\n==\nHi.\n")},
|
||||
"views/mail/layouts/hello.htm": {Data: []byte("name = \"Hello\"\n==\n{{ .Content }}\n==\n<div>{{ .Content }}</div>\n")},
|
||||
},
|
||||
}
|
||||
app := backpack.New(&compass.Config{})
|
||||
if _, err := activate([]Plugin{p}, app, []string{"golem15.hello"}); err != nil {
|
||||
t.Fatalf("activate: %v", err)
|
||||
}
|
||||
if !saw {
|
||||
t.Fatal("mailer must be published before Boot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateMissingMailTemplateFailsBootWithName(t *testing.T) {
|
||||
p := &mailPlugin{
|
||||
id: "golem15.hello",
|
||||
templates: []string{"golem15.hello::mail.hello"},
|
||||
fsys: fstest.MapFS{},
|
||||
}
|
||||
app := backpack.New(&compass.Config{})
|
||||
_, err := activate([]Plugin{p}, app, []string{"golem15.hello"})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing template error")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "party: boot golem15.hello") || !strings.Contains(msg, "golem15.hello::mail.hello") {
|
||||
t.Fatalf("error %q should name the boot plugin and missing template", msg)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user