Merge pull request 'Task #224 close — AB Legacy PCCC fixture: AB_LEGACY_TRUST_WIRE opt-in' (#202) from task-224-close-ablegacy-fixture into v2

This commit was merged in pull request #202.
This commit is contained in:
2026-04-21 04:19:49 -04:00
2 changed files with 102 additions and 41 deletions

View File

@@ -28,6 +28,16 @@ public sealed class AbLegacyServerFixture : IAsyncLifetime
{
private const string EndpointEnvVar = "AB_LEGACY_ENDPOINT";
/// <summary>
/// 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 <c>docs/v2/lmx-followups.md</c>). Without this, the fixture assumes
/// the endpoint is libplctag's <c>ab_server --plc=SLC500</c> Docker container — whose
/// PCCC dispatcher is a known upstream gap — and skips cleanly rather than failing
/// every test with <c>BadCommunicationError</c>.
/// </summary>
private const string TrustWireEnvVar = "AB_LEGACY_TRUST_WIRE";
/// <summary>Standard EtherNet/IP port. PCCC-over-CIP rides on the same port as
/// native CIP; the differentiator is the <c>--plc</c> flag ab_server was started
/// with, not a different TCP listener.</summary>
@@ -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;
/// <summary>
/// Used by <see cref="AbLegacyFactAttribute"/> + <see cref="AbLegacyTheoryAttribute"/>
/// 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.
/// </summary>
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<AbLegacy
}
/// <summary>
/// <c>[Fact]</c>-equivalent that skips when the PCCC simulator isn't reachable.
/// <c>[Fact]</c>-equivalent that skips when the PCCC endpoint isn't wire-trustworthy.
/// See <see cref="AbLegacyServerFixture"/> for the exact skip semantics.
/// </summary>
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.";
}
}

View File

@@ -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)