using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.AbCip.Import; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// Task #231 — verifies that tag/member descriptions parsed from L5K and L5X exports thread /// through / /// + land on /// on the produced address-space variables, so /// downstream OPC UA Variable nodes carry the source-project comment as their Description /// attribute. /// [Trait("Category", "Unit")] public sealed class AbCipDescriptionThreadingTests { private const string DeviceHost = "ab://10.0.0.5/1,0"; [Fact] public void L5kParser_captures_member_description_from_attribute_block() { const string body = """ DATATYPE MyUdt MEMBER Speed : DINT (Description := "Belt speed in RPM"); END_DATATYPE """; var doc = L5kParser.Parse(new StringL5kSource(body)); var member = doc.DataTypes.Single().Members.Single(); member.Name.ShouldBe("Speed"); member.Description.ShouldBe("Belt speed in RPM"); } [Fact] public void L5xParser_captures_member_description_child_node() { const string xml = """ """; var doc = L5xParser.Parse(new StringL5kSource(xml)); doc.DataTypes.Single().Members.Single().Description.ShouldBe("Belt speed in RPM"); } [Fact] public void L5kIngest_threads_tag_and_member_descriptions_into_AbCipTagDefinition() { const string body = """ DATATYPE MotorBlock MEMBER Speed : DINT (Description := "Setpoint RPM"); MEMBER Status : DINT; END_DATATYPE TAG Motor1 : MotorBlock (Description := "Conveyor motor 1") := []; END_TAG """; var doc = L5kParser.Parse(new StringL5kSource(body)); var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc); var tag = result.Tags.Single(); tag.Description.ShouldBe("Conveyor motor 1"); tag.Members.ShouldNotBeNull(); var members = tag.Members!.ToDictionary(m => m.Name); members["Speed"].Description.ShouldBe("Setpoint RPM"); members["Status"].Description.ShouldBeNull(); } [Fact] public async Task DiscoverAsync_sets_Description_on_DriverAttributeInfo_for_atomic_tag() { var builder = new RecordingBuilder(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(DeviceHost)], Tags = [ new AbCipTagDefinition( Name: "Speed", DeviceHostAddress: DeviceHost, TagPath: "Motor1.Speed", DataType: AbCipDataType.DInt, Description: "Belt speed in RPM"), new AbCipTagDefinition( Name: "NoDescription", DeviceHostAddress: DeviceHost, TagPath: "X", DataType: AbCipDataType.DInt), ], }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Single(v => v.BrowseName == "Speed").Info.Description .ShouldBe("Belt speed in RPM"); // Tags without descriptions leave Info.Description null (back-compat path). builder.Variables.Single(v => v.BrowseName == "NoDescription").Info.Description .ShouldBeNull(); } [Fact] public async Task DiscoverAsync_sets_Description_on_DriverAttributeInfo_for_UDT_members() { var builder = new RecordingBuilder(); var drv = new AbCipDriver(new AbCipDriverOptions { Devices = [new AbCipDeviceOptions(DeviceHost)], Tags = [ new AbCipTagDefinition( Name: "Motor1", DeviceHostAddress: DeviceHost, TagPath: "Motor1", DataType: AbCipDataType.Structure, Members: [ new AbCipStructureMember( Name: "Speed", DataType: AbCipDataType.DInt, Description: "Setpoint RPM"), new AbCipStructureMember( Name: "Status", DataType: AbCipDataType.DInt), ]), ], }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); await drv.DiscoverAsync(builder, CancellationToken.None); builder.Variables.Single(v => v.BrowseName == "Speed").Info.Description .ShouldBe("Setpoint RPM"); builder.Variables.Single(v => v.BrowseName == "Status").Info.Description .ShouldBeNull(); } // ---- 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) { } } } }