Auto: twincat-4.1 — nested UDT browse via online type walker
Closes #315
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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) { } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.0">
|
||||
<DUT Name="ST_AlarmRecord" Id="{00000000-0000-0000-0000-000000000403}">
|
||||
<Declaration><![CDATA[// PR 4.1 / #315 — element type for the GVL_Plant.aAlarmRecords ARRAY[1..2000] cutoff
|
||||
// fixture. Two atomic members per element so an over-cap browse short-circuits to a
|
||||
// single IsArrayRoot leaf rather than 2000 × 2 = 4000 individual leaves.
|
||||
TYPE ST_AlarmRecord :
|
||||
STRUCT
|
||||
nCode : DINT;
|
||||
bActive : BOOL;
|
||||
END_STRUCT
|
||||
END_TYPE
|
||||
]]></Declaration>
|
||||
</DUT>
|
||||
</TcPlcObject>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.0">
|
||||
<DUT Name="ST_NestedFlags" Id="{00000000-0000-0000-0000-000000000401}">
|
||||
<Declaration><![CDATA[// PR 4.1 / #315 — exercises TwinCATTypeWalker.Walk against a mixed-atomic struct.
|
||||
// Members chosen to span integer / boolean / real so the per-leaf flatten emits one
|
||||
// row per type the OPC UA layer renders. Bit-packed BOOL members reuse the existing
|
||||
// PR 1.5 bit-extract path on read.
|
||||
TYPE ST_NestedFlags :
|
||||
STRUCT
|
||||
bRunning : BOOL;
|
||||
bFault : BOOL;
|
||||
bWarning : BOOL;
|
||||
nState : INT;
|
||||
rTemperature : REAL;
|
||||
sTagName : STRING(40);
|
||||
END_STRUCT
|
||||
END_TYPE
|
||||
]]></Declaration>
|
||||
</DUT>
|
||||
</TcPlcObject>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.0">
|
||||
<DUT Name="ST_RecursiveCap" Id="{00000000-0000-0000-0000-000000000402}">
|
||||
<Declaration><![CDATA[// PR 4.1 / #315 — exercises the depth-cap / cycle-guard path in TwinCATTypeWalker.
|
||||
// A POINTER TO ST_RecursiveCap surfaces in the IDataType graph as IsPointer=true
|
||||
// (DataTypeCategory.Pointer); the walker should skip the pointer member rather than
|
||||
// recurse, leaving nValue as the only atomic leaf. If the cycle guard ever regresses
|
||||
// the walker would either stack-overflow or emit a flood of self-referential paths,
|
||||
// both of which the integration test asserts against.
|
||||
TYPE ST_RecursiveCap :
|
||||
STRUCT
|
||||
nValue : DINT;
|
||||
pNext : POINTER TO ST_RecursiveCap;
|
||||
END_STRUCT
|
||||
END_TYPE
|
||||
]]></Declaration>
|
||||
</DUT>
|
||||
</TcPlcObject>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.0">
|
||||
<GVL Name="GVL_Plant" Id="{00000000-0000-0000-0000-000000000404}">
|
||||
<Declaration><![CDATA[// PR 4.1 / #315 — UDT decomposition + array-cutoff fixture for TwinCATUdtBrowseTests.
|
||||
// stFlags : nested flags struct exercises the per-member flatten path (one OPC UA
|
||||
// variable per atomic field). aAlarmRecords : 2000-element array of struct-typed
|
||||
// elements exercises the MaxArrayExpansion cutoff; default cap is 1024, so the
|
||||
// browse short-circuits to a single IsArrayRoot leaf instead of 4000 individual
|
||||
// rows. Keep the GVL small overall so the symbol table stays under the AMS request
|
||||
// budget.
|
||||
VAR_GLOBAL
|
||||
stFlags : ST_NestedFlags;
|
||||
aAlarmRecords : ARRAY[1..2000] OF ST_AlarmRecord;
|
||||
END_VAR
|
||||
]]></Declaration>
|
||||
</GVL>
|
||||
</TcPlcObject>
|
||||
@@ -198,6 +198,45 @@ Options to eliminate the manual step:
|
||||
the rotation permanently, worth it if the integration host is
|
||||
long-lived.
|
||||
|
||||
## Complex hierarchy
|
||||
|
||||
PR 4.1 / #315 (nested-UDT browse via online type walker) exercises
|
||||
`TwinCATTypeWalker.Walk` against a real PLC symbol graph. The fixture
|
||||
state required:
|
||||
|
||||
### DUTs
|
||||
|
||||
- `PLC/DUTs/ST_NestedFlags.TcDUT` — mixed-atomic struct (BOOL / INT / REAL /
|
||||
STRING) used for the per-member flatten coverage.
|
||||
- `PLC/DUTs/ST_AlarmRecord.TcDUT` — small two-field struct used as the
|
||||
element type of the cutoff array.
|
||||
- `PLC/DUTs/ST_RecursiveCap.TcDUT` — struct with a `POINTER TO`
|
||||
self-reference; verifies the walker's pointer-skip + cycle-guard
|
||||
paths terminate without exploding the symbol stream.
|
||||
|
||||
### GVL: `GVL_Plant`
|
||||
|
||||
```st
|
||||
VAR_GLOBAL
|
||||
stFlags : ST_NestedFlags;
|
||||
aAlarmRecords : ARRAY[1..2000] OF ST_AlarmRecord;
|
||||
END_VAR
|
||||
```
|
||||
|
||||
`stFlags` produces N atomic leaves where N = the number of `ST_NestedFlags`
|
||||
fields. `aAlarmRecords` has 2000 elements which exceeds the default
|
||||
`TwinCATDriverOptions.MaxArrayExpansion` (1024) — discovery surfaces it as
|
||||
a single `IsArrayRoot` leaf rather than 4000 per-element rows. Lower the
|
||||
cap on the driver instance to force per-element expansion (or raise it if
|
||||
the operator wants the per-element view).
|
||||
|
||||
The XAE-form artefacts ship at `PLC/DUTs/*.TcDUT` + `PLC/GVLs/GVL_Plant.TcGVL`;
|
||||
import them into the PLC project alongside `GVL_Fixture` + `GVL_Perf`.
|
||||
|
||||
The integration test that exercises this fixture lives at
|
||||
`tests/.../TwinCATUdtBrowseTests.cs` and skips via `[TwinCATFact]` when
|
||||
the XAR runtime isn't reachable.
|
||||
|
||||
## Online-change test scenario
|
||||
|
||||
PR 2.3 (proactive Symbol-Version invalidation listener) ships an
|
||||
|
||||
Reference in New Issue
Block a user