package itemevents 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.ItemEventKind from, to string }{ {dnd.ItemEventKindDiscovered, "", ""}, {dnd.ItemEventKindAcquired, "", "party"}, {dnd.ItemEventKindLost, "party", ""}, {dnd.ItemEventKindConsumed, "party", ""}, {dnd.ItemEventKindTransferred, "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.ItemEventKind from, to string }{ {dnd.ItemEventKindDiscovered, "Aria", ""}, {dnd.ItemEventKindAcquired, "", ""}, {dnd.ItemEventKindLost, "", ""}, {dnd.ItemEventKindConsumed, "", "Borin"}, {dnd.ItemEventKindTransferred, "party", "Borin"}, {dnd.ItemEventKindTransferred, "Aria", "Party"}, {dnd.ItemEventKindTransferred, "Aria", "aria"}, {dnd.ItemEventKindTransferred, "Å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.ItemEvent{Name: "Amulet", Kind: dnd.ItemEventKindAcquired, To: "Borin", SourceRefs: ref(20, 20)} tests := []struct { name string left, right dnd.ItemEvent }{ {"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.ItemEventKindAcquired), withKind(base, dnd.ItemEventKindLost)}, {"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.ItemEvent{ Name: " Silver Coin ", Kind: dnd.ItemEventKindAcquired, 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.ItemEvent{ Name: "Silver Coin", Kind: dnd.ItemEventKindAcquired, 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") } 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") } } func TestLessSortsMalformedReferencesDeterministically(t *testing.T) { order := testOrder() events := []dnd.ItemEvent{ {Name: "A", Kind: dnd.ItemEventKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 999, EndUnitID: 999}}}, {Name: "A", Kind: dnd.ItemEventKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: 1, EndUnitID: 1}}}, {Name: "A", Kind: dnd.ItemEventKindDiscovered, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}}, } sort.SliceStable(events, func(left, right int) bool { return Less(order, events[left], events[right]) }) if events[0].SourceRefs[0].StartUnitID != 10 || events[1].SourceRefs[0].SourceID != "other" || events[2].SourceRefs[0].StartUnitID != 999 { t.Fatalf("canonical sort = %#v", events) } } func testOrder() shared.SourceRefOrder { return shared.NewSourceRefOrder(&source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 10}, {ID: 20}, {ID: 30}}}) } func withName(event dnd.ItemEvent, value string) dnd.ItemEvent { event.Name = value; return event } func withKind(event dnd.ItemEvent, value dnd.ItemEventKind) dnd.ItemEvent { event.Kind = value return event } func withFrom(event dnd.ItemEvent, value string) dnd.ItemEvent { event.From = value; return event } func withTo(event dnd.ItemEvent, value string) dnd.ItemEvent { event.To = value; return event } func withoutTo(event dnd.ItemEvent) dnd.ItemEvent { event.To = ""; return event } func withQuantity(event dnd.ItemEvent, value *int) dnd.ItemEvent { event.Quantity = value return event } func withRefs(event dnd.ItemEvent, value []source.SourceRef) dnd.ItemEvent { event.SourceRefs = value return event }