Preserve terminal rejection warnings
This commit is contained in:
142
internal/framework/pipeline/runner_rejection_warnings_test.go
Normal file
142
internal/framework/pipeline/runner_rejection_warnings_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user