Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATArraySupportTests.cs
T

216 lines
8.8 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
/// <summary>
/// Phase 4c — 1-D array support for the TwinCAT (ADS) driver. Covers the two discovery
/// hard-wire flips (pre-declared + discovered symbols now report IsArray/ArrayDim from the
/// ADS symbol's array dimension), the array READ path (the equipment tag's arrayLength drives
/// a native array read boxed into a typed CLR array), and the equipment-tag parser threading
/// <c>arrayLength</c> from the TagConfig JSON.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATArraySupportTests
{
private const string Host = "ads://5.23.91.23.1.1:851";
// ---- (1) discovery: pre-declared array tag flips IsArray/ArrayDim ----
/// <summary>A pre-declared tag carrying an ArrayLength surfaces as a 1-D array node.</summary>
[Fact]
public async Task Predeclared_array_tag_reports_IsArray_and_ArrayDim()
{
var builder = new RecordingBuilder();
var drv = new TwinCATDriver(new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(Host)],
Tags =
[
new TwinCATTagDefinition("Speeds", Host, "MAIN.Speeds", TwinCATDataType.DInt,
Writable: true, WriteIdempotent: false, ArrayLength: 8),
],
Probe = new TwinCATProbeOptions { Enabled = false },
EnableControllerBrowse = false,
}, "drv-1", new FakeTwinCATClientFactory());
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.DiscoverAsync(builder, CancellationToken.None);
var v = builder.Variables.Single(x => x.BrowseName == "Speeds").Info;
v.IsArray.ShouldBeTrue();
v.ArrayDim.ShouldBe(8u);
}
/// <summary>A scalar pre-declared tag still reports IsArray=false / ArrayDim=null.</summary>
[Fact]
public async Task Predeclared_scalar_tag_stays_scalar()
{
var builder = new RecordingBuilder();
var drv = new TwinCATDriver(new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(Host)],
Tags = [new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)],
Probe = new TwinCATProbeOptions { Enabled = false },
}, "drv-1", new FakeTwinCATClientFactory());
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.DiscoverAsync(builder, CancellationToken.None);
var v = builder.Variables.Single(x => x.BrowseName == "Speed").Info;
v.IsArray.ShouldBeFalse();
v.ArrayDim.ShouldBeNull();
}
// ---- (1) discovery: discovered (browsed) array symbol flips IsArray/ArrayDim ----
/// <summary>A discovered (browsed) 1-D array symbol surfaces as a 1-D array node.</summary>
[Fact]
public async Task Discovered_array_symbol_reports_IsArray_and_ArrayDim()
{
var builder = new RecordingBuilder();
var factory = new FakeTwinCATClientFactory
{
Customise = () =>
{
var c = new FakeTwinCATClient();
c.BrowseResults.Add(new TwinCATDiscoveredSymbol(
"MAIN.Buffer", TwinCATDataType.Int, ReadOnly: false, ArrayLength: 16));
c.BrowseResults.Add(new TwinCATDiscoveredSymbol(
"GVL.Scalar", TwinCATDataType.Real, ReadOnly: false));
return c;
},
};
var drv = new TwinCATDriver(new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(Host)],
Probe = new TwinCATProbeOptions { Enabled = false },
EnableControllerBrowse = true,
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.DiscoverAsync(builder, CancellationToken.None);
var arr = builder.Variables.Single(x => x.Info.FullName == "MAIN.Buffer").Info;
arr.IsArray.ShouldBeTrue();
arr.ArrayDim.ShouldBe(16u);
var scalar = builder.Variables.Single(x => x.Info.FullName == "GVL.Scalar").Info;
scalar.IsArray.ShouldBeFalse();
scalar.ArrayDim.ShouldBeNull();
}
// ---- (2) read path: array read returns a typed CLR array ----
/// <summary>An equipment array tag reads a typed CLR array (boxed as object) through the driver.</summary>
[Fact]
public async Task Driver_reads_an_array_equipment_tag_as_typed_clr_array()
{
var json =
$$"""{"deviceHostAddress":"{{Host}}","symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":4}""";
var expected = new[] { 10, 20, 30, 40 };
var factory = new FakeTwinCATClientFactory
{
Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speeds"] = expected } },
};
var drv = new TwinCATDriver(new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(Host)],
Tags = [],
Probe = new TwinCATProbeOptions { Enabled = false },
}, "twincat-arr", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync([json], CancellationToken.None);
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
r[0].Value.ShouldBeOfType<int[]>().ShouldBe(expected);
// The driver must request the array read with the authored length.
factory.Clients[0].LastReadArrayCount.ShouldBe(4);
}
/// <summary>A scalar equipment tag reads with a null array count (scalar path unchanged).</summary>
[Fact]
public async Task Driver_reads_a_scalar_equipment_tag_with_null_array_count()
{
var json = $$"""{"deviceHostAddress":"{{Host}}","symbolPath":"MAIN.Speed","dataType":"DInt"}""";
var factory = new FakeTwinCATClientFactory
{
Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 7 } },
};
var drv = new TwinCATDriver(new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(Host)],
Tags = [],
Probe = new TwinCATProbeOptions { Enabled = false },
}, "twincat-scalar", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync([json], CancellationToken.None);
r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
r[0].Value.ShouldBe(7);
factory.Clients[0].LastReadArrayCount.ShouldBeNull();
}
// ---- (3) resolver: equipment-tag parser threads arrayLength ----
/// <summary>The equipment-tag parser threads <c>arrayLength</c> into the transient definition.</summary>
[Fact]
public void Parser_threads_arrayLength_into_the_definition()
{
var json =
"""{"deviceHostAddress":"ads://5.23.91.23.1.1:851","symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":12}""";
TwinCATEquipmentTagParser.TryParse(json, out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBe(12);
}
/// <summary>A blob without isArray (or with isArray=false) leaves ArrayLength null.</summary>
[Fact]
public void Parser_leaves_ArrayLength_null_for_a_scalar_blob()
{
TwinCATEquipmentTagParser.TryParse(
"""{"symbolPath":"MAIN.X","dataType":"DInt"}""", out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
/// <summary>arrayLength is ignored when isArray is absent/false (no orphan length).</summary>
[Fact]
public void Parser_ignores_arrayLength_when_isArray_is_false()
{
TwinCATEquipmentTagParser.TryParse(
"""{"symbolPath":"MAIN.X","dataType":"DInt","isArray":false,"arrayLength":9}""",
out var def).ShouldBeTrue();
def!.ArrayLength.ShouldBeNull();
}
// ---- helpers ----
private sealed class RecordingBuilder : IAddressSpaceBuilder
{
public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();
public IAddressSpaceBuilder Folder(string browseName, string displayName)
{ Folders.Add((browseName, displayName)); return this; }
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
{ Variables.Add((browseName, info)); return new Handle(info.FullName); }
public void AddProperty(string _, DriverDataType __, object? ___) { }
private sealed class Handle(string fullRef) : IVariableHandle
{
public string FullReference => fullRef;
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
}
private sealed class NullSink : IAlarmConditionSink
{
public void OnTransition(AlarmEventArgs args) { }
}
}
}