Auto: abcip-3.2 — symbolic vs logical addressing toggle

Closes #236
This commit is contained in:
Joseph Doherty
2026-04-25 22:58:33 -04:00
parent 73ff10b595
commit 0c6a0d6e50
13 changed files with 1033 additions and 17 deletions

View File

@@ -0,0 +1,76 @@
using System.Diagnostics;
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-3.2 — wall-clock comparison of Symbolic vs Logical reads on a running
/// <c>ab_server</c> (or a real ControlLogix). Skipped when <c>ab_server</c> isn't
/// reachable, same gating rule as <see cref="AbCipReadSmokeTests"/>.
/// </summary>
/// <remarks>
/// <para>This is a <em>scaffold</em>: it builds + runs against the existing test fixture,
/// but the libplctag .NET 1.5.x wrapper does not yet expose a public knob for instance-ID
/// addressing (see <c>docs/drivers/AbCip-Performance.md</c> §"Addressing mode"). On a live
/// fixture the two paths therefore measure the same wire behaviour today; the assertion
/// just sanity-checks that both modes complete + produce well-formed snapshots, with timing
/// emitted to the test output for inspection. When the wrapper exposes the attribute
/// publicly (or libplctag native gains hot-update of cip_addr) the assertion can be
/// tightened to require Logical &lt; Symbolic on N-tag scans.</para>
///
/// <para>Marked <c>[Trait("Category", "Bench")]</c> so a future <c>--filter</c> rule can
/// opt out of bench tests in CI runs that only want the smoke set.</para>
/// </remarks>
[Trait("Category", "Bench")]
[Trait("Requires", "AbServer")]
public sealed class AbCipAddressingModeBenchTests
{
[AbServerFact]
public async Task Symbolic_and_Logical_modes_both_read_seeded_DInt_and_emit_timing()
{
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 symElapsed = await ReadOnceAsync(deviceUri, profile.Family, AddressingMode.Symbolic);
var logElapsed = await ReadOnceAsync(deviceUri, profile.Family, AddressingMode.Logical);
// Wall-clock timing is captured for human inspection; the assertion just confirms
// both completed. The actual symbolic-vs-logical comparison is qualitative until
// the libplctag wrapper exposes logical-segment addressing publicly — see class doc.
Console.WriteLine($"Symbolic read elapsed: {symElapsed.TotalMilliseconds:F2} ms");
Console.WriteLine($"Logical read elapsed: {logElapsed.TotalMilliseconds:F2} ms");
symElapsed.ShouldBeGreaterThan(TimeSpan.Zero);
logElapsed.ShouldBeGreaterThan(TimeSpan.Zero);
}
finally
{
await fixture.DisposeAsync();
}
}
private static async Task<TimeSpan> ReadOnceAsync(string deviceUri, AbCipPlcFamily family, AddressingMode mode)
{
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(deviceUri, family, AddressingMode: mode)],
Tags = [new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)],
Timeout = TimeSpan.FromSeconds(5),
}, $"drv-bench-{mode}");
await drv.InitializeAsync("{}", CancellationToken.None);
var sw = Stopwatch.StartNew();
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
sw.Stop();
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
await drv.ShutdownAsync(CancellationToken.None);
return sw.Elapsed;
}
}