test(ab): run the ab_server integration suites on both transports
This commit is contained in:
@@ -2,7 +2,46 @@
|
||||
|
||||
## ab_server suites (T17)
|
||||
|
||||
_(pending)_
|
||||
Every ab_server integration test now runs once per transport (`TransportMatrix` in each integration
|
||||
project). Run 2026-10-07 against `10.100.0.35`, one compose profile at a time
|
||||
(AbCip `/opt/otopcua-abcip`, `AB_SERVER_COMPOSE_PROFILE=<p>`; AbLegacy `~/otopcua-ablegacy`,
|
||||
`AB_LEGACY_COMPOSE_PROFILE=<p>`); every stack was brought down afterwards.
|
||||
|
||||
- **Image:** `otopcua-ab-server:libplctag-release` (`sha256:19e814747d85…`, built 2026-08-01; ab_server from
|
||||
libplctag `v2.7.0` + patches 0001/0002). Host compose files md5-identical to the repo's.
|
||||
- **Commit:** fixes `82d011c7` + `2171fc8c`, test matrix in the T17 `test(ab)` commit that follows them.
|
||||
|
||||
Counts are pass / fail / skip for the transport-parameterised rows only. Each AbCip run also passed the
|
||||
6 transport-independent `AbServerProfileTests`. The AbCip skips are the 3 Emulate-tier tests
|
||||
(`AB_SERVER_PROFILE=emulate` only). The AbLegacy skips are `Slc500_write_then_read…`, which skips itself
|
||||
on both transports when the slc500 port (`:44819`) is down.
|
||||
|
||||
| Profile | Libplctag | Native | Native ⊇ Libplctag |
|
||||
|---|---|---|---|
|
||||
| controllogix | 1 / 0 / 3 | 1 / 0 / 3 | yes |
|
||||
| compactlogix | 1 / 0 / 3 | 1 / 0 / 3 | yes |
|
||||
| micro800 | 0 / 1 / 3 | 1 / 0 / 3 | yes (libplctag fails, see below) |
|
||||
| guardlogix | 1 / 0 / 3 | 1 / 0 / 3 | yes |
|
||||
| slc500 | 2 / 0 / 0 | 2 / 0 / 0 | yes |
|
||||
| micrologix | 1 / 0 / 1 | 1 / 0 / 1 | yes |
|
||||
| plc5 | 1 / 0 / 1 | 1 / 0 / 1 | yes |
|
||||
|
||||
**Native-only failures found and fixed:**
|
||||
|
||||
- `82d011c7` (controllogix): RegisterSession carried a non-zero sender context, and ab_server rejects that
|
||||
with encapsulation status 1. Every native session failed.
|
||||
- `2171fc8c` (slc500): ab_server answers Execute PCCC with a fixed requestor id
|
||||
(`07 3D F3 45 43 50 21`), so the requestor-echo check rejected every native PCCC reply. Native now sends
|
||||
libplctag's identity.
|
||||
|
||||
**Skips added:** none. The PLC-5 integration test only reads N7, so the PLC-5 Unsupported paths
|
||||
(non-N reads, all writes) and status-bit sub-element writes are never exercised by the suite.
|
||||
|
||||
**micro800:** the smoke test used to send `/1,0` for every family, and both transports failed against
|
||||
`--plc=Micro800`. It now uses an empty path for Micro800. With the empty path, native passes. libplctag
|
||||
still fails with BadCommunicationError, because the driver's libplctag runtime passes `Path = ""` and
|
||||
libplctag answers that with `ErrorBadData`. A `null` path works (checked with the capture client). That
|
||||
defect is pre-existing and libplctag-side; it is recorded, not fixed, here.
|
||||
|
||||
## Differential vs ab_server (T18)
|
||||
|
||||
|
||||
+11
-9
@@ -6,7 +6,7 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end smoke tests that exercise the real libplctag stack against a running
|
||||
/// End-to-end smoke tests that exercise the real transport stack (libplctag and native, via <see cref="TransportMatrix"/>) against a running
|
||||
/// <c>ab_server</c>. Skipped when the binary isn't on PATH (<see cref="AbServerFactAttribute"/>).
|
||||
/// Parametrized over <see cref="KnownProfiles.All"/> so one test file covers every family
|
||||
/// (ControlLogix / CompactLogix / Micro800 / GuardLogix).
|
||||
@@ -15,15 +15,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
[Trait("Requires", "AbServer")]
|
||||
public sealed class AbCipReadSmokeTests
|
||||
{
|
||||
/// <summary>Gets the test profiles for the theory tests.</summary>
|
||||
public static IEnumerable<object[]> Profiles =>
|
||||
KnownProfiles.All.Select(p => new object[] { p });
|
||||
|
||||
/// <summary>Verifies that the driver can read a seeded DInt value from an AB server.</summary>
|
||||
/// <param name="profile">The AB server profile to test against.</param>
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbServerTheory]
|
||||
[MemberData(nameof(Profiles))]
|
||||
public async Task Driver_reads_seeded_DInt_from_ab_server(AbServerProfile profile)
|
||||
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
|
||||
public async Task Driver_reads_seeded_DInt_from_ab_server(AbServerProfile profile, AbCipTransport transport)
|
||||
{
|
||||
var fixture = new AbServerFixture(profile);
|
||||
await fixture.InitializeAsync();
|
||||
@@ -33,13 +30,18 @@ public sealed class AbCipReadSmokeTests
|
||||
// (10.100.0.35) and AB_SERVER_ENDPOINT overrides reach this test too. The
|
||||
// sibling Emulate tests already use the fixture's resolved endpoint; this
|
||||
// smoke test was missed.
|
||||
var deviceUri = $"ab://{fixture.Host}:{fixture.Port}/1,0";
|
||||
// Micro800 has no backplane: its cip-path is empty (the compose service runs
|
||||
// ab_server --plc=Micro800 with no --path, which refuses a routed request). Every
|
||||
// other family reaches slot 0 through `1,0`.
|
||||
var cipPath = profile.Family == AbCipPlcFamily.Micro800 ? "" : "1,0";
|
||||
var deviceUri = $"ab://{fixture.Host}:{fixture.Port}/{cipPath}";
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(deviceUri, profile.Family)],
|
||||
RawTags = AbCipRawTags.From(new AbCipTagDefinition("Counter", deviceUri, "TestDINT", AbCipDataType.DInt)),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
}, $"drv-smoke-{profile.Family}");
|
||||
Transport = transport,
|
||||
}, $"drv-smoke-{profile.Family}-{transport}");
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
||||
|
||||
+6
-3
@@ -40,8 +40,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;
|
||||
public sealed class AbCipEmulateAlmdTests
|
||||
{
|
||||
/// <summary>Verifies that real ALMD raise fires OnAlarmEvent through the driver projection.</summary>
|
||||
[AbServerFact]
|
||||
public async Task Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection()
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbServerTheory]
|
||||
[MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
|
||||
public async Task Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection(AbCipTransport transport)
|
||||
{
|
||||
AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);
|
||||
|
||||
@@ -73,9 +75,10 @@ public sealed class AbCipEmulateAlmdTests
|
||||
TagPath: "SimulateAlarm",
|
||||
DataType: AbCipDataType.Bool,
|
||||
Writable: true)),
|
||||
Transport = transport,
|
||||
};
|
||||
|
||||
await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-almd");
|
||||
await using var drv = new AbCipDriver(options, driverInstanceId: $"emulate-almd-{transport}");
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
var raised = new TaskCompletionSource<AlarmEventArgs>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
+6
-3
@@ -43,8 +43,10 @@ public sealed class AbCipEmulateNestedUdtTests
|
||||
/// leaves addressable as dot-joined paths, proving the nested template-id threading
|
||||
/// runs the production <c>@udt/{id}</c> CIP fetch against Emulate's firmware.
|
||||
/// </summary>
|
||||
[AbServerFact]
|
||||
public async Task Nested_struct_member_discovered_and_expanded_via_template_id_fetch()
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbServerTheory]
|
||||
[MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
|
||||
public async Task Nested_struct_member_discovered_and_expanded_via_template_id_fetch(AbCipTransport transport)
|
||||
{
|
||||
AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);
|
||||
|
||||
@@ -58,9 +60,10 @@ public sealed class AbCipEmulateNestedUdtTests
|
||||
Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")],
|
||||
EnableControllerBrowse = true,
|
||||
Timeout = TimeSpan.FromSeconds(10),
|
||||
Transport = transport,
|
||||
};
|
||||
|
||||
await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-nested-udt-smoke");
|
||||
await using var drv = new AbCipDriver(options, driverInstanceId: $"emulate-nested-udt-smoke-{transport}");
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
// Drive controller discovery — the driver walks @tags, encounters MotorNested1
|
||||
|
||||
+6
-3
@@ -38,8 +38,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;
|
||||
public sealed class AbCipEmulateUdtReadTests
|
||||
{
|
||||
/// <summary>Verifies that reading a whole UDT decodes each member at its template object offset.</summary>
|
||||
[AbServerFact]
|
||||
public async Task WholeUdt_read_decodes_each_member_at_its_Template_Object_offset()
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbServerTheory]
|
||||
[MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
|
||||
public async Task WholeUdt_read_decodes_each_member_at_its_Template_Object_offset(AbCipTransport transport)
|
||||
{
|
||||
AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);
|
||||
|
||||
@@ -63,9 +65,10 @@ public sealed class AbCipEmulateUdtReadTests
|
||||
new AbCipStructureMember("Status", AbCipDataType.DInt),
|
||||
])),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Transport = transport,
|
||||
};
|
||||
|
||||
await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-udt-smoke");
|
||||
await using var drv = new AbCipDriver(options, driverInstanceId: $"emulate-udt-smoke-{transport}");
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
// Whole-UDT read optimization from task #194: one libplctag read on the
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Crosses every <c>ab_server</c> compose profile with both AbCip transports so each integration
|
||||
/// test runs once on libplctag and once on the native managed stack. The native run must be green
|
||||
/// wherever the libplctag run is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every AbCip compose profile binds host <c>:44818</c>, so only one can run at a time. When
|
||||
/// <c>AB_SERVER_COMPOSE_PROFILE</c> is set, <see cref="Profiles"/> keeps only that profile, so the
|
||||
/// rows match the running container instead of driving, say, Micro800 semantics at a ControlLogix.
|
||||
/// Unset, every profile is crossed (the original behaviour).
|
||||
/// </remarks>
|
||||
public static class TransportMatrix
|
||||
{
|
||||
/// <summary>Both transports, for tests that are not parameterised by profile.</summary>
|
||||
public static IReadOnlyList<AbCipTransport> AllTransports { get; } =
|
||||
[AbCipTransport.Libplctag, AbCipTransport.Native];
|
||||
|
||||
/// <summary>Gets <c>(profile, transport)</c> rows for every selected profile × both transports.</summary>
|
||||
public static IEnumerable<object[]> Profiles
|
||||
{
|
||||
get
|
||||
{
|
||||
var only = Environment.GetEnvironmentVariable("AB_SERVER_COMPOSE_PROFILE");
|
||||
return from p in KnownProfiles.All
|
||||
where string.IsNullOrEmpty(only) ||
|
||||
string.Equals(p.ComposeProfile, only, StringComparison.OrdinalIgnoreCase)
|
||||
from t in AllTransports
|
||||
select new object[] { p, t };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets one row per transport.</summary>
|
||||
public static IEnumerable<object[]> Transports =>
|
||||
AllTransports.Select(t => new object[] { t });
|
||||
}
|
||||
+15
-24
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
/// <summary>
|
||||
/// End-to-end smoke tests against the <c>ab_server</c> PCCC Docker container.
|
||||
/// Promotes the AB Legacy driver from unit-only coverage (<c>FakeAbLegacyTag</c>)
|
||||
/// to wire-level: real libplctag PCCC stack over real TCP against the ab_server
|
||||
/// to wire-level: real PCCC stack (libplctag and native, via <see cref="TransportMatrix"/>) over real TCP against the ab_server
|
||||
/// simulator. Parametrised over all three families (SLC 500 / MicroLogix / PLC-5)
|
||||
/// via <c>[AbLegacyTheory]</c> + <c>[MemberData]</c>.
|
||||
/// </summary>
|
||||
@@ -17,29 +17,16 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
[Trait("Simulator", "ab_server-PCCC")]
|
||||
public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
// Only one ab_server container binds :44818 at a time and `--plc=SLC500` only
|
||||
// answers SLC-mode PCCC, etc. When `AB_LEGACY_COMPOSE_PROFILE` is set, the theory
|
||||
// filters to that profile alone so the suite matches the running container. Unset
|
||||
// (the default for real-hardware runs) parameterises across every family the driver
|
||||
// supports.
|
||||
/// <summary>Gets the available server profiles based on compose configuration.</summary>
|
||||
public static IEnumerable<object[]> Profiles
|
||||
{
|
||||
get
|
||||
{
|
||||
var only = Environment.GetEnvironmentVariable("AB_LEGACY_COMPOSE_PROFILE");
|
||||
var profiles = KnownProfiles.All.Where(p =>
|
||||
string.IsNullOrEmpty(only) ||
|
||||
string.Equals(p.ComposeProfile, only, StringComparison.OrdinalIgnoreCase));
|
||||
return profiles.Select(p => new object[] { p });
|
||||
}
|
||||
}
|
||||
// Rows come from TransportMatrix.Profiles, which keeps the AB_LEGACY_COMPOSE_PROFILE filter this
|
||||
// class applied before the transport split: when set, only that profile is crossed with the
|
||||
// transports, so the suite matches the running container.
|
||||
|
||||
/// <summary>Verifies that the driver reads seeded N file from the AB server via PCCC.</summary>
|
||||
/// <param name="profile">The AB Legacy server profile describing the fixture endpoint.</param>
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbLegacyTheory]
|
||||
[MemberData(nameof(Profiles))]
|
||||
public async Task Driver_reads_seeded_N_file_from_ab_server_PCCC(AbLegacyServerProfile profile)
|
||||
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
|
||||
public async Task Driver_reads_seeded_N_file_from_ab_server_PCCC(AbLegacyServerProfile profile, AbLegacyTransport transport)
|
||||
{
|
||||
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
|
||||
|
||||
@@ -59,7 +46,8 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
DataType: AbLegacyDataType.Int)),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, driverInstanceId: $"ablegacy-smoke-{profile.Family}");
|
||||
Transport = transport,
|
||||
}, driverInstanceId: $"ablegacy-smoke-{profile.Family}-{transport}");
|
||||
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
@@ -72,8 +60,10 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SLC500 write-then-read round trip succeeds on N7 scratch register.</summary>
|
||||
[AbLegacyFact]
|
||||
public async Task Slc500_write_then_read_round_trip_on_N7_scratch_register()
|
||||
/// <param name="transport">The transport stack the driver runs on.</param>
|
||||
[AbLegacyTheory]
|
||||
[MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
|
||||
public async Task Slc500_write_then_read_round_trip_on_N7_scratch_register(AbLegacyTransport transport)
|
||||
{
|
||||
// Addresses the SLC500 container by its OWN port, so it no longer matters which other
|
||||
// families happen to be running. The old AB_LEGACY_COMPOSE_PROFILE guard existed because every
|
||||
@@ -106,7 +96,8 @@ public sealed class AbLegacyReadSmokeTests(AbLegacyServerFixture sim)
|
||||
Writable: true)),
|
||||
Timeout = TimeSpan.FromSeconds(5),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, driverInstanceId: "ablegacy-smoke-rw");
|
||||
Transport = transport,
|
||||
}, driverInstanceId: $"ablegacy-smoke-rw-{transport}");
|
||||
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Crosses the PCCC compose profiles with both AbLegacy transports so each integration test runs
|
||||
/// once on libplctag and once on the native managed stack.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Preserves the <c>AB_LEGACY_COMPOSE_PROFILE</c> filter the smoke theory always applied: when set,
|
||||
/// <see cref="KnownProfiles.All"/> is narrowed to that profile <b>before</b> it is crossed with the
|
||||
/// transports, so the rows match the running container.
|
||||
/// </remarks>
|
||||
public static class TransportMatrix
|
||||
{
|
||||
/// <summary>Both transports, for tests that are not parameterised by profile.</summary>
|
||||
public static IReadOnlyList<AbLegacyTransport> AllTransports { get; } =
|
||||
[AbLegacyTransport.Libplctag, AbLegacyTransport.Native];
|
||||
|
||||
/// <summary>Gets <c>(profile, transport)</c> rows for every selected profile × both transports.</summary>
|
||||
public static IEnumerable<object[]> Profiles
|
||||
{
|
||||
get
|
||||
{
|
||||
var only = Environment.GetEnvironmentVariable("AB_LEGACY_COMPOSE_PROFILE");
|
||||
return from p in KnownProfiles.All
|
||||
where string.IsNullOrEmpty(only) ||
|
||||
string.Equals(p.ComposeProfile, only, StringComparison.OrdinalIgnoreCase)
|
||||
from t in AllTransports
|
||||
select new object[] { p, t };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets one row per transport.</summary>
|
||||
public static IEnumerable<object[]> Transports =>
|
||||
AllTransports.Select(t => new object[] { t });
|
||||
}
|
||||
Reference in New Issue
Block a user