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; [Trait("Category", "Unit")] public sealed class TwinCATSymbolBrowserTests { [Fact] public async Task Discovery_without_EnableControllerBrowse_emits_only_predeclared() { var builder = new RecordingBuilder(); var factory = new FakeTwinCATClientFactory { Customise = () => { var c = new FakeTwinCATClient(); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Hidden", TwinCATDataType.DInt, false)); return c; }, }; var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Tags = [new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)], Probe = new TwinCATProbeOptions { Enabled = false }, EnableControllerBrowse = false, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Select(v => v.BrowseName).ShouldBe(["Declared"]); builder.Folders.ShouldNotContain(f => f.BrowseName == "Discovered"); } [Fact] public async Task Discovery_with_browse_enabled_adds_controller_symbols_under_Discovered_folder() { var builder = new RecordingBuilder(); var factory = new FakeTwinCATClientFactory { Customise = () => { var c = new FakeTwinCATClient(); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Counter", TwinCATDataType.DInt, ReadOnly: false)); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("GVL.Setpoint", TwinCATDataType.Real, ReadOnly: false)); return c; }, }; var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Probe = new TwinCATProbeOptions { Enabled = false }, EnableControllerBrowse = true, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Folders.ShouldContain(f => f.BrowseName == "Discovered"); builder.Variables.Select(v => v.Info.FullName).ShouldContain("MAIN.Counter"); builder.Variables.Select(v => v.Info.FullName).ShouldContain("GVL.Setpoint"); } [Fact] public async Task Browse_filters_system_symbols() { var builder = new RecordingBuilder(); var factory = new FakeTwinCATClientFactory { Customise = () => { var c = new FakeTwinCATClient(); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("TwinCAT_SystemInfoVarList._AppInfo", TwinCATDataType.DInt, false)); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("Constants.PI", TwinCATDataType.LReal, true)); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("Mc_InternalState", TwinCATDataType.DInt, true)); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("__CompilerGen", TwinCATDataType.DInt, true)); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Real", TwinCATDataType.DInt, false)); return c; }, }; var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Probe = new TwinCATProbeOptions { Enabled = false }, EnableControllerBrowse = true, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Select(v => v.Info.FullName).ShouldBe(["MAIN.Real"]); } [Fact] public async Task Browse_skips_symbols_with_null_datatype() { var builder = new RecordingBuilder(); var factory = new FakeTwinCATClientFactory { Customise = () => { var c = new FakeTwinCATClient(); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Struct", DataType: null, ReadOnly: false)); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Counter", TwinCATDataType.DInt, false)); return c; }, }; var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Probe = new TwinCATProbeOptions { Enabled = false }, EnableControllerBrowse = true, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Select(v => v.Info.FullName).ShouldBe(["MAIN.Counter"]); } [Fact] public async Task ReadOnly_symbol_surfaces_ViewOnly() { var builder = new RecordingBuilder(); var factory = new FakeTwinCATClientFactory { Customise = () => { var c = new FakeTwinCATClient(); c.BrowseResults.Add(new TwinCATDiscoveredSymbol("MAIN.Status", TwinCATDataType.DInt, ReadOnly: true)); return c; }, }; var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Probe = new TwinCATProbeOptions { Enabled = false }, EnableControllerBrowse = true, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Single().Info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly); } [Fact] public async Task Browse_failure_is_non_fatal_predeclared_still_emits() { var builder = new RecordingBuilder(); var factory = new FakeTwinCATClientFactory { Customise = () => new FakeTwinCATClient { ThrowOnBrowse = true }, }; var drv = new TwinCATDriver(new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")], Tags = [new TwinCATTagDefinition("Declared", "ads://5.23.91.23.1.1:851", "MAIN.Declared", TwinCATDataType.DInt)], Probe = new TwinCATProbeOptions { Enabled = false }, EnableControllerBrowse = true, }, "drv-1", factory); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Select(v => v.BrowseName).ShouldContain("Declared"); } [Theory] [InlineData("TwinCAT_SystemInfoVarList._AppInfo", true)] [InlineData("TwinCAT_RuntimeInfo.Something", true)] [InlineData("Constants.PI", true)] [InlineData("Mc_AxisState", true)] [InlineData("__hidden", true)] [InlineData("Global_Version", true)] [InlineData("MAIN.UserVar", false)] [InlineData("GVL.Counter", false)] [InlineData("MyFbInstance.State", false)] [InlineData("", true)] [InlineData(" ", true)] public void SystemSymbolFilter_matches_expected_patterns(string path, bool expected) { TwinCATSystemSymbolFilter.IsSystemSymbol(path).ShouldBe(expected); } // ---- 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) { } } } }