143 lines
7.0 KiB
Go
143 lines
7.0 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
)
|
|
|
|
type warningChunker struct {
|
|
key string
|
|
plan source.ChunkPlan
|
|
calls int
|
|
}
|
|
|
|
func (c *warningChunker) Key() string { return c.key }
|
|
func (*warningChunker) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
|
func (c *warningChunker) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
|
c.calls++
|
|
return contracts.ChunkPlanResult{
|
|
Plan: source.CloneChunkPlan(c.plan),
|
|
Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", c.calls), ReasonCode: "operation", Message: "operation warning"}},
|
|
}, nil
|
|
}
|
|
|
|
type chunkValidationFunc struct {
|
|
name string
|
|
validate func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error)
|
|
}
|
|
|
|
func (v chunkValidationFunc) Name() string { return v.name }
|
|
func (chunkValidationFunc) ExecutionClass() contracts.ExecutionClass {
|
|
return contracts.ExecutionClassDeterministic
|
|
}
|
|
func (v chunkValidationFunc) Validate(ctx context.Context, request contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
|
|
return v.validate(ctx, request)
|
|
}
|
|
|
|
func TestRunnerPromotesOnlyTerminalRejectionWarnings(t *testing.T) {
|
|
for _, target := range []ModuleStage{StageChunk, StageExtract, StageMerge, StageNormalize} {
|
|
t.Run(string(target), func(t *testing.T) {
|
|
prepared := preparedAttemptDebugPipeline(t)
|
|
lane := &prepared.Steps[0].lanes[0]
|
|
attempts := 0
|
|
first := func() contracts.ValidationResult {
|
|
return contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("validator-%d", attempts), ReasonCode: "validator", Message: "validator warning"}}}
|
|
}
|
|
reject := func() contracts.ValidationResult {
|
|
return contracts.ValidationResult{Approved: false, ReasonCode: "rejected", Message: "rejected"}
|
|
}
|
|
debug := newCapturedDebugRecorder()
|
|
recorder := &extractCaptureRecorder{CheckpointRecorder: NoopCheckpointRecorder()}
|
|
|
|
switch target {
|
|
case StageChunk:
|
|
chunker := prepared.chunker.(*typedTestChunker)
|
|
prepared.chunker = &warningChunker{key: prepared.resolved.Chunk.Module, plan: source.CloneChunkPlan(chunker.plan)}
|
|
prepared.resolved.Chunk.Retries = 1
|
|
prepared.chunkValidators.validators = []preparedValidator{
|
|
{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetChunk}, chunk: chunkValidationFunc{name: "warning-approval", validate: func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
|
|
return first(), nil
|
|
}}},
|
|
{resolved: ResolvedValidator{Binding: Binding("warning-rejection"), Target: ValidatorTargetChunk}, chunk: chunkValidationFunc{name: "warning-rejection", validate: func(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
|
|
return reject(), nil
|
|
}}},
|
|
}
|
|
chunkerWithWarnings := prepared.chunker.(*warningChunker)
|
|
first = func() contracts.ValidationResult {
|
|
return contracts.ValidationResult{Approved: true, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("validator-%d", chunkerWithWarnings.calls), ReasonCode: "validator", Message: "validator warning"}}}
|
|
}
|
|
case StageExtract:
|
|
lane.resolved.Extract.Retries = 1
|
|
installExtractOperation(prepared, 0, func(context.Context, contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
|
attempts++
|
|
return erasedTypedResult{Value: codecNotes{Items: []string{"extract"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}}, nil
|
|
})
|
|
lane.extractValidators.validators = rejectionWarningTypedValidators(first, reject)
|
|
case StageMerge:
|
|
lane.resolved.Merge.Retries = 1
|
|
lane.typed.merge = func(context.Context, any, contracts.TypedMergeRequest[any]) (erasedTypedResult, error) {
|
|
attempts++
|
|
return erasedTypedResult{Value: codecNotes{Items: []string{"merge"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}}, nil
|
|
}
|
|
lane.mergeValidators.validators = rejectionWarningTypedValidators(first, reject)
|
|
case StageNormalize:
|
|
lane.resolved.Normalize.Retries = 1
|
|
lane.typed.normalize = func(context.Context, any, contracts.TypedNormalizeRequest[any]) (erasedTypedResult, error) {
|
|
attempts++
|
|
return erasedTypedResult{Value: codecNotes{Items: []string{"normalize"}}, Warnings: []contracts.Warning{{Scope: fmt.Sprintf("operation-%d", attempts), ReasonCode: "operation", Message: "operation warning"}}}, nil
|
|
}
|
|
lane.normalizeValidators.validators = rejectionWarningTypedValidators(first, reject)
|
|
}
|
|
|
|
output, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input"), Checkpoints: recorder, Debug: debug})
|
|
if err != nil {
|
|
t.Fatalf("Run() error = %v, want nil", err)
|
|
}
|
|
wantScopes := []string{"operation-2", "validator-2"}
|
|
if got := rejectionWarningScopes(output.Warnings); !reflect.DeepEqual(got, wantScopes) {
|
|
t.Fatalf("published warning scopes = %#v, want %#v", got, wantScopes)
|
|
}
|
|
if len(output.Rejected) != 1 || output.Rejected[0].AttemptCount != 2 {
|
|
t.Fatalf("rejections = %#v, want final rejection after two attempts", output.Rejected)
|
|
}
|
|
if target == StageExtract && !reflect.DeepEqual(rejectionWarningScopes(recorder.checkpoint.Warnings), wantScopes) {
|
|
t.Fatalf("extract checkpoint warnings = %#v, want %#v", recorder.checkpoint.Warnings, wantScopes)
|
|
}
|
|
attemptPath := fmt.Sprintf("%s/notes/attempt-01.json", target)
|
|
if target == StageChunk {
|
|
attemptPath = "chunk/attempt-01.json"
|
|
} else if target == StageExtract {
|
|
attemptPath = "extract/notes/chunk-000001/attempt-01.json"
|
|
}
|
|
if !strings.Contains(string(debug.json[attemptPath]), "operation-1") || !strings.Contains(string(debug.json[attemptPath]), "validator-1") {
|
|
t.Fatalf("first attempt debug = %s, want discarded warnings", debug.json[attemptPath])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func rejectionWarningTypedValidators(first func() contracts.ValidationResult, reject func() contracts.ValidationResult) []preparedValidator {
|
|
return []preparedValidator{
|
|
{resolved: ResolvedValidator{Binding: Binding("warning-approval"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
|
|
return first(), nil
|
|
}},
|
|
{resolved: ResolvedValidator{Binding: Binding("warning-rejection"), Target: ValidatorTargetTyped, ArtifactKind: "test/notes"}, typedValidate: func(context.Context, any, typedValidationTarget) (contracts.ValidationResult, error) {
|
|
return reject(), nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
func rejectionWarningScopes(warnings []contracts.Warning) []string {
|
|
scopes := make([]string, len(warnings))
|
|
for index := range warnings {
|
|
scopes[index] = warnings[index].Scope
|
|
}
|
|
return scopes
|
|
}
|