test(ablegacy): deploy the PCCC fixture, one port per family (#534)

The AbLegacy integration suite has never run against a simulator.
/opt/otopcua-ablegacy/ did not exist on the Docker host — the stack was written
but never deployed — and every ab_server across BOTH ab fixtures bound :44818.
So the AB CIP fixture's ControlLogix container (up for two weeks) answered this
suite's TCP reachability probe, the suite declined to skip, and it ran SLC500-mode
PCCC against a ControlLogix. Four failures that read as "fixture missing".

Two structural problems, both fixed here:

- The theory parameterises over all three PCCC families, but only one container
  could ever exist on the shared port, so at least two cases failed on every
  conceivable run. The suite was un-greenable by construction, and
  AB_LEGACY_COMPOSE_PROFILE existed to paper over it by filtering the theory
  down to whichever container happened to be up.
- A reachability probe that cannot tell one simulator from another is not a skip
  gate. It answered "is some ab_server listening", which is a different question
  from the one the caller is asking, and reported the difference as a test
  failure.

Each family now has its own host port — 44819 slc500, 44820 micrologix, 44821
plc5 — so all three run together and alongside AbCip on 44818 (ab_server still
listens on 44818 inside each container; only the host mapping differs). Skip is
per-family via SkipReasonFor(profile); the class-level gate runs if ANY family is
up so one down container cannot silently skip the other two.
AB_LEGACY_COMPOSE_PROFILE now applies only in AB_LEGACY_ENDPOINT-pinned
real-hardware mode, where the family genuinely is a property of the one device.
This finally makes true what the fixture's XML doc has claimed from the start —
that it and the AB CIP fixture can point at different containers simultaneously.

Deployed to /opt/otopcua-ablegacy/ on 10.100.0.35; all three containers running.

The 4 tests still FAIL, and are left red on purpose — see #534. Now aimed at
correct-family simulators, the PCCC exchange itself returns BadCommunicationError
for every server-path / client-cip-path permutation, reproduced outside the test
harness via the driver CLI. That is a true signal about the driver (or about
libplctag's PCCC support against ab_server) that skipping would hide. What
changed here is that the failure is now diagnostic instead of an artifact of a
port collision.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
Joseph Doherty
2026-08-01 02:18:53 -04:00
parent df340b8bba
commit b13bdc154a
4 changed files with 114 additions and 55 deletions
@@ -13,7 +13,7 @@ Desktop and nothing else.
| File | Purpose |
|---|---|
| [`docker-compose.yml`](docker-compose.yml) | Three per-family services (`slc500` / `micrologix` / `plc5`); all bind `:44818` |
| [`docker-compose.yml`](docker-compose.yml) | Three per-family services — `slc500` on `:44819`, `micrologix` on `:44820`, `plc5` on `:44821`. Run them together; they no longer collide with each other or with the AB CIP fixture on `:44818`. |
## Run
@@ -38,12 +38,12 @@ docker compose -f tests\...\Docker\docker-compose.yml --profile slc500 down
First run builds the `otopcua-ab-server:libplctag-release` image (~3-5
min — clones libplctag + compiles `ab_server`). If the AB CIP fixture
already built the image locally, docker reuses the cached layers + this
runs in seconds. Only one family binds `:44818` at a time; to switch
runs in seconds. (Until 2026-08-01 every family bound `:44818`, so only one could run and the theory's other two cases always failed; each now has its own port.) To switch
families stop the current service + start another.
## Endpoint
- Default: `localhost:44818` (EtherNet/IP standard)
- Default: `10.100.0.35:44819` / `:44820` / `:44821` per family (the shared Docker host)
- Override with `AB_LEGACY_ENDPOINT=host:port` to point at a real SLC /
MicroLogix / PLC-5 PLC on its native port.
@@ -51,9 +51,9 @@ families stop the current service + start another.
| Var | Default | Purpose |
|---|---|---|
| `AB_LEGACY_ENDPOINT` | `localhost:44818` | `host:port` of the PCCC endpoint. |
| `AB_LEGACY_ENDPOINT` | *unset* — per-family ports on `10.100.0.35` | `host:port` of a single PCCC device, pinning **every** family to it (real-hardware mode). |
| `AB_LEGACY_CIP_PATH` | `1,0` | CIP routing path portion of the `ab://host:port/<path>` URI. ab_server rejects empty paths at the CIP unconnected-send layer; real SLC/MicroLogix/PLC-5 hardware accepts empty (no backplane). Set to empty (`AB_LEGACY_CIP_PATH=`) when pointing at real hardware. |
| `AB_LEGACY_COMPOSE_PROFILE` | *unset* | When set (e.g. `slc500`), the parametric theory filters to that profile. Only one compose container binds `:44818` at a time; set this to the profile currently up so the suite doesn't try to hit e.g. the Slc500 family against the PLC-5 container. Leave unset for real-hardware runs (all 3 families parameterize). |
| `AB_LEGACY_COMPOSE_PROFILE` | *unset* | Only consulted when `AB_LEGACY_ENDPOINT` pins an endpoint: names which family that one device is, so the SLC500-only write test skips against other hardware. Against the Docker fixture it is unnecessary — each family has its own port, and each test skips itself if its own port is down. |
## Run the integration tests
@@ -5,15 +5,20 @@
# so `docker compose build` from here produces the same image if it
# doesn't already exist; if it does, docker's cache reuses the layer.
#
# One service per PCCC family. All bind :44818 on the host; run one at a
# time. PCCC tag format differs from CIP: `<file>[<size>]` without a
# type suffix since the type is implicit in the file letter (N = INT,
# F = REAL, B = bit-packed, L = DINT).
# One service per PCCC family, each on its OWN host port — 44819 slc500,
# 44820 micrologix, 44821 plc5 — so all three run together, and alongside the
# AB CIP fixture, which owns :44818. They all bound :44818 until 2026-08-01,
# which made the suite structurally un-greenable: the theory parameterises over
# all three families, but only one container could ever exist, so at least two
# cases failed on every run. Worse, the AB CIP ControlLogix container answers
# on :44818 too, so the fixture's TCP reachability probe saw "up", declined to
# skip, and ran SLC500-mode PCCC against a ControlLogix.
#
# Usage:
# docker compose --profile slc500 up
# docker compose --profile micrologix up
# docker compose --profile plc5 up
# ab_server still listens on 44818 INSIDE the container (--port=44818); only the
# host mapping differs. Keep these in step with AbLegacyServerProfile.Port.
#
# Usage (all three at once — that is the point):
# docker compose --profile slc500 --profile micrologix --profile plc5 up -d
services:
slc500:
profiles: ["slc500"]
@@ -24,7 +29,7 @@ services:
container_name: otopcua-ab-server-slc500
restart: "no"
ports:
- "44818:44818"
- "44819:44818"
command: [
"ab_server",
"--plc=SLC500",
@@ -44,7 +49,7 @@ services:
container_name: otopcua-ab-server-micrologix
restart: "no"
ports:
- "44818:44818"
- "44820:44818"
command: [
"ab_server",
"--plc=Micrologix",
@@ -63,7 +68,7 @@ services:
container_name: otopcua-ab-server-plc5
restart: "no"
ports:
- "44818:44818"
- "44821:44818"
command: [
"ab_server",
"--plc=PLC/5",