The Driver.TwinCAT-011 fix rewrote TwinCATStatusMapper with correct numeric values from Beckhoff.TwinCAT.Ads 7.0.172 (e.g. DeviceSymbol- VersionInvalid = 1809 / 0x0711, not 1794 / 0x0702). Pre-existing StatusMapper_covers_known_ads_error_codes InlineData cases were written against the old wrong mappings and now fail; StatusMapper_recognises_ symbol_version_changed_code asserted the legacy 0x0702 constant. Update both test files to match the corrected mapper and add a comment documenting the correction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
8.1 KiB
C#
199 lines
8.1 KiB
C#
using System.Text.Json;
|
|
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>
|
|
/// Regression coverage for the five High code-review findings closed 2026-05-22:
|
|
/// Driver.TwinCAT-001 (driverConfigJson ignored), -002 (LInt/ULInt narrowed to Int32),
|
|
/// -007 (EnsureConnectedAsync not thread-safe), -008 (ADS callbacks on the router thread),
|
|
/// -013 (no IRediscoverable / symbol-version-changed handling).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class TwinCATHighFindingsRegressionTests
|
|
{
|
|
private const string DeviceA = "ads://5.23.91.23.1.1:851";
|
|
|
|
// ---- Driver.TwinCAT-001 — Reinitialize applies the new config generation ----
|
|
|
|
[Fact]
|
|
public async Task ReinitializeAsync_applies_changed_device_config()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1");
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
drv.DeviceCount.ShouldBe(1);
|
|
|
|
// A config generation that adds a second device must be picked up at runtime.
|
|
var newConfig = JsonSerializer.Serialize(new
|
|
{
|
|
probe = new { enabled = false },
|
|
devices = new[]
|
|
{
|
|
new { hostAddress = DeviceA, deviceName = "Machine1" },
|
|
new { hostAddress = "ads://10.0.0.1.1.1:852", deviceName = "Machine2" },
|
|
},
|
|
});
|
|
|
|
await drv.ReinitializeAsync(newConfig, CancellationToken.None);
|
|
|
|
drv.DeviceCount.ShouldBe(2);
|
|
drv.GetDeviceState("ads://10.0.0.1.1.1:852")!.Options.DeviceName.ShouldBe("Machine2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InitializeAsync_applies_supplied_config_over_constructor_options()
|
|
{
|
|
// Constructor seeds an empty option set; the JSON document is the authoritative config.
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1");
|
|
var config = JsonSerializer.Serialize(new
|
|
{
|
|
probe = new { enabled = false },
|
|
devices = new[] { new { hostAddress = DeviceA } },
|
|
});
|
|
|
|
await drv.InitializeAsync(config, CancellationToken.None);
|
|
|
|
drv.DeviceCount.ShouldBe(1);
|
|
drv.GetDeviceState(DeviceA).ShouldNotBeNull();
|
|
}
|
|
|
|
// ---- Driver.TwinCAT-002 — 64-bit + unsigned types map without truncation ----
|
|
|
|
[Fact]
|
|
public void DataType_mapping_preserves_width_and_signedness()
|
|
{
|
|
TwinCATDataType.LInt.ToDriverDataType().ShouldBe(DriverDataType.Int64);
|
|
TwinCATDataType.ULInt.ToDriverDataType().ShouldBe(DriverDataType.UInt64);
|
|
TwinCATDataType.UDInt.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
|
|
TwinCATDataType.DInt.ToDriverDataType().ShouldBe(DriverDataType.Int32);
|
|
TwinCATDataType.UInt.ToDriverDataType().ShouldBe(DriverDataType.UInt16);
|
|
TwinCATDataType.Int.ToDriverDataType().ShouldBe(DriverDataType.Int16);
|
|
TwinCATDataType.USInt.ToDriverDataType().ShouldBe(DriverDataType.UInt16);
|
|
TwinCATDataType.SInt.ToDriverDataType().ShouldBe(DriverDataType.Int16);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LInt_read_round_trips_value_above_int_MaxValue()
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
|
Tags = [new TwinCATTagDefinition("Big", DeviceA, "GVL.Big", TwinCATDataType.LInt)],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
long big = (long)int.MaxValue + 1_000;
|
|
factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.Big"] = big } };
|
|
|
|
var snapshot = (await drv.ReadAsync(["Big"], CancellationToken.None)).Single();
|
|
|
|
snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.Good);
|
|
snapshot.Value.ShouldBe(big); // no truncation into a 32-bit node
|
|
}
|
|
|
|
// ---- Driver.TwinCAT-007 — concurrent EnsureConnectedAsync creates exactly one client ----
|
|
|
|
[Fact]
|
|
public async Task Concurrent_reads_on_one_device_create_a_single_client()
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
|
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.X"] = 7 } };
|
|
|
|
// 32 readers race the lazy connect. Without the per-device gate this leaks clients.
|
|
var tasks = Enumerable.Range(0, 32)
|
|
.Select(_ => Task.Run(() => drv.ReadAsync(["X"], CancellationToken.None)));
|
|
await Task.WhenAll(tasks);
|
|
|
|
factory.Clients.Count.ShouldBe(1);
|
|
factory.Clients[0].ConnectCount.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Concurrent_reads_and_writes_share_one_client()
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
|
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.X"] = 1 } };
|
|
|
|
var work = new List<Task>();
|
|
for (var i = 0; i < 16; i++)
|
|
{
|
|
work.Add(Task.Run(() => drv.ReadAsync(["X"], CancellationToken.None)));
|
|
work.Add(Task.Run(() => drv.WriteAsync(
|
|
[new WriteRequest("X", 5)], CancellationToken.None)));
|
|
}
|
|
await Task.WhenAll(work);
|
|
|
|
factory.Clients.Count.ShouldBe(1);
|
|
}
|
|
|
|
// ---- Driver.TwinCAT-013 — symbol-version-changed routes to IRediscoverable ----
|
|
|
|
[Fact]
|
|
public void TwinCATDriver_implements_IRediscoverable()
|
|
{
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1");
|
|
drv.ShouldBeAssignableTo<IRediscoverable>();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Symbol_version_changed_raises_OnRediscoveryNeeded()
|
|
{
|
|
var factory = new FakeTwinCATClientFactory();
|
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(DeviceA)],
|
|
Tags = [new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)],
|
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
RediscoveryEventArgs? raised = null;
|
|
drv.OnRediscoveryNeeded += (_, args) => raised = args;
|
|
|
|
// Force a connect so the driver wires its handler onto the client.
|
|
await drv.ReadAsync(["X"], CancellationToken.None);
|
|
factory.Clients.ShouldHaveSingleItem();
|
|
|
|
// The client observed ADS 0x0702 on the wire and signalled it.
|
|
factory.Clients[0].FireSymbolVersionChanged();
|
|
|
|
raised.ShouldNotBeNull();
|
|
raised!.Reason.ShouldContain("0x0711");
|
|
}
|
|
|
|
[Fact]
|
|
public void StatusMapper_recognises_symbol_version_changed_code()
|
|
{
|
|
// DeviceSymbolVersionInvalid is 1809 (0x0711) per Beckhoff.TwinCAT.Ads 7.0.172.
|
|
// Legacy docs cited 0x0702 (= DeviceInvalidGroup, 1794) — that was a transcription error
|
|
// corrected in Driver.TwinCAT-011.
|
|
TwinCATStatusMapper.AdsSymbolVersionChanged.ShouldBe(0x0711u); // = 1809u
|
|
TwinCATStatusMapper.IsSymbolVersionChanged(0x0711u).ShouldBeTrue();
|
|
TwinCATStatusMapper.IsSymbolVersionChanged(0x0702u).ShouldBeFalse(); // DeviceInvalidGroup is NOT the trigger
|
|
TwinCATStatusMapper.IsSymbolVersionChanged(0u).ShouldBeFalse();
|
|
}
|
|
}
|