215 lines
12 KiB
Go
215 lines
12 KiB
Go
package locationoccurrences
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
|
locationcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/locationregistry"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics"
|
|
)
|
|
|
|
func TestNormalizeCanonicalizesNamesByIDAndClonesInputs(t *testing.T) {
|
|
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
|
|
locations := registryLocations("The Tavern", "The Tavern")
|
|
normalizer := newNormalizer(t, registryReferences(t, locations))
|
|
input := dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{{
|
|
LocationID: locations.Locations[1].ID, Name: " a tavern ", Kind: dnd.LocationOccurrenceKindVisited,
|
|
SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}, {SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}},
|
|
}}}
|
|
before := cloneList(input)
|
|
result, err := normalizer.Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
occurrence := result.Value.Occurrences[0]
|
|
if occurrence.Name != locations.Locations[1].Name || !reflect.DeepEqual(occurrence.SourceRefs, []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}) {
|
|
t.Fatalf("normalized occurrence = %#v", occurrence)
|
|
}
|
|
if !hasWarning(result.Warnings, ReasonCodeNameCanonicalized) || !hasWarning(result.Warnings, ReasonCodeSourceRefsNormalized) || !reflect.DeepEqual(input, before) {
|
|
t.Fatalf("warnings/input = %#v/%#v", result.Warnings, input)
|
|
}
|
|
second, err := normalizer.Normalize(context.Background(), normalizeRequest(result.Value, doc, contracts.ReferenceSet{}))
|
|
if err != nil || !reflect.DeepEqual(second.Value, result.Value) || len(second.Warnings) != 0 {
|
|
t.Fatalf("second normalization = %#v, %v", second, err)
|
|
}
|
|
result.Value.Occurrences[0].SourceRefs[0].StartUnitID = 999
|
|
if input.Occurrences[0].SourceRefs[0].StartUnitID == 999 {
|
|
t.Fatal("normalized source references share input storage")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeKeepsSameNamedIDsAndDistinctEvidence(t *testing.T) {
|
|
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 50}, {ID: 10}, {ID: 90}}}
|
|
locations := registryLocations("The Tavern", "The Tavern")
|
|
first, second := locations.Locations[0], locations.Locations[1]
|
|
ref := func(unit int) source.SourceRef {
|
|
return source.SourceRef{SourceID: doc.ID, StartUnitID: unit, EndUnitID: unit}
|
|
}
|
|
firstMention := dnd.LocationOccurrence{LocationID: first.ID, Name: first.Name, Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{ref(50)}}
|
|
input := dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{
|
|
{LocationID: second.ID, Name: second.Name, Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{ref(90)}},
|
|
firstMention,
|
|
firstMention,
|
|
{LocationID: first.ID, Name: first.Name, Kind: dnd.LocationOccurrenceKindVisited, SourceRefs: []source.SourceRef{ref(50)}},
|
|
{LocationID: first.ID, Name: first.Name, Kind: dnd.LocationOccurrenceKindPlanned, SourceRefs: []source.SourceRef{ref(50)}},
|
|
{LocationID: first.ID, Name: first.Name, Kind: dnd.LocationOccurrenceKindRecalled, SourceRefs: []source.SourceRef{ref(50)}},
|
|
{LocationID: first.ID, Name: first.Name, Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{ref(10)}},
|
|
{LocationID: first.ID, Name: first.Name, Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{ref(999)}},
|
|
}}
|
|
result, err := newNormalizer(t, registryReferences(t, locations)).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := result.Value.Occurrences
|
|
if len(got) != 7 || got[0].Kind != dnd.LocationOccurrenceKindVisited || got[1].Kind != dnd.LocationOccurrenceKindPlanned || got[2].Kind != dnd.LocationOccurrenceKindRecalled || got[3].Kind != dnd.LocationOccurrenceKindMentioned || got[3].SourceRefs[0].StartUnitID != 50 || got[4].SourceRefs[0].StartUnitID != 10 || got[5].LocationID != second.ID || got[6].SourceRefs[0].StartUnitID != 999 {
|
|
t.Fatalf("canonical occurrences = %#v", got)
|
|
}
|
|
if !hasWarning(result.Warnings, ReasonCodeDuplicateCollapsed) || !hasWarning(result.Warnings, ReasonCodeOccurrencesReordered) {
|
|
t.Fatalf("warnings = %#v", result.Warnings)
|
|
}
|
|
}
|
|
|
|
func TestNormalizePreservesUnknownIDsAndMalformedOperationRegistry(t *testing.T) {
|
|
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}}}
|
|
locations := registryLocations("The Mill")
|
|
unknown := dnd.LocationOccurrence{LocationID: "location:sha256:unknown", Name: "The Mill", Kind: "unexpected", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}
|
|
result, err := newNormalizer(t, registryReferences(t, locations)).Normalize(context.Background(), normalizeRequest(dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{unknown}}, doc, contracts.ReferenceSet{}))
|
|
if err != nil || !reflect.DeepEqual(result.Value.Occurrences[0], unknown) || !hasWarning(result.Warnings, ReasonCodeUnknownLocationID) {
|
|
t.Fatalf("unknown normalization = %#v, %v", result, err)
|
|
}
|
|
malformed := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{LocationRegistryReferenceSlot: {
|
|
Items: []contracts.ReferenceItem{{SlotName: LocationRegistryReferenceSlot, MediaType: "application/json", Content: []byte(`{"private":"registry evidence"}`)}},
|
|
}}}
|
|
if _, err := newNormalizer(t).Normalize(context.Background(), normalizeRequest(dnd.LocationOccurrenceList{}, doc, malformed)); err == nil || !strings.Contains(err.Error(), "resolve location registry") || strings.Contains(err.Error(), "registry evidence") {
|
|
t.Fatalf("operation registry error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
|
|
if _, err := New(Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one reference set") {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
normalizer := newNormalizer(t, registryReferences(t, registryLocations("The Mill")))
|
|
spec := ModuleSpec()
|
|
if spec.Key != Key || spec.Stage != pipeline.StageNormalize || spec.ExecutionClass != contracts.ExecutionClassDeterministic || spec.ArtifactKind != dnd.LocationOccurrenceListKind {
|
|
t.Fatalf("ModuleSpec() = %#v", spec)
|
|
}
|
|
assertLocationRegistryReferenceSlots(t, "ModuleSpec", spec.ReferenceSlots)
|
|
assertLocationRegistryReferenceSlots(t, "Normalizer", normalizer.ReferenceSlots())
|
|
registry := pipeline.NewNormalizerRegistry()
|
|
if err := Register(registry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registeredSpec, ok := registry.Spec(Key)
|
|
if !ok {
|
|
t.Fatalf("registry missing %q", Key)
|
|
}
|
|
assertLocationRegistryReferenceSlots(t, "registered ModuleSpec", registeredSpec.ReferenceSlots)
|
|
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
|
|
t.Fatal("DecodeOptions() accepted unknown options")
|
|
}
|
|
if metadata := normalizer.ManifestMetadata(); metadata["normalization_policy"] != normalizationPolicy || metadata["location_registry_digest"] == "" || metadata["location_count"] != 1 {
|
|
t.Fatalf("metadata = %#v", metadata)
|
|
}
|
|
if fingerprints := normalizer.CheckpointFingerprints(); len(fingerprints) != 2 || fingerprints[1].Name != "location_registry" || fingerprints[1].Value == "" {
|
|
t.Fatalf("fingerprints = %#v", fingerprints)
|
|
}
|
|
if _, err := newNormalizer(t).Normalize(context.Background(), normalizeRequest(dnd.LocationOccurrenceList{}, nil, contracts.ReferenceSet{})); err == nil || !strings.Contains(err.Error(), "required") {
|
|
t.Fatalf("unbound registry error = %v", err)
|
|
}
|
|
|
|
count := diagnostics.MaxWarnings + 5
|
|
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, count)}
|
|
input := dnd.LocationOccurrenceList{Occurrences: make([]dnd.LocationOccurrence, count)}
|
|
location := registryLocations("The Mill").Locations[0]
|
|
for index := range doc.Units {
|
|
doc.Units[index].ID = index + 1
|
|
input.Occurrences[index] = dnd.LocationOccurrence{LocationID: location.ID, Name: "not canonical", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: count - index, EndUnitID: count - index}}}
|
|
}
|
|
bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationRegistry{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
|
|
if err != nil || len(bounded.Warnings) != diagnostics.MaxWarnings || bounded.Warnings[len(bounded.Warnings)-1].ReasonCode != ReasonCodeWarningsOmitted {
|
|
t.Fatalf("bounded warnings = %#v, %v", bounded.Warnings, err)
|
|
}
|
|
}
|
|
|
|
func assertLocationRegistryReferenceSlots(t *testing.T, owner string, slots []contracts.ReferenceSlot) {
|
|
t.Helper()
|
|
if len(slots) != 1 {
|
|
t.Fatalf("%s reference slots = %#v, want one location registry", owner, slots)
|
|
}
|
|
got := slots[0]
|
|
description := got.Description
|
|
got.Description = ""
|
|
want := contracts.ReferenceSlot{
|
|
Name: LocationRegistryReferenceSlot,
|
|
Required: true,
|
|
AcceptedMediaTypes: []string{"application/json"},
|
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind},
|
|
MaxBytes: LocationRegistryMaxBytes,
|
|
}
|
|
if !reflect.DeepEqual(got, want) || strings.TrimSpace(description) == "" {
|
|
t.Fatalf("%s location registry slot = %#v, want contract %#v with a nonempty description", owner, slots[0], want)
|
|
}
|
|
}
|
|
|
|
func newNormalizer(t *testing.T, references ...contracts.ReferenceSet) *Normalizer {
|
|
t.Helper()
|
|
normalizer, err := New(Options{}, references...)
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
return normalizer
|
|
}
|
|
|
|
func normalizeRequest(value dnd.LocationOccurrenceList, doc *source.SourceDocument, references contracts.ReferenceSet) contracts.TypedNormalizeRequest[dnd.LocationOccurrenceList] {
|
|
return contracts.TypedNormalizeRequest[dnd.LocationOccurrenceList]{Source: doc, MergeOutput: contracts.MergeArtifact[dnd.LocationOccurrenceList]{Value: value}, References: references}
|
|
}
|
|
|
|
func registryLocations(names ...string) dnd.LocationRegistry {
|
|
locations := make([]dnd.Location, len(names))
|
|
for index, name := range names {
|
|
refs := []source.SourceRef{{SourceID: "registry", StartUnitID: index + 1, EndUnitID: index + 1}}
|
|
locations[index] = dnd.Location{ID: identity.DeriveID(name, refs), Name: name, SourceRefs: refs}
|
|
}
|
|
return dnd.LocationRegistry{Locations: locations}
|
|
}
|
|
|
|
func registryReferences(t *testing.T, locations dnd.LocationRegistry) contracts.ReferenceSet {
|
|
t.Helper()
|
|
content, err := locationcodec.New().Encode(locations)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{LocationRegistryReferenceSlot: {
|
|
Items: []contracts.ReferenceItem{{SlotName: LocationRegistryReferenceSlot, MediaType: locationcodec.MediaType, Content: content}},
|
|
}}}
|
|
}
|
|
|
|
func cloneList(input dnd.LocationOccurrenceList) dnd.LocationOccurrenceList {
|
|
output := dnd.LocationOccurrenceList{Occurrences: make([]dnd.LocationOccurrence, len(input.Occurrences))}
|
|
for index, occurrence := range input.Occurrences {
|
|
output.Occurrences[index] = occurrence
|
|
output.Occurrences[index].SourceRefs = append([]source.SourceRef(nil), occurrence.SourceRefs...)
|
|
}
|
|
if input.Occurrences == nil {
|
|
output.Occurrences = nil
|
|
}
|
|
return output
|
|
}
|
|
|
|
func hasWarning(warnings []contracts.Warning, code string) bool {
|
|
for _, warning := range warnings {
|
|
if warning.ReasonCode == code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|