using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; [Trait("Category", "Unit")] public sealed class FocasPmcCoalescerTests { [Fact] public void Empty_input_yields_no_groups() { var groups = FocasPmcCoalescer.Plan(Array.Empty()); groups.ShouldBeEmpty(); } [Fact] public void Contiguous_same_letter_same_path_coalesces_into_one_group() { // 100 contiguous R-letter byte reads at byte 0..99 var requests = new List(); for (var i = 0; i < 100; i++) requests.Add(new PmcAddressRequest("R", PathId: 1, ByteNumber: i, ByteWidth: 1, OriginalIndex: i)); var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(1); var g = groups[0]; g.Letter.ShouldBe("R"); g.PathId.ShouldBe(1); g.StartByte.ShouldBe(0); g.ByteCount.ShouldBe(100); g.Members.Count.ShouldBe(100); g.Members[42].Offset.ShouldBe(42); g.Members[42].OriginalIndex.ShouldBe(42); } [Fact] public void Range_cap_splits_oversized_runs_into_multiple_groups() { // 300 contiguous bytes — must split (cap = 256) var requests = new List(); for (var i = 0; i < 300; i++) requests.Add(new PmcAddressRequest("R", 1, i, 1, i)); var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(2); groups[0].ByteCount.ShouldBe(FocasPmcCoalescer.MaxRangeBytes); groups[0].StartByte.ShouldBe(0); groups[1].StartByte.ShouldBe(FocasPmcCoalescer.MaxRangeBytes); groups[1].ByteCount.ShouldBe(300 - FocasPmcCoalescer.MaxRangeBytes); } [Fact] public void Gap_within_bridge_threshold_is_bridged() { // Two runs: R0..R9 then R20..R29 — gap = 10 bytes, within bridge cap of 16. var requests = new List { new("R", 1, 0, 1, 0), new("R", 1, 9, 1, 1), new("R", 1, 20, 1, 2), new("R", 1, 29, 1, 3), }; var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(1); groups[0].StartByte.ShouldBe(0); groups[0].ByteCount.ShouldBe(30); } [Fact] public void Gap_larger_than_bridge_threshold_splits() { // Two runs: R0 then R100 — gap of 99 bytes >> 16, must split. var requests = new List { new("R", 1, 0, 1, 0), new("R", 1, 100, 1, 1), }; var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(2); groups[0].StartByte.ShouldBe(0); groups[1].StartByte.ShouldBe(100); } [Fact] public void Different_letters_split_into_separate_groups() { var requests = new List { new("R", 1, 0, 1, 0), new("R", 1, 1, 1, 1), new("D", 1, 0, 1, 2), new("D", 1, 1, 1, 3), }; var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(2); groups.ShouldContain(g => g.Letter == "R" && g.ByteCount == 2); groups.ShouldContain(g => g.Letter == "D" && g.ByteCount == 2); } [Fact] public void Different_paths_split_into_separate_groups() { var requests = new List { new("R", 1, 0, 1, 0), new("R", 1, 1, 1, 1), new("R", 2, 0, 1, 2), new("R", 2, 1, 1, 3), }; var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(2); groups.ShouldContain(g => g.Letter == "R" && g.PathId == 1); groups.ShouldContain(g => g.Letter == "R" && g.PathId == 2); } [Fact] public void Wider_data_types_extend_range_correctly() { // R0 is Int32 (4 bytes covers R0..R3), R4 is Byte → contiguous, one group of 5 bytes. var requests = new List { new("R", 1, 0, ByteWidth: 4, 0), new("R", 1, 4, ByteWidth: 1, 1), }; var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(1); groups[0].ByteCount.ShouldBe(5); groups[0].Members[0].ByteWidth.ShouldBe(4); groups[0].Members[0].Offset.ShouldBe(0); groups[0].Members[1].ByteWidth.ShouldBe(1); groups[0].Members[1].Offset.ShouldBe(4); } [Fact] public void Overlapping_requests_do_not_grow_range_beyond_their_union() { // R10 Int32 (R10..R13) + R12 Byte — overlap; range should still be 4 bytes from 10. var requests = new List { new("R", 1, 10, 4, 0), new("R", 1, 12, 1, 1), }; var groups = FocasPmcCoalescer.Plan(requests); groups.Count.ShouldBe(1); groups[0].StartByte.ShouldBe(10); groups[0].ByteCount.ShouldBe(4); groups[0].Members[1].Offset.ShouldBe(2); // member at byte 12, offset within range = 2 } [Fact] public void ByteWidth_helper_matches_data_type_sizes() { FocasPmcCoalescer.ByteWidth(FocasDataType.Bit).ShouldBe(1); FocasPmcCoalescer.ByteWidth(FocasDataType.Byte).ShouldBe(1); FocasPmcCoalescer.ByteWidth(FocasDataType.Int16).ShouldBe(2); FocasPmcCoalescer.ByteWidth(FocasDataType.Int32).ShouldBe(4); FocasPmcCoalescer.ByteWidth(FocasDataType.Float32).ShouldBe(4); FocasPmcCoalescer.ByteWidth(FocasDataType.Float64).ShouldBe(8); } }