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");
}
}