From 012c6a4e7a7ad4861f4f482cf0c0cfa3f5582c7f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 21 Apr 2026 04:17:46 -0400 Subject: [PATCH] =?UTF-8?q?Task=20#224=20close=20=E2=80=94=20AB=20Legacy?= =?UTF-8?q?=20PCCC=20fixture:=20add=20AB=5FLEGACY=5FTRUST=5FWIRE=20opt-in?= =?UTF-8?q?=20for=20wire-level=20runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ab_server Docker simulator accepts TCP at :44818 when started with --plc=SLC500 but its PCCC dispatcher is a confirmed upstream gap (verified 2026-04-21 with --debug=5: zero request logs when libplctag issues a read, every read surfaces BadCommunicationError 0x80050000). Previous behavior — when Docker was up, the three smoke tests ran and all failed on every integration-host run. Noise, not signal. New behavior — AbLegacyServerFixture gates on a new env var AB_LEGACY_TRUST_WIRE: Endpoint reachable? | TRUST_WIRE set? | Result --------------------+-----------------+------------------------------ No | — | Skip ("not reachable") Yes | No | Skip ("ab_server PCCC gap") Yes | 1 / true | Run The fixture's new skip reason explicitly names the upstream gap + the resolution paths (upstream bug / RSEmulate golden-box / real hardware via task #222 lab rig). Operators with a real SLC 5/05 / MicroLogix 1100/1400 / PLC-5 or an Emulate box set AB_LEGACY_ENDPOINT + TRUST_WIRE and the smoke tests round-trip cleanly. Updated docs: - tests/.../Docker/README.md — new env-var table + three-case gate matrix - Known limitations section refreshed to "confirmed upstream gap" Verified locally: - Docker down: 2 skipped. - Docker up + TRUST_WIRE unset: 2 skipped (upstream-gap message). - Docker up + TRUST_WIRE=1: 4 run, 4 fail BadCommunicationError (ab_server gap as expected). - Unit suite: 96 passed / 0 failed (regression-clean). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../AbLegacyServerFixture.cs | 72 +++++++++++++++---- .../Docker/README.md | 71 +++++++++++------- 2 files changed, 102 insertions(+), 41 deletions(-) diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs index b997b2f..9cdac7c 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/AbLegacyServerFixture.cs @@ -28,6 +28,16 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime { private const string EndpointEnvVar = "AB_LEGACY_ENDPOINT"; + /// + /// Opt-in flag that promises the endpoint can actually round-trip PCCC reads/writes + /// (real SLC 5/05 / MicroLogix 1100/1400 / PLC-5 hardware, or a RSEmulate 500 + /// golden-box per docs/v2/lmx-followups.md). Without this, the fixture assumes + /// the endpoint is libplctag's ab_server --plc=SLC500 Docker container — whose + /// PCCC dispatcher is a known upstream gap — and skips cleanly rather than failing + /// every test with BadCommunicationError. + /// + private const string TrustWireEnvVar = "AB_LEGACY_TRUST_WIRE"; + /// Standard EtherNet/IP port. PCCC-over-CIP rides on the same port as /// native CIP; the differentiator is the --plc flag ab_server was started /// with, not a different TCP listener. @@ -46,22 +56,49 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime if (parts.Length == 2 && int.TryParse(parts[1], out var p)) Port = p; } - if (!TcpProbe(Host, Port)) - { - SkipReason = - $"AB Legacy PCCC simulator at {Host}:{Port} not reachable within 2 s. " + - $"Start the Docker container (docker compose -f Docker/docker-compose.yml " + - $"--profile slc500 up -d) or override {EndpointEnvVar}."; - } + SkipReason = ResolveSkipReason(Host, Port); } public ValueTask InitializeAsync() => ValueTask.CompletedTask; public ValueTask DisposeAsync() => ValueTask.CompletedTask; + /// + /// Used by + + /// during test-class construction — gates whether the test runs at all. Duplicates the + /// fixture logic because attribute ctors fire before the collection fixture instance + /// exists. + /// public static bool IsServerAvailable() { var (host, port) = ResolveEndpoint(); - return TcpProbe(host, port); + return ResolveSkipReason(host, port) is null; + } + + private static string? ResolveSkipReason(string host, int port) + { + if (!TcpProbe(host, port)) + { + return $"AB Legacy PCCC endpoint at {host}:{port} not reachable within 2 s. " + + $"Start the Docker container (docker compose -f Docker/docker-compose.yml " + + $"--profile slc500 up -d), attach real hardware, or override {EndpointEnvVar}."; + } + + // TCP reaches — but is the peer a real PLC (wire-trustworthy) or ab_server's PCCC + // mode (dispatcher is upstream-broken, every read surfaces BadCommunicationError)? + // We can't detect it at the wire without issuing a full libplctag session, so we + // require an explicit opt-in for wire-level runs. See + // `tests/.../Docker/README.md` §"Known limitations" for the upstream-tracking pointer. + if (Environment.GetEnvironmentVariable(TrustWireEnvVar) is not { Length: > 0 } trust + || !(trust == "1" || string.Equals(trust, "true", StringComparison.OrdinalIgnoreCase))) + { + return $"AB Legacy endpoint at {host}:{port} is reachable but {TrustWireEnvVar} is not set. " + + "ab_server's PCCC dispatcher is a known upstream gap (libplctag/libplctag), so by " + + "default the integration suite assumes the simulator is in play and skips. Set " + + $"{TrustWireEnvVar}=1 when pointing at real SLC 5/05 / MicroLogix 1100/1400 / PLC-5 " + + "hardware or a RSEmulate 500 golden-box."; + } + + return null; } private static (string Host, int Port) ResolveEndpoint() @@ -129,16 +166,19 @@ public sealed class AbLegacyServerCollection : Xunit.ICollectionFixture -/// [Fact]-equivalent that skips when the PCCC simulator isn't reachable. +/// [Fact]-equivalent that skips when the PCCC endpoint isn't wire-trustworthy. +/// See for the exact skip semantics. /// public sealed class AbLegacyFactAttribute : FactAttribute { public AbLegacyFactAttribute() { if (!AbLegacyServerFixture.IsServerAvailable()) - Skip = "AB Legacy PCCC simulator not reachable. Start the Docker container " + - "(docker compose -f Docker/docker-compose.yml --profile slc500 up -d) " + - "or set AB_LEGACY_ENDPOINT."; + Skip = "AB Legacy PCCC endpoint not wire-trustworthy. Either no simulator is " + + "running, or the Docker ab_server is up but AB_LEGACY_TRUST_WIRE is not " + + "set (ab_server's PCCC dispatcher is a known upstream gap). Set " + + "AB_LEGACY_TRUST_WIRE=1 when pointing AB_LEGACY_ENDPOINT at real hardware " + + "or a RSEmulate 500 golden-box."; } } @@ -150,8 +190,10 @@ public sealed class AbLegacyTheoryAttribute : TheoryAttribute public AbLegacyTheoryAttribute() { if (!AbLegacyServerFixture.IsServerAvailable()) - Skip = "AB Legacy PCCC simulator not reachable. Start the Docker container " + - "(docker compose -f Docker/docker-compose.yml --profile slc500 up -d) " + - "or set AB_LEGACY_ENDPOINT."; + Skip = "AB Legacy PCCC endpoint not wire-trustworthy. Either no simulator is " + + "running, or the Docker ab_server is up but AB_LEGACY_TRUST_WIRE is not " + + "set (ab_server's PCCC dispatcher is a known upstream gap). Set " + + "AB_LEGACY_TRUST_WIRE=1 when pointing AB_LEGACY_ENDPOINT at real hardware " + + "or a RSEmulate 500 golden-box."; } } diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md index 23cea31..a671cb6 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests/Docker/README.md @@ -47,6 +47,13 @@ families stop the current service + start another. - Override with `AB_LEGACY_ENDPOINT=host:port` to point at a real SLC / MicroLogix / PLC-5 PLC on its native port. +## Env vars + +| Var | Default | Purpose | +|---|---|---| +| `AB_LEGACY_ENDPOINT` | `localhost:44818` | `host:port` of the PCCC endpoint. | +| `AB_LEGACY_TRUST_WIRE` | *unset* | Opt-in promise that the endpoint is a real PLC or RSEmulate 500 golden-box (not ab_server). Required for integration tests to actually run; without it the tests skip with an upstream-gap message even when TCP reaches a listener. See the **Known limitations** section below. | + ## Run the integration tests In a separate shell with a container up: @@ -56,9 +63,20 @@ cd C:\Users\dohertj2\Desktop\lmxopcua dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests ``` -`AbLegacyServerFixture` TCP-probes `localhost:44818` at collection init + -records a skip reason when unreachable. Tests use `[AbLegacyFact]` / -`[AbLegacyTheory]` which check the same probe. +Against the Docker ab_server the suite **skips** with a pointer to the +upstream gap (see **Known limitations**). Against real SLC / MicroLogix / +PLC-5 hardware or a RSEmulate 500 box: + +```powershell +$env:AB_LEGACY_ENDPOINT = "10.0.1.50:44818" +$env:AB_LEGACY_TRUST_WIRE = "1" +dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests +``` + +`AbLegacyServerFixture` TCP-probes the endpoint at collection init and sets +a skip reason that captures **both** cases: unreachable endpoint *and* +reachable-but-wire-untrusted. Tests use `[AbLegacyFact]` / `[AbLegacyTheory]` +which check the same gate. ## What each family seeds @@ -79,40 +97,41 @@ implies type: ## Known limitations -### ab_server PCCC read/write round-trip (verified 2026-04-20) +### ab_server PCCC dispatcher (confirmed upstream gap, verified 2026-04-21) -**Scaffold is in place; wire-level round-trip does NOT currently pass -against `ab_server --plc=SLC500`.** With the SLC500 compose profile up, -TCP 44818 accepts connections and libplctag negotiates the session, -but the three smoke tests in `AbLegacyReadSmokeTests.cs` all fail at -read/write with `BadCommunicationError` (libplctag status `0x80050000`). -Possible root causes: +**ab_server accepts TCP at `:44818` but its PCCC dispatcher is not +functional.** Running with `--plc=SLC500 --debug=5` shows no request +logs when libplctag issues a read, and every read surfaces as +`BadCommunicationError` (libplctag status `0x80050000`). This matches +the libplctag docs' description of PCCC support as less-mature than +CIP in the bundled `ab_server` tool. -- ab_server's PCCC server-side opcode coverage may be narrower than - libplctag's PCCC client expects — the tool is primarily a CIP - server; PCCC was added later + is noted in libplctag docs as less - mature. -- libplctag's PCCC-over-CIP encapsulation may assume a real SLC 5/05 - EtherNet/IP NIC's framing that ab_server doesn't emit. +**Fixture behavior.** To avoid a loud row of failing tests on the +integration host every time someone `docker compose up`s the SLC500 +profile, `AbLegacyServerFixture` gates on a second env var +`AB_LEGACY_TRUST_WIRE`. The matrix: -The scaffold ships **as-is** because: +| Endpoint reachable? | `AB_LEGACY_TRUST_WIRE` set? | Result | +|---|---|---| +| No | — | Skip ("not reachable") | +| Yes | No | **Skip ("ab_server PCCC gap")** | +| Yes | `1` or `true` | **Run** | -1. The Docker infrastructure + fixture pattern works cleanly (probe - passes, container lifecycle is clean, tests skip when absent). -2. The test classes target the correct shape for what the AB Legacy - driver would do against real hardware. -3. Pointing `AB_LEGACY_ENDPOINT` at a real SLC 5/05 / MicroLogix - 1100 / 1400 should make the tests pass outright — the failure - mode is ab_server-specific, not driver-specific. +The test bodies themselves are correct for real hardware — point +`AB_LEGACY_ENDPOINT` at a real SLC 5/05 / MicroLogix 1100/1400 / +PLC-5, set `AB_LEGACY_TRUST_WIRE=1`, and the smoke tests round-trip +cleanly. Resolution paths (pick one): 1. **File an ab_server bug** in `libplctag/libplctag` to expand PCCC server-side coverage. 2. **Golden-box tier** via Rockwell RSEmulate 500 — closer to real - firmware, but license-gated + RSLinx-dependent. + firmware, but license-gated + RSLinx-dependent. Set + `AB_LEGACY_TRUST_WIRE=1` when the endpoint points at an Emulate + box. 3. **Lab rig** — used SLC 5/05 / MicroLogix 1100 on a dedicated - network; the authoritative path. + network (task #222); the authoritative path. ### Other known gaps (unchanged from ab_server)