Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/README.md
T
Joseph Doherty c2c3c52274
v2-ci / build (push) Successful in 4m17s
v2-ci / unit-tests (push) Failing after 14m43s
fix(abcip): unblock the controller-browse gate, and fix the decoder it exposed (#468)
The AbCip controller browse could never be verified end to end: libplctag's
ab_server answers CIP service 0x55 (Get Instance Attribute List) on the Symbol
Object class with "unsupported", so the `@tags` walk had no backend. #468's own
definition of done offered a sim that serves `@tags` as an alternative to real
hardware; this is that.

The fixture now applies a vendored patch teaching ab_server to serve the Symbol
Object listing, with partial-transfer paging. Correctness is checked against
libplctag's own `list_tags_logix` rather than against our decoder — our decoder
is the thing under test, so its agreement would prove nothing. The clone is also
pinned to a concrete tag: the Dockerfile called `--branch release` "pinned" while
tracking a branch that moves.

That immediately exposed a real defect. CipSymbolObjectDecoder advanced to an
even byte boundary after each symbol name, but entries are packed with no
padding — libplctag walks them by sizeof(entry) + string_len, and a captured
reply confirms it. Any odd-length name therefore desynchronised every entry
behind it, and odd-length names are ordinary. In the live browse below,
`Motor_Run` is 9 bytes and first on the wire, so the old decoder would have
shown that one tag and silently dropped the other eight.

Every existing multi-entry test passed throughout, because the test builder
padded exactly as the decoder did: the two agreed with each other and neither
agreed with a controller. The builder no longer pads, and the layout is pinned
by a captured reply (CipSymbolObjectWireCaptureTests) instead of by a second
statement of the assumption. Restoring the alignment step reddens exactly the
three new tests and none of the 29 others.

Live gate on docker-dev, all three of #468's criteria:
  - populated tree — all nine seeded tags render as leaves, in wire order
  - refresh re-captures
  - leaf commit — Pump_1_Speed + TestDINT saved with their CIP tag paths

The gate also surfaced two things it was not asked about: browse-commit drops
the discovered data type, so a browsed REAL commits as a DINT and the driver
decodes it as one (filed as #536), and Test Connect fails against this fixture
regardless of the device because it probes `@raw_cpu_type` (documented).

TwinCAT and FOCAS still have no browsable backend; #468's AbCip half is closed,
theirs is not.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-08-01 12:25:15 -04:00

7.0 KiB

AB CIP integration-test fixture — ab_server (Docker)

libplctag's ab_server — a MIT-licensed C program that emulates a ControlLogix / CompactLogix CIP endpoint over EtherNet/IP. Docker is the only supported launch path; ab_server ships as a source-only tool under libplctag's src/tools/ab_server/ so the Dockerfile's multi-stage build is the only reproducible way to get a working binary across developer boxes. A fresh clone needs Docker Desktop and nothing else.

File Purpose
Dockerfile Multi-stage: build from libplctag at pinned tag → copy binary into debian:bookworm-slim runtime image
docker-compose.yml One service per family (controllogix / compactlogix / micro800 / guardlogix); all bind :44818
patches/ Applied to the libplctag checkout before the build — see Local patches

Local patches

patches/0001-ab_server-serve-cip-symbol-object-listing.patch teaches ab_server to answer CIP service 0x55 (Get Instance Attribute List) on class 0x6B, the Symbol Object listing a client reads through the @tags pseudo-tag to enumerate a controller's symbol table. Upstream answers unsupported, which is what left the AbCip controller browse unverifiable (Gitea #468) — the browse binding was proven alive, but no backend ever returned a tag.

Two things worth knowing before touching it:

  • The reply layout is fixed to the attribute set libplctag asks for — {0x02 symbol type, 0x07 element size, 0x08 array dimensions, 0x01 symbol name}. A request for a different set is refused rather than answered with a differently-shaped reply the client would misparse.
  • Entries are packed with no padding. The next entry begins on the byte after the last name byte, even on an odd offset. This is not a detail of the fixture: it is what a controller does, and assuming otherwise is exactly the decoder defect the gate found.

Correctness is checked against libplctag's own list_tags_logix client, not against our decoder — our decoder is the thing under test, so agreeing with it would prove nothing. list_tags_logix is the reference implementation exercised against real Logix hardware:

git clone --depth 1 --branch v2.7.0 https://github.com/libplctag/libplctag.git
cd libplctag && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build --parallel
./build/bin_dist/list_tags_logix 10.100.0.35 1,0    # expect all nine tags, "SUCCESS!"

Partial-transfer paging is exercised the same way by starting ab_server with a few hundred tags: all of them should come back, with no duplicates and no gaps at the page boundaries.

Bumping LIBPLCTAG_TAG means re-checking the patch against the new tag. It is a concrete tag rather than the release branch precisely so that this stays a deliberate act — the branch moves, and the Dockerfile previously called it "pinned" while tracking it.

Run

From the repo root:

# ControlLogix — widest-coverage profile
docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests\Docker\docker-compose.yml --profile controllogix up

# Per-family
docker compose -f tests\...\Docker\docker-compose.yml --profile compactlogix up
docker compose -f tests\...\Docker\docker-compose.yml --profile micro800 up
docker compose -f tests\...\Docker\docker-compose.yml --profile guardlogix up

Detached + stop:

docker compose -f tests\...\Docker\docker-compose.yml --profile controllogix up -d
docker compose -f tests\...\Docker\docker-compose.yml --profile controllogix down

First run builds the image (~3-5 minutes — clones libplctag + compiles ab_server + its dependencies). Subsequent runs are fast because the multi-stage build layer-caches the checkout + compile.

Endpoint

  • Default: localhost:44818 (EtherNet/IP standard; non-privileged)
  • Override with AB_SERVER_ENDPOINT=host:port to point at a real PLC.

Run the integration tests

In a separate shell with a container up:

cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests

AbServerFixture TCP-probes localhost:44818 at collection init + records a skip reason when unreachable, so tests stay green on a fresh clone without the container running. Tests use [AbServerFact] / [AbServerTheory] which check the same probe.

What each family seeds

Tag sets match AbServerProfile.cs exactly — changing seeds in one place means updating both.

Family Seeded tags Notes
ControlLogix TestDINT TestREAL TestBOOL TestSINT TestString TestArray Pump_1_Speed Line1_Count Motor_Run Widest-coverage; PR 9 baseline. UDT emulation missing from ab_server. The last three serve the controller-browse gate and are additive — the read/write suites key off the Test* tags. Motor_Run (9), Line1_Count (11) and TestArray (9) have odd-length names on purpose: entries are unpadded, so an odd name puts every following entry on an odd offset, and a decoder that assumes even alignment loses everything behind it
CompactLogix TestDINT TestREAL TestBOOL Narrow ConnectionSize cap enforced driver-side; ab_server accepts any size
Micro800 TestDINT TestREAL ab_server has no micro800 mode; falls back to controllogix emulation
GuardLogix TestDINT SafetyDINT_S ab_server has no safety subsystem; _S suffix triggers driver-side classification only

Known limitations

  • No UDT / CIP Template Object emulation — ab_server covers atomic types only. UDT reads + task #194 whole-UDT optimization verify via unit tests with golden byte buffers. The Symbol Object patch above lists symbols, including their types; it does not add Template Object support, so a browsed UDT still cannot be expanded into members here.
  • No @raw_cpu_type — the driver's Test Connect probe falls back to that pseudo-tag when a device has no authored tags, and ab_server answers ErrorBadParam. So Test Connect reports failure against this fixture even when the device is fine, and browse works regardless. Author one tag and the probe uses it instead.
  • Controller-scope listing only — a program-scope listing arrives with a leading symbolic segment naming the program, and ab_server has no notion of programs. Program:-scoped tags cannot be browsed here.
  • Family-specific quirks trust driver-side code — ab_server emulates a generic Logix CPU; the ConnectionSize cap, empty-path unconnected mode, and safety-partition write rejection all need lab rigs for wire-level proof.

See docs/drivers/AbServer-Test-Fixture.md for the full coverage map.

References