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; /// /// PR 4.1 / #315 — integration coverage for nested-UDT browse. Drives a real ADS /// SymbolLoaderFactory in VirtualTree mode against the XAR fixture and /// asserts that the discovery surface flattens UDT members into per-leaf /// rows. Skips cleanly via /// when the runtime isn't reachable. /// /// /// Required PLC project state (see TwinCatProject/README.md §UDT): /// /// ST_NestedFlags DUT with at least 3 atomic members. /// GVL_Plant (or compatible GVL) holding a nested-struct instance + a /// large array (ARRAY[1..2000] OF ST_AlarmRecord) for cutoff coverage. /// /// The fixture project today is a stub (the .tsproj ships once the XAR VM /// is up). When that lands the browse assertion below should observe ≥ 50 atomic /// leaves under GVL_Plant's UDT tree. Until then the test is build-time /// coverage. /// [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) { } } } }