package itemoccurrences import ( "sort" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) func TestValidHolderCombination(t *testing.T) { valid := []struct { kind dnd.ItemOccurrenceKind from, to string }{ {dnd.ItemOccurrenceKindDiscovered, "", ""}, {dnd.ItemOccurrenceKindAcquired, "", "party"}, {dnd.ItemOccurrenceKindLost, "party", ""}, {dnd.ItemOccurrenceKindConsumed, "party", ""}, {dnd.ItemOccurrenceKindTransferred, "Aria", "Borin"}, } for _, test := range valid { if !ValidHolderCombination(test.kind, test.from, test.to) { t.Fatalf("ValidHolderCombination(%q, %q, %q) = false", test.kind, test.from, test.to) } } invalid := []struct { kind dnd.ItemOccurrenceKind from, to string }{ {dnd.ItemOccurrenceKindDiscovered, "Aria", ""}, {dnd.ItemOccurrenceKindAcquired, "", ""}, {dnd.ItemOccurrenceKindLost, "", ""}, {dnd.ItemOccurrenceKindConsumed, "", "Borin"}, {dnd.ItemOccurrenceKindTransferred, "party", "Borin"}, {dnd.ItemOccurrenceKindTransferred, "Aria", "Party"}, {dnd.ItemOccurrenceKindTransferred, "Aria", "aria"}, {dnd.ItemOccurrenceKindTransferred, "Åria", "Åria"}, {"unsupported", "", ""}, } for _, test := range invalid { if ValidHolderCombination(test.kind, test.from, test.to) { t.Fatalf("ValidHolderCombination(%q, %q, %q) = true", test.kind, test.from, test.to) } } } func TestLessUsesEveryCanonicalTieBreaker(t *testing.T) { order := testOrder() ref := func(start, end int) []source.SourceRef { return []source.SourceRef{{SourceID: "session", StartUnitID: start, EndUnitID: end}} } quantity := func(value int) *int { return &value } base := dnd.ItemOccurrence{ItemID: "item", Name: "Amulet", Kind: dnd.ItemOccurrenceKindAcquired, To: "Borin", SourceRefs: ref(20, 20)} tests := []struct { name string left, right dnd.ItemOccurrence }{ {"earlier evidence", withRefs(base, ref(10, 10)), base}, {"valid evidence before malformed", base, withRefs(base, ref(999, 999))}, {"normalized name", withName(base, "Amulet"), withName(base, "Blade")}, {"exact trimmed name", withName(base, "Amulet"), withName(base, "amulet")}, {"kind", withKind(base, dnd.ItemOccurrenceKindAcquired), withKind(base, dnd.ItemOccurrenceKindLost)}, {"from presence", withFrom(base, ""), withFrom(base, "Aria")}, {"from normalized value", withFrom(base, "Aria"), withFrom(base, "Borin")}, {"from exact value", withFrom(base, "Aria"), withFrom(base, "aria")}, {"to presence", withoutTo(base), withTo(base, "Borin")}, {"to normalized value", withTo(base, "Aria"), withTo(base, "Borin")}, {"to exact value", withTo(base, "Aria"), withTo(base, "aria")}, {"quantity presence", withQuantity(base, nil), withQuantity(base, quantity(1))}, {"quantity value", withQuantity(base, quantity(1)), withQuantity(base, quantity(2))}, {"canonical source reference sequence", withRefs(base, ref(20, 20)), withRefs(base, ref(30, 30))}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if !Less(order, test.left, test.right) || Less(order, test.right, test.left) { t.Fatalf("Less() did not order %#v before %#v", test.left, test.right) } }) } } func TestLessAndExactEqualityCanonicalizeEvidence(t *testing.T) { order := testOrder() first := dnd.ItemOccurrence{ItemID: "item", Name: " Silver Coin ", Kind: dnd.ItemOccurrenceKindAcquired, To: " party ", SourceRefs: []source.SourceRef{ {SourceID: "session", StartUnitID: 30, EndUnitID: 30}, {SourceID: "session", StartUnitID: 10, EndUnitID: 10}, {SourceID: "session", StartUnitID: 10, EndUnitID: 10}, }, } second := dnd.ItemOccurrence{ItemID: "item", Name: "Silver Coin", Kind: dnd.ItemOccurrenceKindAcquired, To: "party", SourceRefs: []source.SourceRef{ {SourceID: "session", StartUnitID: 10, EndUnitID: 10}, {SourceID: "session", StartUnitID: 30, EndUnitID: 30}, }, } if !ExactEqual(order, first, second) { t.Fatal("ExactEqual() = false, want canonical duplicate") } if Less(order, first, second) || Less(order, second, first) { t.Fatal("Less() distinguished canonically equal records") } if ExactIdentity(order, first) != ExactIdentity(order, second) { t.Fatal("ExactIdentity() differs for canonical duplicates") } canonicalFirst := first canonicalFirst.SourceRefs = order.Canonicalize(first.SourceRefs) if !CanonicalExactEqual(canonicalFirst, second) || CanonicalExactIdentity(canonicalFirst) != CanonicalExactIdentity(second) { t.Fatal("canonical helpers did not recognize canonical duplicates") } if CanonicalExactEqual(first, second) || CanonicalExactIdentity(first) == CanonicalExactIdentity(second) { t.Fatal("canonical helpers repaired noncanonical source references") } quantity := 1 differentQuantity := second differentQuantity.Quantity = &quantity differentEvidence := second differentEvidence.SourceRefs = append([]source.SourceRef(nil), second.SourceRefs...) differentEvidence.SourceRefs[1].EndUnitID = 20 if ExactEqual(order, second, differentQuantity) || ExactEqual(order, second, differentEvidence) { t.Fatal("ExactEqual() collapsed distinct optional field or evidence values") } } func TestSourceReferenceHelpers(t *testing.T) { refs := []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 20}} if !SourceRefsEqual(refs, append([]source.SourceRef(nil), refs...)) || SourceRefsEqual(nil, []source.SourceRef{}) { t.Fatal("SourceRefsEqual() did not preserve source-reference representation") } doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}, {ID: 20}, {ID: 30}}} if !ValidSourceRefs(source.NewDocumentIndex(doc), refs) { t.Fatal("ValidSourceRefs() = false, want valid reference") } if ValidSourceRefs(source.NewDocumentIndex(doc), []source.SourceRef{{SourceID: "other", StartUnitID: 10, EndUnitID: 20}}) { t.Fatal("ValidSourceRefs() = true, want invalid source identifier rejection") } nilEvidence := dnd.ItemOccurrence{ItemID: "item"} emptyEvidence := nilEvidence emptyEvidence.SourceRefs = []source.SourceRef{} if CanonicalExactIdentity(nilEvidence) == CanonicalExactIdentity(emptyEvidence) { t.Fatal("CanonicalExactIdentity() conflated nil and empty source references") } } func TestLessSortsMalformedReferencesDeterministically(t *testing.T) { order := testOrder() occurrences := []dnd.ItemOccurrence{{ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 999, EndUnitID: 999}}}, {ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 1}}}, {ItemID: "item", Name: "A", Kind: dnd.ItemOccurrenceKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}}, } sort.SliceStable(occurrences, func(left, right int) bool { return Less(order, occurrences[left], occurrences[right]) }) if occurrences[0].SourceRefs[0].StartUnitID != 10 || occurrences[1].SourceRefs[0].SourceID != "other" || occurrences[2].SourceRefs[0].StartUnitID != 999 { t.Fatalf("canonical sort = %#v", occurrences) } } func testOrder() shared.SourceRefOrder { return shared.NewSourceRefOrder(&source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}, {ID: 20}, {ID: 30}}}) } func withName(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence { occurrence.Name = value return occurrence } func withKind(occurrence dnd.ItemOccurrence, value dnd.ItemOccurrenceKind) dnd.ItemOccurrence { occurrence.Kind = value return occurrence } func withFrom(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence { occurrence.From = value return occurrence } func withTo(occurrence dnd.ItemOccurrence, value string) dnd.ItemOccurrence { occurrence.To = value return occurrence } func withoutTo(occurrence dnd.ItemOccurrence) dnd.ItemOccurrence { occurrence.To = "" return occurrence } func withQuantity(occurrence dnd.ItemOccurrence, value *int) dnd.ItemOccurrence { occurrence.Quantity = value return occurrence } func withRefs(occurrence dnd.ItemOccurrence, value []source.SourceRef) dnd.ItemOccurrence { occurrence.SourceRefs = value return occurrence }