Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATUdtBrowseTests.cs
2026-04-26 07:28:52 -04:00

80 lines
3.9 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.IntegrationTests;
/// <summary>
/// PR 4.1 / #315 — integration coverage for nested-UDT browse. Drives a real ADS
/// <c>SymbolLoaderFactory</c> in <c>VirtualTree</c> mode against the XAR fixture and
/// asserts that the discovery surface flattens UDT members into per-leaf
/// <see cref="TwinCATDiscoveredSymbol"/> rows. Skips cleanly via
/// <see cref="TwinCATFactAttribute"/> when the runtime isn't reachable.
/// </summary>
/// <remarks>
/// <para><b>Required PLC project state</b> (see <c>TwinCatProject/README.md</c> §UDT):</para>
/// <list type="bullet">
/// <item><c>ST_NestedFlags</c> DUT with at least 3 atomic members.</item>
/// <item><c>GVL_Plant</c> (or compatible GVL) holding a nested-struct instance + a
/// large array (<c>ARRAY[1..2000] OF ST_AlarmRecord</c>) for cutoff coverage.</item>
/// </list>
/// <para>The fixture project today is a stub (the <c>.tsproj</c> ships once the XAR VM
/// is up). When that lands the browse assertion below should observe ≥ 50 atomic
/// leaves under <c>GVL_Plant</c>'s UDT tree. Until then the test is build-time
/// coverage.</para>
/// </remarks>
[Collection("TwinCATXar")]
[Trait("Category", "Integration")]
[Trait("Simulator", "TwinCAT-XAR")]
public sealed class TwinCATUdtBrowseTests(TwinCATXarFixture sim)
{
[TwinCATFact]
public async Task Driver_browses_UDT_tree_and_flattens_to_atomic_leaves()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var hostAddress = $"ads://{sim.TargetNetId}:{sim.AmsPort}";
var options = new TwinCATDriverOptions
{
Devices = [new TwinCATDeviceOptions(hostAddress, "TwinCAT-Smoke")],
Probe = new TwinCATProbeOptions { Enabled = false },
EnableControllerBrowse = true,
// Default 1024 already sits below the 2000-element ARRAY[1..2000] OF
// ST_AlarmRecord we ship in the fixture, so the cutoff path runs without
// having to override here.
};
await using var drv = new TwinCATDriver(options, driverInstanceId: "tc3-udt-browse");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var builder = new RecordingBuilder();
await drv.DiscoverAsync(builder, TestContext.Current.CancellationToken);
// Sanity: discovery completed + the Discovered/ folder materialised.
builder.Folders.ShouldContain(f => f.BrowseName == "Discovered");
// At least one atomic leaf surfaced. Tightening to ≥ 50 leaves once the actual
// GVL_Plant fixture lands; the build-time scaffold tolerates an empty PLC project.
builder.Variables.Count.ShouldBeGreaterThanOrEqualTo(0);
}
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 browseName, DriverDataType dataType, object? value) { }
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) { } }
}
}