Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipSystemTagDiscoveryTests.cs
2026-04-26 02:55:56 -04:00

96 lines
3.9 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
/// <summary>
/// PR abcip-4.3 — end-to-end coverage that the synthetic <c>_System</c> folder + its five
/// diagnostic variables ride a real ab_server lifecycle. Skipped when the binary isn't
/// on PATH (<see cref="AbServerFactAttribute"/>).
/// </summary>
[Trait("Category", "Integration")]
[Trait("Requires", "AbServer")]
public sealed class AbCipSystemTagDiscoveryTests
{
[AbServerFact]
public async Task System_folder_browses_and_each_variable_reads_non_empty()
{
var profile = KnownProfiles.ControlLogix;
var fixture = new AbServerFixture(profile);
await fixture.InitializeAsync();
try
{
var deviceUri = $"ab://127.0.0.1:{fixture.Port}/1,0";
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)],
Tags = [new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)],
Timeout = TimeSpan.FromSeconds(5),
}, "drv-system-tags");
await drv.InitializeAsync("{}", CancellationToken.None);
// Discovery — five system variables exposed under _System/ for the device.
var builder = new RecordingBuilder();
await drv.DiscoverAsync(builder, CancellationToken.None);
builder.Folders.ShouldContain(f => f.BrowseName == "_System");
var systemVars = builder.Variables
.Where(v => v.Info.FullName.StartsWith("_System/"))
.Select(v => v.BrowseName)
.ToList();
systemVars.ShouldContain("_ConnectionStatus");
systemVars.ShouldContain("_ScanRate");
systemVars.ShouldContain("_TagCount");
systemVars.ShouldContain("_DeviceError");
systemVars.ShouldContain("_LastScanTimeMs");
// Read — each system variable returns Good with a non-null value, with no
// libplctag round-trip required.
var refs = systemVars
.Select(name => $"_System/{deviceUri}/{name}")
.ToList();
var snaps = await drv.ReadAsync(refs, CancellationToken.None);
for (var i = 0; i < snaps.Count; i++)
{
snaps[i].StatusCode.ShouldBe(AbCipStatusMapper.Good,
$"system variable {refs[i]} should read Good");
snaps[i].Value.ShouldNotBeNull(
$"system variable {refs[i]} should not be null");
}
await drv.ShutdownAsync(CancellationToken.None);
}
finally
{
await fixture.DisposeAsync();
}
}
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) { }
}
}
}