package pipeline import ( "context" "errors" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) type registryBehaviorCase[M any] struct { name string key string stage ModuleStage wrongStage ModuleStage newRegistry func() any register func(any, string, func() (M, error)) error registerWithSpec func(any, ModuleSpec, func() (M, error)) error build func(any, string) (M, error) spec func(any, string) (ModuleSpec, bool) registeredKeys func(any) []string nilRegister func(string, func() (M, error)) error nilBuild func(string) (M, error) nilSpec func(string) (ModuleSpec, bool) nilRegisteredKey func() []string constructor func(string) func() (M, error) moduleKey func(M) string } func TestChunkerRegistryBehavior(t *testing.T) { runRegistryBehaviorTests(t, registryBehaviorCase[contracts.Chunker]{ name: "ChunkerRegistry", key: "generic-chunker", stage: StageChunk, wrongStage: StageExtract, newRegistry: func() any { return NewChunkerRegistry() }, register: func(registry any, key string, constructor func() (contracts.Chunker, error)) error { return registry.(*ChunkerRegistry).Register(key, constructor) }, registerWithSpec: func(registry any, spec ModuleSpec, constructor func() (contracts.Chunker, error)) error { return registry.(*ChunkerRegistry).RegisterWithSpec(spec, constructor) }, build: func(registry any, key string) (contracts.Chunker, error) { return registry.(*ChunkerRegistry).Build(key) }, spec: func(registry any, key string) (ModuleSpec, bool) { return registry.(*ChunkerRegistry).Spec(key) }, registeredKeys: func(registry any) []string { return registry.(*ChunkerRegistry).RegisteredKeys() }, nilRegister: func(key string, constructor func() (contracts.Chunker, error)) error { var registry *ChunkerRegistry return registry.Register(key, constructor) }, nilBuild: func(key string) (contracts.Chunker, error) { var registry *ChunkerRegistry return registry.Build(key) }, nilSpec: func(key string) (ModuleSpec, bool) { var registry *ChunkerRegistry return registry.Spec(key) }, nilRegisteredKey: func() []string { var registry *ChunkerRegistry return registry.RegisteredKeys() }, constructor: func(key string) func() (contracts.Chunker, error) { return func() (contracts.Chunker, error) { return registryChunker{key: key}, nil } }, moduleKey: func(module contracts.Chunker) string { return module.Key() }, }) } func runRegistryBehaviorTests[M any](t *testing.T, testCase registryBehaviorCase[M]) { t.Helper() t.Run(testCase.name+"/register and build", func(t *testing.T) { registry := testCase.newRegistry() if err := testCase.register(registry, testCase.key, testCase.constructor(testCase.key)); err != nil { t.Fatalf("Register() error = %v, want nil", err) } module, err := testCase.build(registry, testCase.key) if err != nil { t.Fatalf("Build() error = %v, want nil", err) } if got := testCase.moduleKey(module); got != testCase.key { t.Fatalf("module key = %q, want %q", got, testCase.key) } }) t.Run(testCase.name+"/metadata registration and lookup", func(t *testing.T) { registry := testCase.newRegistry() spec := ModuleSpec{ Key: " " + testCase.key + " ", Stage: testCase.stage, ExecutionClass: contracts.ExecutionClassDeterministic, Provides: []string{" beta ", "alpha", "", "beta"}, Requires: []string{" source ", "source", ""}, } if err := testCase.registerWithSpec(registry, spec, testCase.constructor(testCase.key)); err != nil { t.Fatalf("RegisterWithSpec() error = %v, want nil", err) } got, ok := testCase.spec(registry, " "+testCase.key+"\n") if !ok { t.Fatal("Spec() ok = false, want true") } want := ModuleSpec{ Key: testCase.key, Stage: testCase.stage, ExecutionClass: contracts.ExecutionClassDeterministic, Provides: []string{"alpha", "beta"}, Requires: []string{"source"}, } if !reflect.DeepEqual(got, want) { t.Fatalf("Spec() = %#v, want %#v", got, want) } got.Provides[0] = "changed" again, ok := testCase.spec(registry, testCase.key) if !ok { t.Fatal("Spec() after caller mutation ok = false, want true") } if !reflect.DeepEqual(again, want) { t.Fatalf("Spec() after caller mutation = %#v, want %#v", again, want) } }) t.Run(testCase.name+"/default spec from register", func(t *testing.T) { registry := testCase.newRegistry() if err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)); err != nil { t.Fatalf("Register() error = %v, want nil", err) } spec, ok := testCase.spec(registry, testCase.key) if !ok { t.Fatal("Spec() ok = false, want true") } want := ModuleSpec{Key: testCase.key, Stage: testCase.stage, ExecutionClass: contracts.ExecutionClassDeterministic} if !reflect.DeepEqual(spec, want) { t.Fatalf("Spec() = %#v, want %#v", spec, want) } }) t.Run(testCase.name+"/wrong stage rejection", func(t *testing.T) { registry := testCase.newRegistry() err := testCase.registerWithSpec(registry, ModuleSpec{Key: testCase.key, Stage: testCase.wrongStage, ExecutionClass: contracts.ExecutionClassDeterministic}, testCase.constructor(testCase.key)) if err == nil { t.Fatal("RegisterWithSpec() error = nil, want error") } if !strings.Contains(err.Error(), "stage") { t.Fatalf("RegisterWithSpec() error = %q, want stage error", err.Error()) } }) t.Run(testCase.name+"/execution class rejection", func(t *testing.T) { for _, spec := range []ModuleSpec{ {Key: testCase.key, Stage: testCase.stage}, {Key: testCase.key, Stage: testCase.stage, ExecutionClass: contracts.ExecutionClass("unsupported")}, } { registry := testCase.newRegistry() err := testCase.registerWithSpec(registry, spec, testCase.constructor(testCase.key)) if err == nil { t.Fatal("RegisterWithSpec() error = nil, want execution class error") } if !strings.Contains(err.Error(), "execution class") { t.Fatalf("RegisterWithSpec() error = %q, want execution class error", err.Error()) } } }) t.Run(testCase.name+"/key trimming", func(t *testing.T) { registry := testCase.newRegistry() if err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)); err != nil { t.Fatalf("Register() error = %v, want nil", err) } module, err := testCase.build(registry, "\t"+testCase.key+"\n") if err != nil { t.Fatalf("Build() error = %v, want nil", err) } if got := testCase.moduleKey(module); got != testCase.key { t.Fatalf("module key = %q, want %q", got, testCase.key) } }) t.Run(testCase.name+"/empty key rejection", func(t *testing.T) { registry := testCase.newRegistry() err := testCase.register(registry, " \t", testCase.constructor(testCase.key)) if err == nil { t.Fatal("Register() error = nil, want error") } if !strings.Contains(err.Error(), "key must not be empty") { t.Fatalf("Register() error = %q, want empty key error", err.Error()) } }) t.Run(testCase.name+"/duplicate key rejection", func(t *testing.T) { registry := testCase.newRegistry() if err := testCase.register(registry, testCase.key, testCase.constructor(testCase.key)); err != nil { t.Fatalf("Register() error = %v, want nil", err) } err := testCase.register(registry, " "+testCase.key+" ", testCase.constructor(testCase.key)) if err == nil { t.Fatal("Register() error = nil, want error") } if !strings.Contains(err.Error(), "already registered") { t.Fatalf("Register() error = %q, want duplicate key error", err.Error()) } }) t.Run(testCase.name+"/nil constructor rejection", func(t *testing.T) { registry := testCase.newRegistry() err := testCase.register(registry, testCase.key, nil) if err == nil { t.Fatal("Register() error = nil, want error") } if !strings.Contains(err.Error(), "constructor") { t.Fatalf("Register() error = %q, want constructor error", err.Error()) } }) t.Run(testCase.name+"/unknown key build error", func(t *testing.T) { registry := testCase.newRegistry() _, err := testCase.build(registry, "missing") if err == nil { t.Fatal("Build() error = nil, want error") } if !strings.Contains(err.Error(), "not registered") { t.Fatalf("Build() error = %q, want unknown key error", err.Error()) } }) t.Run(testCase.name+"/constructor error wrapping", func(t *testing.T) { registry := testCase.newRegistry() constructorErr := errors.New("constructor failed") if err := testCase.register(registry, testCase.key, func() (M, error) { var zero M return zero, constructorErr }); err != nil { t.Fatalf("Register() error = %v, want nil", err) } _, err := testCase.build(registry, testCase.key) if err == nil { t.Fatal("Build() error = nil, want error") } if !errors.Is(err, constructorErr) { t.Fatalf("Build() error = %v, want wrapped constructor error", err) } if !strings.Contains(err.Error(), testCase.key) { t.Fatalf("Build() error = %q, want key context", err.Error()) } }) t.Run(testCase.name+"/nil module rejection", func(t *testing.T) { registry := testCase.newRegistry() if err := testCase.register(registry, testCase.key, func() (M, error) { var zero M return zero, nil }); err != nil { t.Fatalf("Register() error = %v, want nil", err) } _, err := testCase.build(registry, testCase.key) if err == nil { t.Fatal("Build() error = nil, want error") } if !strings.Contains(err.Error(), "returned nil") { t.Fatalf("Build() error = %q, want nil module error", err.Error()) } }) t.Run(testCase.name+"/key mismatch rejection", func(t *testing.T) { registry := testCase.newRegistry() if err := testCase.register(registry, testCase.key, testCase.constructor("other")); err != nil { t.Fatalf("Register() error = %v, want nil", err) } _, err := testCase.build(registry, testCase.key) if err == nil { t.Fatal("Build() error = nil, want error") } if !strings.Contains(err.Error(), "returned") { t.Fatalf("Build() error = %q, want mismatch error", err.Error()) } }) t.Run(testCase.name+"/sorted registered keys", func(t *testing.T) { registry := testCase.newRegistry() for _, key := range []string{"zeta", "alpha", "middle"} { if err := testCase.register(registry, key, testCase.constructor(key)); err != nil { t.Fatalf("Register(%q) error = %v, want nil", key, err) } } keys := testCase.registeredKeys(registry) want := []string{"alpha", "middle", "zeta"} if !reflect.DeepEqual(keys, want) { t.Fatalf("RegisteredKeys() = %#v, want %#v", keys, want) } keys[0] = "changed" if got := testCase.registeredKeys(registry); !reflect.DeepEqual(got, want) { t.Fatalf("RegisteredKeys() after caller mutation = %#v, want %#v", got, want) } }) t.Run(testCase.name+"/nil registry behavior", func(t *testing.T) { if err := testCase.nilRegister(testCase.key, testCase.constructor(testCase.key)); err == nil { t.Fatal("Register() error = nil, want error") } if _, err := testCase.nilBuild(testCase.key); err == nil { t.Fatal("Build() error = nil, want error") } if _, ok := testCase.nilSpec(testCase.key); ok { t.Fatal("Spec() ok = true, want false") } if keys := testCase.nilRegisteredKey(); keys != nil { t.Fatalf("RegisteredKeys() = %#v, want nil", keys) } }) t.Run(testCase.name+"/unknown spec lookup", func(t *testing.T) { registry := testCase.newRegistry() if _, ok := testCase.spec(registry, "missing"); ok { t.Fatal("Spec() ok = true, want false") } }) } type registryChunker struct { key string } func (chunker registryChunker) Key() string { return chunker.key } func (chunker registryChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil } func (chunker registryChunker) Plan(ctx context.Context, req contracts.ChunkRequest) (contracts.ChunkPlanResult, error) { return contracts.ChunkPlanResult{}, nil } type registryOutputEncoder struct { key string } func (encoder registryOutputEncoder) Key() string { return encoder.key } func (encoder registryOutputEncoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) { return contracts.OutputResult{}, nil }