233 lines
9.3 KiB
C#
233 lines
9.3 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasPmcCoalescedReadTests
|
|
{
|
|
private const string Host = "focas://10.0.0.5:8193";
|
|
|
|
private static (FocasDriver drv, FakeFocasClientFactory factory, FakeFocasClient client) NewDriver(
|
|
params FocasTagDefinition[] tags)
|
|
{
|
|
var client = new FakeFocasClient();
|
|
var factory = new FakeFocasClientFactory { Customise = () => client };
|
|
var drv = new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions(Host)],
|
|
Tags = tags,
|
|
Probe = new FocasProbeOptions { Enabled = false },
|
|
}, "drv-coalesce", factory);
|
|
return (drv, factory, client);
|
|
}
|
|
|
|
private static FocasTagDefinition Tag(string name, string addr, FocasDataType type) =>
|
|
new(name, Host, addr, type);
|
|
|
|
[Fact]
|
|
public async Task Hundred_contiguous_PMC_bytes_collapse_to_one_wire_call()
|
|
{
|
|
// 100 contiguous R-letter byte-shaped tags → coalescer cap is 256 → one range read.
|
|
var tags = new FocasTagDefinition[100];
|
|
var refs = new string[100];
|
|
for (var i = 0; i < 100; i++)
|
|
{
|
|
tags[i] = Tag($"r{i}", $"R{i}", FocasDataType.Byte);
|
|
refs[i] = $"r{i}";
|
|
}
|
|
var (drv, _, client) = NewDriver(tags);
|
|
client.PmcByteRanges[("R", 1)] = new byte[200];
|
|
for (var i = 0; i < 100; i++) client.PmcByteRanges[("R", 1)][i] = (byte)(i + 1);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(refs, CancellationToken.None);
|
|
|
|
snapshots.Count.ShouldBe(100);
|
|
foreach (var s in snapshots) s.StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
// sbyte cast: 1 → 1, 100 stays positive
|
|
snapshots[0].Value.ShouldBe((sbyte)1);
|
|
snapshots[99].Value.ShouldBe((sbyte)100);
|
|
client.RangeReadLog.Count.ShouldBe(1);
|
|
client.RangeReadLog[0].Letter.ShouldBe("R");
|
|
client.RangeReadLog[0].StartByte.ShouldBe(0);
|
|
client.RangeReadLog[0].ByteCount.ShouldBe(100);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Gap_larger_than_bridge_threshold_splits_into_two_wire_calls()
|
|
{
|
|
// R0, R1, then R100, R101 — gap of 98 > bridge cap of 16 → 2 ranges.
|
|
var tags = new[]
|
|
{
|
|
Tag("r0", "R0", FocasDataType.Byte),
|
|
Tag("r1", "R1", FocasDataType.Byte),
|
|
Tag("r100", "R100", FocasDataType.Byte),
|
|
Tag("r101", "R101", FocasDataType.Byte),
|
|
};
|
|
var (drv, _, client) = NewDriver(tags);
|
|
client.PmcByteRanges[("R", 1)] = new byte[200];
|
|
client.PmcByteRanges[("R", 1)][0] = 10;
|
|
client.PmcByteRanges[("R", 1)][1] = 11;
|
|
client.PmcByteRanges[("R", 1)][100] = 20;
|
|
client.PmcByteRanges[("R", 1)][101] = 21;
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["r0", "r1", "r100", "r101"], CancellationToken.None);
|
|
|
|
snapshots.Count.ShouldBe(4);
|
|
foreach (var s in snapshots) s.StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
snapshots[0].Value.ShouldBe((sbyte)10);
|
|
snapshots[3].Value.ShouldBe((sbyte)21);
|
|
client.RangeReadLog.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Different_letters_yield_separate_wire_calls()
|
|
{
|
|
var tags = new[]
|
|
{
|
|
Tag("r0", "R0", FocasDataType.Byte),
|
|
Tag("r1", "R1", FocasDataType.Byte),
|
|
Tag("d0", "D0", FocasDataType.Byte),
|
|
Tag("d1", "D1", FocasDataType.Byte),
|
|
};
|
|
var (drv, _, client) = NewDriver(tags);
|
|
client.PmcByteRanges[("R", 1)] = new byte[8] { 1, 2, 3, 4, 5, 6, 7, 8 };
|
|
client.PmcByteRanges[("D", 1)] = new byte[8] { 9, 10, 11, 12, 13, 14, 15, 16 };
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["r0", "r1", "d0", "d1"], CancellationToken.None);
|
|
|
|
foreach (var s in snapshots) s.StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
snapshots[0].Value.ShouldBe((sbyte)1);
|
|
snapshots[2].Value.ShouldBe((sbyte)9);
|
|
client.RangeReadLog.Count.ShouldBe(2);
|
|
client.RangeReadLog.ShouldContain(c => c.Letter == "R");
|
|
client.RangeReadLog.ShouldContain(c => c.Letter == "D");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Different_paths_yield_separate_wire_calls()
|
|
{
|
|
var tags = new[]
|
|
{
|
|
Tag("p1a", "R0", FocasDataType.Byte),
|
|
Tag("p1b", "R1", FocasDataType.Byte),
|
|
Tag("p2a", "R0@2", FocasDataType.Byte),
|
|
Tag("p2b", "R1@2", FocasDataType.Byte),
|
|
};
|
|
var (drv, _, client) = NewDriver(tags);
|
|
client.PathCount = 2;
|
|
client.PmcByteRanges[("R", 1)] = new byte[] { 1, 2 };
|
|
client.PmcByteRanges[("R", 2)] = new byte[] { 7, 8 };
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["p1a", "p1b", "p2a", "p2b"], CancellationToken.None);
|
|
|
|
foreach (var s in snapshots) s.StatusCode.ShouldBe(FocasStatusMapper.Good);
|
|
snapshots[0].Value.ShouldBe((sbyte)1);
|
|
snapshots[2].Value.ShouldBe((sbyte)7);
|
|
client.RangeReadLog.Count.ShouldBe(2);
|
|
client.RangeReadLog.ShouldContain(c => c.PathId == 1);
|
|
client.RangeReadLog.ShouldContain(c => c.PathId == 2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Single_PMC_tag_does_not_invoke_range_path()
|
|
{
|
|
// Single-tag PMC reads aren't worth coalescing — the driver falls through to the
|
|
// existing per-tag dispatch so connect/set-path overhead isn't paid twice.
|
|
var (drv, _, client) = NewDriver(Tag("only", "R5", FocasDataType.Byte));
|
|
client.Values["R5"] = (sbyte)42;
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["only"], CancellationToken.None);
|
|
|
|
snapshots.Single().Value.ShouldBe((sbyte)42);
|
|
client.RangeReadLog.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Bit_addressed_tags_share_parent_byte_range()
|
|
{
|
|
// R10.0 + R10.3 + R10.7 + R11.0 — all addressing R10 / R11, one coalesced range.
|
|
var tags = new[]
|
|
{
|
|
Tag("b0", "R10.0", FocasDataType.Bit),
|
|
Tag("b3", "R10.3", FocasDataType.Bit),
|
|
Tag("b7", "R10.7", FocasDataType.Bit),
|
|
Tag("c0", "R11.0", FocasDataType.Bit),
|
|
};
|
|
var (drv, _, client) = NewDriver(tags);
|
|
client.PmcByteRanges[("R", 1)] = new byte[20];
|
|
// R10 = 0b1000_1001 → bit0=1, bit3=1, bit7=1
|
|
client.PmcByteRanges[("R", 1)][10] = 0b1000_1001;
|
|
// R11 = 0b0000_0000 → bit0=0
|
|
client.PmcByteRanges[("R", 1)][11] = 0;
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["b0", "b3", "b7", "c0"], CancellationToken.None);
|
|
|
|
snapshots[0].Value.ShouldBe(true);
|
|
snapshots[1].Value.ShouldBe(true);
|
|
snapshots[2].Value.ShouldBe(true);
|
|
snapshots[3].Value.ShouldBe(false);
|
|
client.RangeReadLog.Count.ShouldBe(1);
|
|
client.RangeReadLog[0].StartByte.ShouldBe(10);
|
|
client.RangeReadLog[0].ByteCount.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Wider_types_decode_correctly_from_coalesced_buffer()
|
|
{
|
|
// R0 Int16 + R2 Int32 + R6 Byte — contiguous (R0..R6 = 7 bytes), one range.
|
|
var tags = new[]
|
|
{
|
|
Tag("w16", "R0", FocasDataType.Int16),
|
|
Tag("w32", "R2", FocasDataType.Int32),
|
|
Tag("b", "R6", FocasDataType.Byte),
|
|
};
|
|
var (drv, _, client) = NewDriver(tags);
|
|
// Little-endian: R0..R1 = 0x1234 → bytes 0x34, 0x12; R2..R5 = 0x12345678 → 0x78,0x56,0x34,0x12; R6=0x05
|
|
client.PmcByteRanges[("R", 1)] = new byte[] { 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0x05 };
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["w16", "w32", "b"], CancellationToken.None);
|
|
|
|
snapshots[0].Value.ShouldBe((short)0x1234);
|
|
snapshots[1].Value.ShouldBe(0x12345678);
|
|
snapshots[2].Value.ShouldBe((sbyte)0x05);
|
|
client.RangeReadLog.Count.ShouldBe(1);
|
|
client.RangeReadLog[0].ByteCount.ShouldBe(7);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Non_PMC_tags_in_same_batch_use_per_tag_path()
|
|
{
|
|
// Mix PMC + Parameter + Macro — only the PMC half coalesces.
|
|
var tags = new[]
|
|
{
|
|
Tag("r0", "R0", FocasDataType.Byte),
|
|
Tag("r1", "R1", FocasDataType.Byte),
|
|
Tag("p", "PARAM:1820", FocasDataType.Int32),
|
|
Tag("m", "MACRO:500", FocasDataType.Float64),
|
|
};
|
|
var (drv, _, client) = NewDriver(tags);
|
|
client.PmcByteRanges[("R", 1)] = new byte[] { 11, 22 };
|
|
client.Values["PARAM:1820"] = 7777;
|
|
client.Values["MACRO:500"] = 2.71828;
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var snapshots = await drv.ReadAsync(["r0", "r1", "p", "m"], CancellationToken.None);
|
|
|
|
snapshots[0].Value.ShouldBe((sbyte)11);
|
|
snapshots[1].Value.ShouldBe((sbyte)22);
|
|
snapshots[2].Value.ShouldBe(7777);
|
|
snapshots[3].Value.ShouldBe(2.71828);
|
|
client.RangeReadLog.Count.ShouldBe(1);
|
|
}
|
|
}
|