370 lines
12 KiB
Go
370 lines
12 KiB
Go
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
|
|
}
|