using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; /// /// A controller's @tags listing names each program (Program:UN01_Press, a system /// symbol) but none of its tags — those are listed only by Program:<name>.@tags. /// Seen live on a production press: the PackML state (Program:UN01_Press.Out_eState) and /// every alarm bit the Ignition project reads are program-scoped, and the browse showed none. /// [Trait("Category", "Unit")] public sealed class AbCipProgramScopeEnumerationTests { // Captured entries (CompactLogix 5069-L320ERS2 rev 34.11): WW_InCycle + the Program:UN01_Press entry // from @tags, and Out_eState from Program:UN01_Press.@tags. private const string ControllerListing = "AF010000C10001000000000000000000000000000A0057575F496E4379636C65" + "7900000068100000000000000000000000000000120050726F6772616D3A554E30315F5072657373"; private const string Un01Listing = "1A000000C40004000000000000000000000000000A004F75745F655374617465"; private static readonly AbCipTagCreateParams Params = new( Gateway: "10.0.0.5", Port: 44818, CipPath: "1,0", LibplctagPlcAttribute: "controllogix", TagName: "@tags", Timeout: TimeSpan.FromSeconds(2)); /// The enumerator reads each program's own listing and yields its tags program-scoped. [Fact] public async Task Program_scoped_tags_are_enumerated_from_each_program_listing() { var reads = new List(); using var enumerator = new SymbolListingEnumerator((_, name, _) => { reads.Add(name); return Task.FromResult(name switch { "@tags" => Convert.FromHexString(ControllerListing), "Program:UN01_Press.@tags" => Convert.FromHexString(Un01Listing), _ => throw new InvalidOperationException(name), }); }); var tags = new List(); await foreach (var t in enumerator.EnumerateAsync(Params, TestContext.Current.CancellationToken)) tags.Add(t); reads.ShouldBe(["@tags", "Program:UN01_Press.@tags"]); var outState = tags.Single(t => t.Name == "Out_eState"); outState.ProgramScope.ShouldBe("UN01_Press"); outState.DataType.ShouldBe(AbCipDataType.DInt); tags.ShouldContain(t => t.Name == "WW_InCycle" && t.ProgramScope == null); } /// One unreadable program listing does not cost the controller tags or other programs. [Fact] public async Task A_failing_program_listing_is_skipped() { using var enumerator = new SymbolListingEnumerator((_, name, _) => name == "@tags" ? Task.FromResult(Convert.FromHexString(ControllerListing)) : Task.FromException(new InvalidOperationException("ErrorNotFound"))); var tags = new List(); await foreach (var t in enumerator.EnumerateAsync(Params, TestContext.Current.CancellationToken)) tags.Add(t); tags.ShouldContain(t => t.Name == "WW_InCycle"); } }