package party import ( "io/fs" "os" "path/filepath" "strings" "testing" "testing/fstest" "git.golem15.com/golem15/summercms/backpack" "git.golem15.com/golem15/summercms/compass" ) 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) } }