using Mbproxy.Proxy.Multiplexing; using Shouldly; using Xunit; namespace Mbproxy.Tests.Proxy.Multiplexing; /// /// Equality + hash-distribution coverage for the Phase-10 /// record struct. The key is the load-bearing primitive of read coalescing: bad equality /// would either cause unrelated requests to share a backend round-trip (correctness loss) /// or prevent legitimate same-key requests from coalescing (performance loss). /// [Trait("Category", "Unit")] public sealed class CoalescingKeyTests { [Fact] public void Equality_OnIdenticalKeys_ReturnsTrue() { var a = new CoalescingKey(UnitId: 1, Fc: 0x03, StartAddress: 100, Qty:4); var b = new CoalescingKey(UnitId: 1, Fc: 0x03, StartAddress: 100, Qty:4); a.ShouldBe(b, "identical keys must compare equal"); a.GetHashCode().ShouldBe(b.GetHashCode(), "identical keys must hash to the same bucket"); } [Fact] public void Equality_OnDifferentFc_ReturnsFalse() { // FC03 (Read Holding Registers) and FC04 (Read Input Registers) name DIFFERENT // Modbus tables even for the same address. Coalescing them would deliver wrong // data. var fc03 = new CoalescingKey(UnitId: 1, Fc: 0x03, StartAddress: 100, Qty:4); var fc04 = new CoalescingKey(UnitId: 1, Fc: 0x04, StartAddress: 100, Qty:4); fc03.ShouldNotBe(fc04, "FC03 and FC04 keys must never coalesce"); } [Fact] public void Equality_OnDifferentUnitId_ReturnsFalse() { // Different unit IDs typically address different PLC personalities behind a shared // socket (multi-drop / gateway-backed setups). Never coalesce across them. var u1 = new CoalescingKey(UnitId: 1, Fc: 0x03, StartAddress: 100, Qty:4); var u2 = new CoalescingKey(UnitId: 2, Fc: 0x03, StartAddress: 100, Qty:4); u1.ShouldNotBe(u2, "different unit IDs must never coalesce"); } [Fact] public void Equality_OnDifferentQty_ReturnsFalse() { var read1 = new CoalescingKey(UnitId: 1, Fc: 0x03, StartAddress: 100, Qty:1); var read4 = new CoalescingKey(UnitId: 1, Fc: 0x03, StartAddress: 100, Qty:4); read1.ShouldNotBe(read4, "different qty must not coalesce — response register count differs"); } [Fact] public void HashCode_DistributionSanity() { // Build 10,000 keys at random V-memory-ish addresses and bucket the low byte of // GetHashCode. A reasonable hash should spread fairly evenly across 256 buckets. // Threshold: no single bucket holds > 5% of total (well above ideal 1/256 = 0.4%). const int Count = 10_000; var rng = new Random(17); var buckets = new int[256]; for (int i = 0; i < Count; i++) { ushort start = (ushort)rng.Next(0, 4096); // 12-bit V-memory space ushort qty = (ushort)rng.Next(1, 128); byte unit = (byte)rng.Next(0, 4); byte fc = rng.Next(2) == 0 ? (byte)0x03 : (byte)0x04; int bucket = new CoalescingKey(unit, fc, start, qty).GetHashCode() & 0xFF; buckets[bucket]++; } int max = 0; for (int i = 0; i < buckets.Length; i++) if (buckets[i] > max) max = buckets[i]; int ceiling = Count * 5 / 100; max.ShouldBeLessThanOrEqualTo(ceiling, $"hash distribution is uneven — the busiest bucket holds {max} > {ceiling} keys"); } }