From be81c92a9c1e0d8de988e5e59985436301ce9dd2 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 8 Nov 2026 14:42:51 +0100 Subject: [PATCH 1/5] test(pluginkit): pin that register fails a plugin that calls itself by another id --- pluginkit/features/boot.feature | 9 ++++++++ pluginkit/features/registration.feature | 14 ++++++++++-- pluginkit/features/steps_boot_test.go | 22 ++++++++++++++++++- pluginkit/features/steps_registration_test.go | 17 +++++++++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/pluginkit/features/boot.feature b/pluginkit/features/boot.feature index f27a9e7..003495d 100644 --- a/pluginkit/features/boot.feature +++ b/pluginkit/features/boot.feature @@ -37,6 +37,15 @@ Feature: Booting plugins and pausing the one that fails Then "events" started And the store records "feed" as paused with "pluginkit: feed register panicked: boom" + @wip + Scenario: A plugin that calls itself by another id is paused under its entry's id + Given the compiled plugins "events" and "feed" + And the Register of "feed" answers a plugin that calls itself "news" + When the application boots its plugins + Then "events" started + And the store records "feed" as paused with "pluginkit: feed register: the plugin calls itself "news"" + And the store holds no record of "news" + Scenario: A plugin whose migration fails is paused and never starts Given the compiled plugins "events" and "feed" And the migration of "feed" fails with "schema exploded" diff --git a/pluginkit/features/registration.feature b/pluginkit/features/registration.feature index 13e9771..7412fef 100644 --- a/pluginkit/features/registration.feature +++ b/pluginkit/features/registration.feature @@ -3,8 +3,9 @@ Feature: Registering compiled plugins one by one every backend plugin's id and Register function, in wiring order, in place of one function that registers them all. The application keeps the plugins it wants on, builds each one's dependencies from its id, - and registers them one by one. A plugin whose Register fails or panics - is reported by its id, and the plugins after it still register. + and registers them one by one. A plugin whose Register fails or panics, + or answers a plugin whose ID is not the entry's id, is reported by the + entry's id, and the plugins after it still register. Scenario: The registration list names each backend plugin in wiring order Given the plugin "events" has a backend @@ -58,6 +59,15 @@ Feature: Registering compiled plugins one by one And "feed" failed at the step "register" with: pluginkit: feed register: the title is too long And the failure of "feed" is not marked as a panic + @wip + Scenario: A plugin that calls itself by another id fails alone + Given the compiled plugins "events", "feed" and "gallery" + And the Register of "feed" answers a plugin that calls itself "news" + When the application registers each plugin + Then "events" and "gallery" are registered, in that order + And "feed" failed at the step "register" with: pluginkit: feed register: the plugin calls itself "news" + And the failure of "feed" is not marked as a panic + Scenario: An error that holds the text of a panic is not marked as a panic Given the compiled plugins "events" and "feed" And the Register of "feed" answers the error "pluginkit: feed register panicked: boom" diff --git a/pluginkit/features/steps_boot_test.go b/pluginkit/features/steps_boot_test.go index 96c7400..cb46645 100644 --- a/pluginkit/features/steps_boot_test.go +++ b/pluginkit/features/steps_boot_test.go @@ -106,6 +106,7 @@ type bootWorld struct { seeds map[string]pluginkit.PluginRecord registerFail map[string]string registerBoom map[string]string + calledAs map[string]string migrateFails map[string]string startFails map[string]string recordFails map[string]string @@ -197,6 +198,9 @@ func (w *bootWorld) registerOf(id string) func(string) (*bootPlugin, error) { if err := w.failure(w.registerFail, id); err != nil { return nil, err } + if name, ok := w.calledAs[id]; ok { + return &bootPlugin{id: name, world: w}, nil + } return &bootPlugin{id: id, world: w}, nil } } @@ -287,6 +291,18 @@ func (w *bootWorld) neverStarted(id string) error { return nil } +// noRecordOf checks that the store holds no record of the plugin id. +func (w *bootWorld) noRecordOf(id string) error { + records, err := w.store.inner.Records(w.t.Context()) + if err != nil { + return err + } + if held, ok := records[id]; ok { + return fmt.Errorf("the store holds %+v, want no record of %s", held, id) + } + return nil +} + // holds checks that the store holds want. func (w *bootWorld) holds(want pluginkit.PluginRecord) error { records, err := w.store.inner.Records(w.t.Context()) @@ -341,9 +357,11 @@ func givenBoot(sc *godog.ScenarioContext, w *bootWorld) { sc.Step(`^the compiled plugins (.+)$`, w.compile) sc.Step(`^the store holds no record$`, w.noRecord) sc.Step(`^the store records (.+) as (active|inactive|not installed)$`, w.seed) - sc.Step(`^the store records "([^"]*)" as paused with "([^"]*)"$`, w.pausedWith) + sc.Step(`^the store records "([^"]*)" as paused with "(.*)"$`, w.pausedWith) sc.Step(`^the Register of "([^"]*)" fails with "([^"]*)"$`, func(id, text string) { set(w.registerFail, id, text) }) sc.Step(`^the Register of "([^"]*)" panics with "([^"]*)"$`, func(id, text string) { set(w.registerBoom, id, text) }) + sc.Step(`^the Register of "([^"]*)" answers a plugin that calls itself "([^"]*)"$`, + func(id, name string) { set(w.calledAs, id, name) }) sc.Step(`^the migration of "([^"]*)" fails with "([^"]*)"$`, func(id, text string) { set(w.migrateFails, id, text) }) sc.Step(`^the Start of "([^"]*)" fails with "([^"]*)"$`, func(id, text string) { set(w.startFails, id, text) }) sc.Step(`^the migration record of "([^"]*)" fails with "([^"]*)"$`, @@ -362,6 +380,7 @@ func thenBoot(sc *godog.ScenarioContext, w *bootWorld) { sc.Step(`^"([^"]*)" never started$`, w.neverStarted) sc.Step(`^the store records "([^"]*)" as (active|inactive|not installed) with no error$`, w.recordsClean) sc.Step(`^the store was not written$`, w.notWritten) + sc.Step(`^the store holds no record of "([^"]*)"$`, w.noRecordOf) sc.Step(`^the Register of "([^"]*)" never ran$`, w.neverRegistered) sc.Step(`^the boot reports that its context ended$`, w.contextEnded) sc.Step(`^the boot fails with: (.+)$`, w.failedWith) @@ -374,6 +393,7 @@ func initializeBoot(t *testing.T) func(*godog.ScenarioContext) { t: t, seeds: map[string]pluginkit.PluginRecord{}, registerFail: map[string]string{}, registerBoom: map[string]string{}, migrateFails: map[string]string{}, startFails: map[string]string{}, recordFails: map[string]string{}, rejected: map[string]bool{}, endsOn: map[string]string{}, + calledAs: map[string]string{}, } givenBoot(sc, w) thenBoot(sc, w) diff --git a/pluginkit/features/steps_registration_test.go b/pluginkit/features/steps_registration_test.go index 625c54f..d6c1760 100644 --- a/pluginkit/features/steps_registration_test.go +++ b/pluginkit/features/steps_registration_test.go @@ -40,6 +40,11 @@ type laneDeps struct{ id string } // lanePlugin is a plugin a scenario registered. type lanePlugin struct{ id string } +// ID returns the id the plugin calls itself. +func (p *lanePlugin) ID() string { + return p.id +} + // registrationFailure is one failure of a registration outcome, its error as text. type registrationFailure struct { ID string @@ -58,6 +63,7 @@ type registrationWorld struct { compiled []string leftOut map[string]bool answers map[string]func() error + calledAs map[string]string depsPanic map[string]string built []string received map[string]laneDeps @@ -254,6 +260,11 @@ func (w *registrationWorld) registerFails(id, text string) { w.answers[id] = func() error { return errors.New(text) } } +// registerCallsItself makes the Register of id answer a plugin whose ID is name. +func (w *registrationWorld) registerCallsItself(id, name string) { + w.calledAs[id] = name +} + // depsPanics makes building the dependencies of id panic with value. func (w *registrationWorld) depsPanics(id, value string) { w.depsPanic[id] = value @@ -280,6 +291,9 @@ func (w *registrationWorld) registerOf(id string) func(laneDeps) (*lanePlugin, e return nil, err } } + if name, ok := w.calledAs[id]; ok { + return &lanePlugin{id: name}, nil + } return &lanePlugin{id: id}, nil } } @@ -414,6 +428,7 @@ func givenRegistration(sc *godog.ScenarioContext, w *registrationWorld) { sc.Step(`^the application leaves "([^"]*)" out$`, w.leaveOut) sc.Step(`^the Register of "([^"]*)" panics with "([^"]*)"$`, w.registerPanics) sc.Step(`^the Register of "([^"]*)" answers the error "([^"]*)"$`, w.registerFails) + sc.Step(`^the Register of "([^"]*)" answers a plugin that calls itself "([^"]*)"$`, w.registerCallsItself) sc.Step(`^building the dependencies of "([^"]*)" panics with "([^"]*)"$`, w.depsPanics) givenBuild(sc, w) } @@ -449,7 +464,7 @@ func initializeRegistration(t *testing.T) func(*godog.ScenarioContext) { w := ®istrationWorld{ t: t, manifests: map[string]map[string]any{}, cfg: registrationConfig, leftOut: map[string]bool{}, answers: map[string]func() error{}, depsPanic: map[string]string{}, received: map[string]laneDeps{}, - sources: map[string]string{}, + sources: map[string]string{}, calledAs: map[string]string{}, } givenRegistration(sc, w) whenRegistration(sc, w) From 28ddde432a251d1090691078094681077e59cc27 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 8 Nov 2026 14:42:52 +0100 Subject: [PATCH 2/5] feat(pluginkit): fail a plugin at register when its ID is not its entry's id --- pluginkit/boot.go | 2 +- pluginkit/registration.go | 21 +++++++++++++----- pluginkit/registration_test.go | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/pluginkit/boot.go b/pluginkit/boot.go index abe971d..5fca55f 100644 --- a/pluginkit/boot.go +++ b/pluginkit/boot.go @@ -28,7 +28,7 @@ func NewBoot(ctx context.Context, store PluginStore) (*Boot, error) { } // Register records each new entry active, registers the active and paused ones, and pauses each that fails. -func Register[D, P any](ctx context.Context, boot *Boot, entries []Registration[D, P], +func Register[D any, P interface{ ID() string }](ctx context.Context, boot *Boot, entries []Registration[D, P], depsFor func(id string) D) (RegisterOutcome[P], error) { wanted, err := admitted(ctx, boot, entries) if err != nil { diff --git a/pluginkit/registration.go b/pluginkit/registration.go index e77105c..65d814a 100644 --- a/pluginkit/registration.go +++ b/pluginkit/registration.go @@ -2,7 +2,10 @@ package pluginkit -import "context" +import ( + "context" + "fmt" +) // Registration is one compiled plugin's manifest id and the function that registers it from its dependencies. type Registration[D, P any] struct { @@ -16,8 +19,10 @@ type RegisterOutcome[P any] struct { Failed []Failure } -// RegisterEach calls each entry's Register with depsFor of its id, each inside its own recover, past any failure. -func RegisterEach[D, P any](entries []Registration[D, P], depsFor func(id string) D) RegisterOutcome[P] { +// RegisterEach calls each entry's Register with depsFor of its id inside a recover, past any failure or mismatched ID. +func RegisterEach[D any, P interface{ ID() string }]( + entries []Registration[D, P], depsFor func(id string) D, +) RegisterOutcome[P] { var outcome RegisterOutcome[P] for _, entry := range entries { plugin, err := register(entry, depsFor(entry.ID)) @@ -31,11 +36,17 @@ func RegisterEach[D, P any](entries []Registration[D, P], depsFor func(id string } // register calls entry's Register with deps inside a recover, naming the plugin in any failure. -func register[D, P any](entry Registration[D, P], deps D) (plugin P, err error) { +func register[D any, P interface{ ID() string }](entry Registration[D, P], deps D) (plugin P, err error) { err = guarded(context.Background(), entry.ID+" "+string(StepRegister), func(context.Context) error { var registerErr error plugin, registerErr = entry.Register(deps) - return registerErr + if registerErr != nil { + return registerErr + } + if id := plugin.ID(); id != entry.ID { + return fmt.Errorf("the plugin calls itself %q", id) + } + return nil }) return plugin, err } diff --git a/pluginkit/registration_test.go b/pluginkit/registration_test.go index f40b044..6de23e1 100644 --- a/pluginkit/registration_test.go +++ b/pluginkit/registration_test.go @@ -18,6 +18,11 @@ type listedPlugin struct { deps listedDeps } +// ID returns the id the plugin calls itself. +func (p *listedPlugin) ID() string { + return p.id +} + // entry is one registration a test lists, its Register noting its call in calls and answering through answer. func entry(id string, calls *[]string, answer func() error) pluginkit.Registration[listedDeps, *listedPlugin] { register := func(deps listedDeps) (*listedPlugin, error) { @@ -140,6 +145,40 @@ func TestRegisterEachGoesOnPastAFailingRegisterAndNamesIt(t *testing.T) { } } +func TestRegisterEachFailsAPluginWhoseIDIsNotItsEntrys(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + plugin *listedPlugin + panicked bool + text string + }{ + "another id": {&listedPlugin{id: "news"}, false, `pluginkit: feed register: the plugin calls itself "news"`}, + "no id": {&listedPlugin{}, false, `pluginkit: feed register: the plugin calls itself ""`}, + "no plugin": { + nil, true, "pluginkit: feed register panicked: runtime error: invalid memory address or nil pointer dereference", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + var calls []string + entries := []pluginkit.Registration[listedDeps, *listedPlugin]{ + entry("events", &calls, succeeds), + {ID: "feed", Register: func(listedDeps) (*listedPlugin, error) { return tc.plugin, nil }}, + } + + outcome := pluginkit.RegisterEach(entries, depsOf(&calls)) + + wantEqual(t, "registered", registeredIDs(outcome), []string{"events"}) + wantEqual(t, "failed", failures(outcome), []failureWant{ + {"feed", pluginkit.StepRegister, tc.panicked, tc.text}, + }) + }) + } +} + func TestRegisterEachWrapsTheErrorARegisterAnswered(t *testing.T) { t.Parallel() From 66afd2b4b7e0540b51a3d5b6161aa440120055aa Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 8 Nov 2026 14:42:53 +0100 Subject: [PATCH 3/5] docs(pluginkit): list the register id check under Unreleased --- pluginkit/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/pluginkit/CHANGELOG.md b/pluginkit/CHANGELOG.md index 4f66526..fb0279b 100644 --- a/pluginkit/CHANGELOG.md +++ b/pluginkit/CHANGELOG.md @@ -9,6 +9,7 @@ Releases of this module are tagged `pluginkit/vX.Y.Z`. Releases up to - `NewBoot`, `Register`, `Boot.Pause` and `Boot.Started` carry one boot from the records to the start: a plugin seen for the first time is recorded active, inactive and not installed plugins are never registered, a plugin that fails to register, migrate or start is paused, and a paused plugin that starts again is active with no error. - A boot whose context ends pauses nothing, and a store read or write that fails is returned to the application. - `pluginstoretest.Run` checks any `PluginStore` against the contract, and `pluginstoretest.NewMemory` is a store in memory that keeps it. +- `RegisterEach` and `Register` take only plugins with an `ID() string` method, and fail at `StepRegister` a plugin whose `ID` is not its entry's id, or a `Register` that answers no plugin, a breaking change. ## 0.12.0 - 2026-11-07 From 3bd1ca2a009c1f3765d718bdeb4d29fe6360c338 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 8 Nov 2026 14:42:53 +0100 Subject: [PATCH 4/5] test(pluginkit): run the register id scenarios --- pluginkit/features/boot.feature | 1 - pluginkit/features/registration.feature | 1 - 2 files changed, 2 deletions(-) diff --git a/pluginkit/features/boot.feature b/pluginkit/features/boot.feature index 003495d..8a209ec 100644 --- a/pluginkit/features/boot.feature +++ b/pluginkit/features/boot.feature @@ -37,7 +37,6 @@ Feature: Booting plugins and pausing the one that fails Then "events" started And the store records "feed" as paused with "pluginkit: feed register panicked: boom" - @wip Scenario: A plugin that calls itself by another id is paused under its entry's id Given the compiled plugins "events" and "feed" And the Register of "feed" answers a plugin that calls itself "news" diff --git a/pluginkit/features/registration.feature b/pluginkit/features/registration.feature index 7412fef..f2ab2b9 100644 --- a/pluginkit/features/registration.feature +++ b/pluginkit/features/registration.feature @@ -59,7 +59,6 @@ Feature: Registering compiled plugins one by one And "feed" failed at the step "register" with: pluginkit: feed register: the title is too long And the failure of "feed" is not marked as a panic - @wip Scenario: A plugin that calls itself by another id fails alone Given the compiled plugins "events", "feed" and "gallery" And the Register of "feed" answers a plugin that calls itself "news" From 17c547953f3ca97de4bd4bc5536e98f59faef5e0 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 8 Nov 2026 15:43:42 +0100 Subject: [PATCH 5/5] fix(pluginkit): fail a Register that answers a nil plugin, whatever its ID reads --- pluginkit/features/registration.feature | 8 +++ pluginkit/features/steps_registration_test.go | 12 +++- pluginkit/registration.go | 21 +++++++ pluginkit/registration_test.go | 63 +++++++++++++++++-- 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/pluginkit/features/registration.feature b/pluginkit/features/registration.feature index f2ab2b9..a6d69d9 100644 --- a/pluginkit/features/registration.feature +++ b/pluginkit/features/registration.feature @@ -67,6 +67,14 @@ Feature: Registering compiled plugins one by one And "feed" failed at the step "register" with: pluginkit: feed register: the plugin calls itself "news" And the failure of "feed" is not marked as a panic + Scenario: A plugin whose Register answers no plugin fails alone + Given the compiled plugins "events", "feed" and "gallery" + And the Register of "feed" answers no plugin + When the application registers each plugin + Then "events" and "gallery" are registered, in that order + And "feed" failed at the step "register" with: pluginkit: feed register: Register answered no plugin + And the failure of "feed" is not marked as a panic + Scenario: An error that holds the text of a panic is not marked as a panic Given the compiled plugins "events" and "feed" And the Register of "feed" answers the error "pluginkit: feed register panicked: boom" diff --git a/pluginkit/features/steps_registration_test.go b/pluginkit/features/steps_registration_test.go index d6c1760..85d2d41 100644 --- a/pluginkit/features/steps_registration_test.go +++ b/pluginkit/features/steps_registration_test.go @@ -65,6 +65,7 @@ type registrationWorld struct { answers map[string]func() error calledAs map[string]string depsPanic map[string]string + noPlugin map[string]bool built []string received map[string]laneDeps registered []string @@ -265,6 +266,11 @@ func (w *registrationWorld) registerCallsItself(id, name string) { w.calledAs[id] = name } +// registerAnswersNothing makes the Register of id answer no plugin and no error. +func (w *registrationWorld) registerAnswersNothing(id string) { + w.noPlugin[id] = true +} + // depsPanics makes building the dependencies of id panic with value. func (w *registrationWorld) depsPanics(id, value string) { w.depsPanic[id] = value @@ -291,6 +297,9 @@ func (w *registrationWorld) registerOf(id string) func(laneDeps) (*lanePlugin, e return nil, err } } + if w.noPlugin[id] { + return nil, nil + } if name, ok := w.calledAs[id]; ok { return &lanePlugin{id: name}, nil } @@ -429,6 +438,7 @@ func givenRegistration(sc *godog.ScenarioContext, w *registrationWorld) { sc.Step(`^the Register of "([^"]*)" panics with "([^"]*)"$`, w.registerPanics) sc.Step(`^the Register of "([^"]*)" answers the error "([^"]*)"$`, w.registerFails) sc.Step(`^the Register of "([^"]*)" answers a plugin that calls itself "([^"]*)"$`, w.registerCallsItself) + sc.Step(`^the Register of "([^"]*)" answers no plugin$`, w.registerAnswersNothing) sc.Step(`^building the dependencies of "([^"]*)" panics with "([^"]*)"$`, w.depsPanics) givenBuild(sc, w) } @@ -464,7 +474,7 @@ func initializeRegistration(t *testing.T) func(*godog.ScenarioContext) { w := ®istrationWorld{ t: t, manifests: map[string]map[string]any{}, cfg: registrationConfig, leftOut: map[string]bool{}, answers: map[string]func() error{}, depsPanic: map[string]string{}, received: map[string]laneDeps{}, - sources: map[string]string{}, calledAs: map[string]string{}, + sources: map[string]string{}, calledAs: map[string]string{}, noPlugin: map[string]bool{}, } givenRegistration(sc, w) whenRegistration(sc, w) diff --git a/pluginkit/registration.go b/pluginkit/registration.go index 65d814a..3ef7025 100644 --- a/pluginkit/registration.go +++ b/pluginkit/registration.go @@ -4,9 +4,14 @@ package pluginkit import ( "context" + "errors" "fmt" + "reflect" ) +// errNoPlugin is the failure of a Register that answered neither a plugin nor an error. +var errNoPlugin = errors.New("Register answered no plugin") + // Registration is one compiled plugin's manifest id and the function that registers it from its dependencies. type Registration[D, P any] struct { ID string @@ -43,6 +48,9 @@ func register[D any, P interface{ ID() string }](entry Registration[D, P], deps if registerErr != nil { return registerErr } + if isNil(plugin) { + return errNoPlugin + } if id := plugin.ID(); id != entry.ID { return fmt.Errorf("the plugin calls itself %q", id) } @@ -50,3 +58,16 @@ func register[D any, P interface{ ID() string }](entry Registration[D, P], deps }) return plugin, err } + +// isNil reports whether plugin holds nothing: a nil interface, pointer, map, slice, channel or function. +func isNil(plugin any) bool { + value := reflect.ValueOf(plugin) + switch value.Kind() { + case reflect.Invalid: + return true + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func: + return value.IsNil() + default: + return false + } +} diff --git a/pluginkit/registration_test.go b/pluginkit/registration_test.go index 6de23e1..4f610aa 100644 --- a/pluginkit/registration_test.go +++ b/pluginkit/registration_test.go @@ -3,6 +3,7 @@ package pluginkit_test import ( + "context" "errors" "testing" @@ -68,7 +69,7 @@ func registeredIDs(outcome pluginkit.RegisterOutcome[*listedPlugin]) []string { } // failures returns the failures of outcome with their errors as text. -func failures(outcome pluginkit.RegisterOutcome[*listedPlugin]) []failureWant { +func failures[P any](outcome pluginkit.RegisterOutcome[P]) []failureWant { got := []failureWant{} for _, failure := range outcome.Failed { got = append(got, failureWant{failure.ID, failure.Step, failure.Panicked, failure.Err.Error()}) @@ -155,9 +156,7 @@ func TestRegisterEachFailsAPluginWhoseIDIsNotItsEntrys(t *testing.T) { }{ "another id": {&listedPlugin{id: "news"}, false, `pluginkit: feed register: the plugin calls itself "news"`}, "no id": {&listedPlugin{}, false, `pluginkit: feed register: the plugin calls itself ""`}, - "no plugin": { - nil, true, "pluginkit: feed register panicked: runtime error: invalid memory address or nil pointer dereference", - }, + "no plugin": {nil, false, "pluginkit: feed register: Register answered no plugin"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { @@ -179,6 +178,62 @@ func TestRegisterEachFailsAPluginWhoseIDIsNotItsEntrys(t *testing.T) { } } +// steadyPlugin is a plugin whose ID reads nothing behind its pointer. +type steadyPlugin struct{} + +// ID returns the id of the entry that registers it. +func (*steadyPlugin) ID() string { + return "feed" +} + +// valuePlugin is a plugin held by value. +type valuePlugin struct{} + +// ID returns the id of the entry that registers it. +func (valuePlugin) ID() string { + return "feed" +} + +// Start starts nothing. +func (valuePlugin) Start(context.Context) error { + return nil +} + +// Stop stops nothing. +func (valuePlugin) Stop(context.Context) error { + return nil +} + +func TestRegisterEachFailsARegisterThatAnswersANilPlugin(t *testing.T) { + t.Parallel() + + var calls []string + want := []failureWant{{"feed", pluginkit.StepRegister, false, "pluginkit: feed register: Register answered no plugin"}} + + pointer := pluginkit.RegisterEach([]pluginkit.Registration[listedDeps, *steadyPlugin]{ + {ID: "feed", Register: func(listedDeps) (*steadyPlugin, error) { return nil, nil }}, + }, depsOf(&calls)) + held := pluginkit.RegisterEach([]pluginkit.Registration[listedDeps, pluginkit.Plugin]{ + {ID: "feed", Register: func(listedDeps) (pluginkit.Plugin, error) { return nil, nil }}, + }, depsOf(&calls)) + + wantEqual(t, "a nil pointer whose ID reads nothing", failures(pointer), want) + wantEqual(t, "a nil interface", failures(held), want) +} + +func TestRegisterEachRegistersAPluginHeldByValue(t *testing.T) { + t.Parallel() + + var calls []string + + outcome := pluginkit.RegisterEach([]pluginkit.Registration[listedDeps, valuePlugin]{ + {ID: "feed", Register: func(listedDeps) (valuePlugin, error) { return valuePlugin{}, nil }}, + }, depsOf(&calls)) + + wantEqual(t, "registered", outcome.Registered, []valuePlugin{{}}) + wantEqual(t, "failed", failures(outcome), []failureWant{}) +} + func TestRegisterEachWrapsTheErrorARegisterAnswered(t *testing.T) { t.Parallel()