using Mbproxy.Proxy.Cache; using Shouldly; using Xunit; namespace Mbproxy.Tests.Proxy.Cache; /// /// Six range-overlap unit tests required by the Phase-11 doc. Half-open interval math: /// write [w, w+writeQty) overlaps entry [s, s+qty) iff w < s+qty AND s < w+writeQty. /// [Trait("Category", "Unit")] public sealed class CacheInvalidatorTests { private static CacheKey K(byte unit, ushort start, ushort qty, byte fc = 0x03) => new(unit, fc, start, qty); [Fact] public void FullOverlap_WriteCoversEntryRange_Invalidates() { // Entry [100..110), write [95..115) — write covers entry fully. var entry = K(unit: 1, start: 100, qty: 10); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 95, writeQty: 20).ToList(); hits.ShouldContain(entry, "a write that fully contains the entry's range must invalidate it"); } [Fact] public void PartialOverlap_WriteStartsBeforeEntry_Invalidates() { // Entry [100..110), write [95..105) — overlaps low side. var entry = K(unit: 1, start: 100, qty: 10); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 95, writeQty: 10).ToList(); hits.ShouldContain(entry, "low-side partial overlap must invalidate"); } [Fact] public void PartialOverlap_WriteEndsAfterEntry_Invalidates() { // Entry [100..110), write [105..115) — overlaps high side. var entry = K(unit: 1, start: 100, qty: 10); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 105, writeQty: 10).ToList(); hits.ShouldContain(entry, "high-side partial overlap must invalidate"); } [Fact] public void Adjacent_NotOverlapping_DoesNotInvalidate() { // Half-open intervals: write [10..15) is adjacent to but NOT overlapping entry // [15..20) — register 15 is in the entry but NOT in the write. Should not match. var entry = K(unit: 1, start: 15, qty: 5); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 10, writeQty: 5).ToList(); hits.ShouldBeEmpty("adjacent-but-not-overlapping ranges must not invalidate (half-open semantics)"); } [Fact] public void NoOverlap_DoesNotInvalidate() { // Entry [100..110), write [200..210) — fully disjoint. var entry = K(unit: 1, start: 100, qty: 10); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 200, writeQty: 10).ToList(); hits.ShouldBeEmpty("disjoint ranges must not invalidate"); } [Fact] public void DifferentUnitId_DoesNotInvalidate() { // Same address range, different unit ID — must not match. var entry = K(unit: 1, start: 100, qty: 10); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 2, writeStart: 95, writeQty: 20).ToList(); hits.ShouldBeEmpty("writes on a different unit ID must not invalidate this entry"); } // ── Auxiliary correctness checks ───────────────────────────────────────────── [Fact] public void FcOtherThan03Or04_NeverInvalidated() { // Defensive: only FC03/FC04 entries are ever stored, but if a non-read key // somehow appeared the invalidator must skip it. var nonRead = new CacheKey(UnitId: 1, Fc: 0x06, StartAddress: 100, Qty: 10); var hits = CacheInvalidator.FindOverlapping([nonRead], unitId: 1, writeStart: 95, writeQty: 20).ToList(); hits.ShouldBeEmpty("only FC03/FC04 entries should ever be invalidated"); } [Fact] public void ZeroWriteQty_NeverInvalidates() { var entry = K(unit: 1, start: 100, qty: 10); var hits = CacheInvalidator.FindOverlapping([entry], unitId: 1, writeStart: 100, writeQty: 0).ToList(); hits.ShouldBeEmpty("a degenerate write covering zero registers must not invalidate anything"); } }