@@ -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 < 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-3.2 — coverage for the per-device <c>AddressingMode</c> toggle.
|
||||
/// Asserts (a) <see cref="AddressingMode.Auto"/> resolves to
|
||||
/// <see cref="AddressingMode.Symbolic"/> at the device level, (b) explicit
|
||||
/// <see cref="AddressingMode.Logical"/> threads through every
|
||||
/// <see cref="AbCipTagCreateParams"/> the driver builds, (c) Logical against an unsupported
|
||||
/// family (Micro800) emits a warning + falls back to Symbolic, (d) the Driver-config DTO
|
||||
/// round-trips the mode, and (e) family compatibility is captured by
|
||||
/// <see cref="AbCipPlcFamilyProfile.SupportsLogicalAddressing"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipAddressingModeTests
|
||||
{
|
||||
// ---- Auto resolves to Symbolic ----
|
||||
|
||||
[Fact]
|
||||
public async Task Default_AddressingMode_resolves_to_Symbolic_on_DeviceState()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0", AbCipPlcFamily.ControlLogix)],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Auto_AddressingMode_resolves_to_Symbolic_on_DeviceState()
|
||||
{
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Auto),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
// ---- Logical threads through to AbCipTagCreateParams ----
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_AddressingMode_threads_through_into_create_params()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory,
|
||||
enumeratorFactory: new EmptyEnumeratorFactoryStub());
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Symbolic_AddressingMode_explicitly_set_threads_through()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Symbolic),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
// ---- Logical against unsupported family falls back with warning ----
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_on_Micro800_falls_back_to_Symbolic_with_warning()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.6/",
|
||||
PlcFamily: AbCipPlcFamily.Micro800,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
drv.GetDeviceState("ab://10.0.0.6/")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
warnings.ShouldHaveSingleItem();
|
||||
warnings[0].ShouldContain("Micro800");
|
||||
warnings[0].ShouldContain("Logical");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_on_Micro800_carries_Symbolic_into_create_params()
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.6/",
|
||||
PlcFamily: AbCipPlcFamily.Micro800,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.6/", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
OnWarning = _ => { },
|
||||
}, "drv-1", tagFactory: factory);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
factory.Tags["Speed"].CreationParams.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_on_ControlLogix_does_not_warn()
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
OnWarning = warnings.Add,
|
||||
}, "drv-1");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
warnings.ShouldBeEmpty();
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||
}
|
||||
|
||||
// ---- Family-profile compatibility flags ----
|
||||
|
||||
[Fact]
|
||||
public void Family_profiles_advertise_logical_support_correctly()
|
||||
{
|
||||
AbCipPlcFamilyProfile.ControlLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||
AbCipPlcFamilyProfile.CompactLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||
AbCipPlcFamilyProfile.GuardLogix.SupportsLogicalAddressing.ShouldBeTrue();
|
||||
AbCipPlcFamilyProfile.Micro800.SupportsLogicalAddressing.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---- DTO round-trip ----
|
||||
|
||||
[Fact]
|
||||
public async Task DTO_round_trips_AddressingMode_Logical_through_config_json()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix",
|
||||
"AddressingMode": "Logical"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Logical);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DTO_round_trips_AddressingMode_Symbolic_through_config_json()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix",
|
||||
"AddressingMode": "Symbolic"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DTO_omitted_AddressingMode_falls_back_to_Auto_then_Symbolic()
|
||||
{
|
||||
// No AddressingMode in JSON → DTO field is null → factory parses fallback Auto →
|
||||
// device-level resolution lands on Symbolic.
|
||||
var json = """
|
||||
{
|
||||
"Devices": [
|
||||
{
|
||||
"HostAddress": "ab://10.0.0.5/1,0",
|
||||
"PlcFamily": "ControlLogix"
|
||||
}
|
||||
],
|
||||
"Probe": { "Enabled": false }
|
||||
}
|
||||
""";
|
||||
var drv = AbCipDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
await drv.InitializeAsync(json, CancellationToken.None);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.AddressingMode.ShouldBe(AddressingMode.Symbolic);
|
||||
}
|
||||
|
||||
// ---- Logical-mode triggers a one-time symbol walk ----
|
||||
|
||||
[Fact]
|
||||
public async Task Logical_mode_first_read_triggers_symbol_walk_once()
|
||||
{
|
||||
var enumStub = new RecordingEnumeratorFactory(
|
||||
new AbCipDiscoveredTag("Speed", null, AbCipDataType.DInt, false),
|
||||
new AbCipDiscoveredTag("Counter", null, AbCipDataType.DInt, false));
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Logical),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
new AbCipTagDefinition("Counter", "ab://10.0.0.5/1,0", "Counter", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory, enumeratorFactory: enumStub);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
// First read fires the walk
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
// Second read must NOT walk again
|
||||
await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
|
||||
enumStub.CreateCount.ShouldBe(1);
|
||||
var device = drv.GetDeviceState("ab://10.0.0.5/1,0")!;
|
||||
device.LogicalWalkComplete.ShouldBeTrue();
|
||||
device.LogicalInstanceMap.ShouldContainKey("Speed");
|
||||
device.LogicalInstanceMap.ShouldContainKey("Counter");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Symbolic_mode_does_not_trigger_symbol_walk()
|
||||
{
|
||||
var enumStub = new RecordingEnumeratorFactory();
|
||||
var factory = new FakeAbCipTagFactory();
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbCipDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.5/1,0",
|
||||
PlcFamily: AbCipPlcFamily.ControlLogix,
|
||||
AddressingMode: AddressingMode.Symbolic),
|
||||
],
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
Tags =
|
||||
[
|
||||
new AbCipTagDefinition("Speed", "ab://10.0.0.5/1,0", "Speed", AbCipDataType.DInt),
|
||||
],
|
||||
}, "drv-1", tagFactory: factory, enumeratorFactory: enumStub);
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
await drv.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
enumStub.CreateCount.ShouldBe(0);
|
||||
drv.GetDeviceState("ab://10.0.0.5/1,0")!.LogicalWalkComplete.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ---- Stubs ----
|
||||
|
||||
private sealed class EmptyEnumeratorFactoryStub : IAbCipTagEnumeratorFactory
|
||||
{
|
||||
public IAbCipTagEnumerator Create() => new EmptyStub();
|
||||
|
||||
private sealed class EmptyStub : IAbCipTagEnumerator
|
||||
{
|
||||
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||
AbCipTagCreateParams deviceParams,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield break;
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingEnumeratorFactory : IAbCipTagEnumeratorFactory
|
||||
{
|
||||
private readonly AbCipDiscoveredTag[] _seed;
|
||||
public int CreateCount;
|
||||
|
||||
public RecordingEnumeratorFactory(params AbCipDiscoveredTag[] seed) => _seed = seed;
|
||||
|
||||
public IAbCipTagEnumerator Create()
|
||||
{
|
||||
CreateCount++;
|
||||
return new SeededStub(_seed);
|
||||
}
|
||||
|
||||
private sealed class SeededStub(AbCipDiscoveredTag[] seed) : IAbCipTagEnumerator
|
||||
{
|
||||
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
|
||||
AbCipTagCreateParams deviceParams,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var tag in seed)
|
||||
yield return tag;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user