Add a framework mechanism for prepared modules and validators to contribute checkpoint identity fingerprints
This commit is contained in:
@@ -72,6 +72,167 @@ func TestPrepareConstructsEverythingInStableOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparedCheckpointFingerprintsCollectEveryComponentInStableScopeOrder(t *testing.T) {
|
||||
provider := func(value string) checkpointFingerprintTestProvider {
|
||||
return checkpointFingerprintTestProvider{{Name: "identity", Value: value}}
|
||||
}
|
||||
prepared := &PreparedPipeline{
|
||||
resolved: ResolvedPipeline{
|
||||
ID: "fingerprints",
|
||||
Input: Binding("input"),
|
||||
Chunk: Binding("chunk"),
|
||||
Output: Binding("output"),
|
||||
},
|
||||
input: provider("input"),
|
||||
chunker: provider("chunk"),
|
||||
chunkValidators: preparedValidatorChain{validators: []preparedValidator{{
|
||||
resolved: ResolvedValidator{Binding: Binding("chunk-validator"), Target: ValidatorTargetChunk},
|
||||
chunk: provider("chunk-validator"),
|
||||
}}},
|
||||
output: provider("output"),
|
||||
}
|
||||
lane := preparedLaneExecutor{
|
||||
resolved: ResolvedArtifactLane{
|
||||
ID: "spells",
|
||||
Extract: Binding("extract"),
|
||||
Merge: Binding("merge"),
|
||||
Normalize: Binding("normalize"),
|
||||
},
|
||||
typed: &preparedTypedLane{
|
||||
extractor: provider("extract"),
|
||||
merger: provider("merge"),
|
||||
normalizer: provider("normalize"),
|
||||
},
|
||||
extractValidators: fingerprintTestValidatorChain("extract-validator", provider("extract-validator")),
|
||||
mergeValidators: fingerprintTestValidatorChain("merge-validator", provider("merge-validator")),
|
||||
normalizeValidators: fingerprintTestValidatorChain("normalize-validator", provider("normalize-validator")),
|
||||
}
|
||||
prepared.lanes = []preparedLaneExecutor{lane}
|
||||
|
||||
first, err := collectPreparedCheckpointFingerprints(prepared)
|
||||
if err != nil {
|
||||
t.Fatalf("collectPreparedCheckpointFingerprints() error = %v, want nil", err)
|
||||
}
|
||||
second, err := collectPreparedCheckpointFingerprints(prepared)
|
||||
if err != nil {
|
||||
t.Fatalf("second collection error = %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(first, second) {
|
||||
t.Fatalf("fingerprints are not deterministic: %#v / %#v", first, second)
|
||||
}
|
||||
if len(first) != 10 {
|
||||
t.Fatalf("fingerprints = %#v, want all 10 prepared components", first)
|
||||
}
|
||||
for index := 1; index < len(first); index++ {
|
||||
if first[index-1].Name >= first[index].Name {
|
||||
t.Fatalf("fingerprints are not sorted: %#v", first)
|
||||
}
|
||||
}
|
||||
prepared.checkpointFingerprints = first
|
||||
copy := prepared.CheckpointFingerprints()
|
||||
copy[0].Value = "mutated"
|
||||
if reflect.DeepEqual(copy, prepared.CheckpointFingerprints()) {
|
||||
t.Fatal("CheckpointFingerprints() exposed mutable backing storage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparedCheckpointFingerprintsValidateProviderValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fingerprints []CheckpointFingerprint
|
||||
want string
|
||||
}{
|
||||
{name: "empty name", fingerprints: []CheckpointFingerprint{{Value: "value"}}, want: "non-empty name and value"},
|
||||
{name: "empty value", fingerprints: []CheckpointFingerprint{{Name: "name"}}, want: "non-empty name and value"},
|
||||
{name: "duplicate after trimming", fingerprints: []CheckpointFingerprint{{Name: "same", Value: "one"}, {Name: " same ", Value: "two"}}, want: "duplicated"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
prepared := &PreparedPipeline{
|
||||
resolved: ResolvedPipeline{ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output")},
|
||||
input: checkpointFingerprintTestProvider(test.fingerprints),
|
||||
}
|
||||
_, err := collectPreparedCheckpointFingerprints(prepared)
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("error = %v, want substring %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparedCheckpointFingerprintsRemainEmptyWithoutProviders(t *testing.T) {
|
||||
registries, _ := constructionRegistries(t, nil, nil)
|
||||
resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prepared, err := Prepare(resolved, registries, ModuleDependencies{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := prepared.CheckpointFingerprints(); len(got) != 0 {
|
||||
t.Fatalf("CheckpointFingerprints() = %#v, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparedCheckpointFingerprintsScopeRepeatedValidatorsByPosition(t *testing.T) {
|
||||
provider := checkpointFingerprintTestProvider{{Name: "identity", Value: "same"}}
|
||||
validator := func() preparedValidator {
|
||||
return preparedValidator{resolved: ResolvedValidator{Binding: Binding("configured"), Target: ValidatorTargetChunk}, chunk: provider}
|
||||
}
|
||||
prepared := &PreparedPipeline{
|
||||
resolved: ResolvedPipeline{ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output")},
|
||||
chunkValidators: preparedValidatorChain{validators: []preparedValidator{validator(), validator()}},
|
||||
}
|
||||
got, err := collectPreparedCheckpointFingerprints(prepared)
|
||||
if err != nil {
|
||||
t.Fatalf("collect repeated validators: %v", err)
|
||||
}
|
||||
if len(got) != 2 || got[0].Name == got[1].Name {
|
||||
t.Fatalf("fingerprints = %#v, want independently scoped repeated validators", got)
|
||||
}
|
||||
}
|
||||
|
||||
type checkpointFingerprintTestProvider []CheckpointFingerprint
|
||||
|
||||
func (p checkpointFingerprintTestProvider) CheckpointFingerprints() []CheckpointFingerprint {
|
||||
return append([]CheckpointFingerprint(nil), p...)
|
||||
}
|
||||
func (checkpointFingerprintTestProvider) Key() string { return "fingerprint-test" }
|
||||
func (checkpointFingerprintTestProvider) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
||||
func (checkpointFingerprintTestProvider) Parse(context.Context, contracts.ParseRequest) (*source.SourceDocument, error) {
|
||||
return typedTestDocument(), nil
|
||||
}
|
||||
func (checkpointFingerprintTestProvider) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
||||
return contracts.ChunkPlanResult{}, nil
|
||||
}
|
||||
func (checkpointFingerprintTestProvider) Encode(context.Context, contracts.OutputRequest) (contracts.OutputResult, error) {
|
||||
return contracts.OutputResult{}, nil
|
||||
}
|
||||
func (checkpointFingerprintTestProvider) Name() string { return "fingerprint-test" }
|
||||
func (checkpointFingerprintTestProvider) ExecutionClass() contracts.ExecutionClass {
|
||||
return contracts.ExecutionClassDeterministic
|
||||
}
|
||||
func (checkpointFingerprintTestProvider) Validate(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
|
||||
type checkpointFingerprintOutput struct {
|
||||
contracts.OutputEncoder
|
||||
fingerprints []CheckpointFingerprint
|
||||
}
|
||||
|
||||
func (o checkpointFingerprintOutput) CheckpointFingerprints() []CheckpointFingerprint {
|
||||
return append([]CheckpointFingerprint(nil), o.fingerprints...)
|
||||
}
|
||||
|
||||
func fingerprintTestValidatorChain(key string, provider checkpointFingerprintTestProvider) preparedValidatorChain {
|
||||
return preparedValidatorChain{validators: []preparedValidator{{
|
||||
resolved: ResolvedValidator{Binding: Binding(key), Target: ValidatorTargetTyped},
|
||||
typed: provider,
|
||||
}}}
|
||||
}
|
||||
|
||||
func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) {
|
||||
var built []string
|
||||
var observations []constructionBuildObservation
|
||||
@@ -183,9 +344,26 @@ func TestPrepareFailuresOccurBeforeInputParse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareRejectsInvalidComponentCheckpointFingerprint(t *testing.T) {
|
||||
failure := &constructionFailure{outputFingerprints: []CheckpointFingerprint{{Name: "identity"}}}
|
||||
registries, input := constructionRegistries(t, nil, failure)
|
||||
resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = Prepare(resolved, registries, ModuleDependencies{})
|
||||
if err == nil || !strings.Contains(err.Error(), "checkpoint fingerprint") || !strings.Contains(err.Error(), "output:output") {
|
||||
t.Fatalf("Prepare() error = %v, want scoped checkpoint fingerprint error", err)
|
||||
}
|
||||
if len(input.requests) != 0 {
|
||||
t.Fatalf("input Parse calls = %d, want zero", len(input.requests))
|
||||
}
|
||||
}
|
||||
|
||||
type constructionFailure struct {
|
||||
requireExtractorLLM bool
|
||||
output error
|
||||
outputFingerprints []CheckpointFingerprint
|
||||
}
|
||||
|
||||
func constructionProfile() PipelineProfile {
|
||||
@@ -298,7 +476,11 @@ func constructionRegistriesWithHooks(t *testing.T, built *[]string, failure *con
|
||||
if failure.output != nil {
|
||||
return nil, failure.output
|
||||
}
|
||||
return &typedTestOutput{key: "output"}, nil
|
||||
output := contracts.OutputEncoder(&typedTestOutput{key: "output"})
|
||||
if failure.outputFingerprints != nil {
|
||||
output = checkpointFingerprintOutput{OutputEncoder: output, fingerprints: failure.outputFingerprints}
|
||||
}
|
||||
return output, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user