199 lines
9.6 KiB
Go
199 lines
9.6 KiB
Go
package contracts
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestProducerDiagnosticValidationAcceptsClassificationMatrix(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
disposition DiagnosticDisposition
|
|
category DiagnosticCategory
|
|
}{
|
|
{name: "configuration warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryConfiguration},
|
|
{name: "degradation warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryDegradation},
|
|
{name: "incomplete validation warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryValidationIncomplete},
|
|
{name: "fallback warning", disposition: DiagnosticDispositionWarning, category: DiagnosticCategoryFallback},
|
|
{name: "quality advisory", disposition: DiagnosticDispositionAdvisory, category: DiagnosticCategoryDataQuality},
|
|
{name: "normalization observation", disposition: DiagnosticDispositionObservation, category: DiagnosticCategoryNormalization},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
diagnostic := validProducerDiagnostic()
|
|
diagnostic.Disposition = test.disposition
|
|
diagnostic.Category = test.category
|
|
if err := diagnostic.Validate(); err != nil {
|
|
t.Fatalf("Validate() error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProducerDiagnosticValidationRejectsInvalidFieldsAndCounts(t *testing.T) {
|
|
tooLongReason := strings.Repeat("r", MaxDiagnosticReasonCodeBytes+1)
|
|
tooLongScope := strings.Repeat("s", MaxDiagnosticScopeBytes+1)
|
|
tooLongMessage := strings.Repeat("m", MaxDiagnosticMessageBytes+1)
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(*ProducerDiagnostic)
|
|
}{
|
|
{name: "invalid classification", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Category = DiagnosticCategoryDataQuality }},
|
|
{name: "blank reason", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = " \t" }},
|
|
{name: "invalid reason UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = string([]byte{0xff}) }},
|
|
{name: "oversized reason", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.ReasonCode = tooLongReason }},
|
|
{name: "blank scope", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = "\n" }},
|
|
{name: "invalid scope UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = string([]byte{0xff}) }},
|
|
{name: "oversized scope", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Scope = tooLongScope }},
|
|
{name: "blank message", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = " " }},
|
|
{name: "invalid message UTF-8", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = string([]byte{0xff}) }},
|
|
{name: "oversized message", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.Samples[0].Message = tooLongMessage }},
|
|
{name: "zero occurrences", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.OccurrenceCount = 0 }},
|
|
{name: "missing samples", mutate: func(diagnostic *ProducerDiagnostic) {
|
|
diagnostic.Samples = nil
|
|
diagnostic.OmittedSampleCount = diagnostic.OccurrenceCount
|
|
}},
|
|
{name: "too many samples", mutate: func(diagnostic *ProducerDiagnostic) {
|
|
diagnostic.OccurrenceCount = 4
|
|
diagnostic.Samples = []DiagnosticSample{{Scope: "one", Message: "one"}, {Scope: "two", Message: "two"}, {Scope: "three", Message: "three"}, {Scope: "four", Message: "four"}}
|
|
diagnostic.OmittedSampleCount = 0
|
|
}},
|
|
{name: "duplicate samples", mutate: func(diagnostic *ProducerDiagnostic) {
|
|
diagnostic.OccurrenceCount = 2
|
|
diagnostic.Samples = []DiagnosticSample{{Scope: "scope", Message: "message"}, {Scope: "scope", Message: "message"}}
|
|
diagnostic.OmittedSampleCount = 0
|
|
}},
|
|
{name: "inconsistent omission", mutate: func(diagnostic *ProducerDiagnostic) { diagnostic.OmittedSampleCount = 1 }},
|
|
{name: "producer chunk context", mutate: func(diagnostic *ProducerDiagnostic) { chunkIndex := 0; diagnostic.Samples[0].ChunkIndex = &chunkIndex }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
diagnostic := validProducerDiagnostic()
|
|
test.mutate(&diagnostic)
|
|
if err := diagnostic.Validate(); err == nil {
|
|
t.Fatal("Validate() error = nil, want invalid diagnostic error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateProducerDiagnosticsEnforcesLocalGroupBound(t *testing.T) {
|
|
diagnostics := make([]ProducerDiagnostic, MaxProducerDiagnosticGroups)
|
|
for index := range diagnostics {
|
|
diagnostics[index] = validProducerDiagnostic()
|
|
diagnostics[index].ReasonCode = "reason-" + string(rune('a'+index))
|
|
}
|
|
if err := ValidateProducerDiagnostics(diagnostics); err != nil {
|
|
t.Fatalf("ValidateProducerDiagnostics() error = %v", err)
|
|
}
|
|
diagnostics = append(diagnostics, validProducerDiagnostic())
|
|
if err := ValidateProducerDiagnostics(diagnostics); err == nil {
|
|
t.Fatal("ValidateProducerDiagnostics() error = nil, want excessive-group error")
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticGroupValidationPreservesChunkIndexZero(t *testing.T) {
|
|
chunkIndex := 0
|
|
group := DiagnosticGroup{
|
|
Disposition: DiagnosticDispositionAdvisory,
|
|
Category: DiagnosticCategoryDataQuality,
|
|
ReasonCode: "unresolved",
|
|
Origin: DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells", ValidatorKey: "dnd/spells/source-relatedness"},
|
|
OccurrenceCount: 1,
|
|
Samples: []DiagnosticSample{{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &chunkIndex}},
|
|
}
|
|
if err := group.Validate(); err != nil {
|
|
t.Fatalf("Validate() error = %v", err)
|
|
}
|
|
collection := DiagnosticCollection{Groups: []DiagnosticGroup{group}}
|
|
if err := collection.Validate(); err != nil {
|
|
t.Fatalf("DiagnosticCollection.Validate() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticGroupRejectsRepeatedSampleWithEqualChunkIndex(t *testing.T) {
|
|
firstIndex := 0
|
|
secondIndex := 0
|
|
group := DiagnosticGroup{
|
|
Disposition: DiagnosticDispositionAdvisory,
|
|
Category: DiagnosticCategoryDataQuality,
|
|
ReasonCode: "unresolved",
|
|
Origin: DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"},
|
|
OccurrenceCount: 2,
|
|
Samples: []DiagnosticSample{
|
|
{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &firstIndex},
|
|
{Scope: "spells[0]", Message: "Spell was not found", ChunkID: "chunk-1", ChunkIndex: &secondIndex},
|
|
},
|
|
}
|
|
if err := group.Validate(); err == nil {
|
|
t.Fatal("Validate() error = nil, want duplicate sample error")
|
|
}
|
|
}
|
|
|
|
func TestCloneDiagnosticCollectionOwnsGroupsAndChunkIndex(t *testing.T) {
|
|
chunkIndex := 0
|
|
collection := DiagnosticCollection{Groups: []DiagnosticGroup{{
|
|
Disposition: DiagnosticDispositionAdvisory,
|
|
Category: DiagnosticCategoryDataQuality,
|
|
ReasonCode: "unresolved",
|
|
Origin: DiagnosticOrigin{Stage: DiagnosticOriginStageExtract, StepID: "extract", LaneID: "spells", ModuleKey: "dnd/spells"},
|
|
OccurrenceCount: 1,
|
|
Samples: []DiagnosticSample{{Scope: "spells[0]", Message: "Spell was not found", ChunkIndex: &chunkIndex}},
|
|
}}}
|
|
cloned := CloneDiagnosticCollection(collection)
|
|
collection.Groups[0].Samples[0].Message = "changed"
|
|
*collection.Groups[0].Samples[0].ChunkIndex = 1
|
|
if got := cloned.Groups[0].Samples[0]; got.Message != "Spell was not found" || got.ChunkIndex == nil || *got.ChunkIndex != 0 {
|
|
t.Fatalf("cloned sample = %#v, want independently owned original", got)
|
|
}
|
|
}
|
|
|
|
func TestProjectDiagnosticCollectionPartitionsAndChecksTotals(t *testing.T) {
|
|
warning := validDiagnosticGroup(DiagnosticDispositionWarning, DiagnosticCategoryFallback, "fallback", 2)
|
|
diagnostic := validDiagnosticGroup(DiagnosticDispositionAdvisory, DiagnosticCategoryDataQuality, "quality", 3)
|
|
collection := DiagnosticCollection{
|
|
Groups: []DiagnosticGroup{warning, diagnostic},
|
|
Truncated: true,
|
|
UnrepresentedOccurrenceCount: 4,
|
|
}
|
|
projection, err := ProjectDiagnosticCollection(collection)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(projection.Warnings) != 1 || len(projection.Diagnostics) != 1 || projection.WarningOccurrenceCount != 2 || projection.DiagnosticOccurrenceCount != 7 {
|
|
t.Fatalf("projection = %#v, want partitioned exact totals", projection)
|
|
}
|
|
collection.Groups[0].Samples[0].Message = "mutated"
|
|
if projection.Warnings[0].Samples[0].Message != "message" {
|
|
t.Fatal("projection retained caller-owned sample storage")
|
|
}
|
|
|
|
overflow := DiagnosticCollection{Groups: []DiagnosticGroup{
|
|
validDiagnosticGroup(DiagnosticDispositionWarning, DiagnosticCategoryFallback, "first", int(^uint(0)>>1)),
|
|
validDiagnosticGroup(DiagnosticDispositionWarning, DiagnosticCategoryFallback, "second", 1),
|
|
}}
|
|
if _, err := ProjectDiagnosticCollection(overflow); err == nil {
|
|
t.Fatal("ProjectDiagnosticCollection() overflow error = nil")
|
|
}
|
|
}
|
|
|
|
func validDiagnosticGroup(disposition DiagnosticDisposition, category DiagnosticCategory, reason string, occurrences int) DiagnosticGroup {
|
|
return DiagnosticGroup{
|
|
Disposition: disposition,
|
|
Category: category,
|
|
ReasonCode: reason,
|
|
Origin: DiagnosticOrigin{Stage: DiagnosticOriginStageNormalize, StepID: "step", LaneID: "lane", ModuleKey: "module"},
|
|
OccurrenceCount: occurrences,
|
|
Samples: []DiagnosticSample{{Scope: "scope", Message: "message"}},
|
|
OmittedSampleCount: occurrences - 1,
|
|
}
|
|
}
|
|
|
|
func validProducerDiagnostic() ProducerDiagnostic {
|
|
return ProducerDiagnostic{
|
|
Disposition: DiagnosticDispositionWarning,
|
|
Category: DiagnosticCategoryConfiguration,
|
|
ReasonCode: "empty_reference",
|
|
OccurrenceCount: 1,
|
|
Samples: []DiagnosticSample{{Scope: "references.glossary", Message: "Reference is empty"}},
|
|
}
|
|
}
|