f85fc8fbeb
- L2: WriteWidthRefusal leaves a BOOL over a wider value to a runtime that sees the CIP type (native): mistyped integer member -> BadTypeMismatch (as its read), 0xD3 BOOL-array DWORD -> BadNotSupported; libplctag keeps BadNotSupported for both (size only). - L4: direct theory over WriteWidthRefusal (1/2/4/8 widths incl LINT/ULINT/LREAL, String/Dt, unknown size) + array/bit-selector exemptions with a non-vacuous control. - L1: CLAUDE.md member/BIT-member writes are ab_server-proven only. - L3: patch 0003 blind spots documented (Docker README, AbServer-Test-Fixture.md); nits. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
198 lines
11 KiB
Markdown
198 lines
11 KiB
Markdown
# AB CIP integration-test fixture — `ab_server` (Docker)
|
|
|
|
[libplctag](https://github.com/libplctag/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`](Dockerfile) | Multi-stage: build from libplctag at pinned tag → copy binary into `debian:bookworm-slim` runtime image |
|
|
| [`docker-compose.yml`](docker-compose.yml) | One service per family (`controllogix` / `compactlogix` / `micro800` / `guardlogix`); all bind `:44818` |
|
|
| [`patches/`](patches/) | Applied to the libplctag checkout before the build, in filename order — see [Local patches](#local-patches) |
|
|
|
|
## Local patches
|
|
|
|
### 0001 — serve the Symbol Object listing
|
|
|
|
`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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
### 0002 — answer "tag does not exist" the way a controller does
|
|
|
|
`patches/0002-ab_server-answer-tag-not-found-with-cip-0x04.patch` changes the reply
|
|
to an unresolvable tag path from CIP **0x03** (invalid parameter) to CIP **0x04**
|
|
(*IOI could not be deciphered or tag does not exist*), which is what a Logix
|
|
controller returns. libplctag translates the two very differently — `ErrorBadParam`
|
|
versus `ErrorNotFound` — and every client that classifies by status code reads the
|
|
first as "your request was malformed" and the second as "the controller answered,
|
|
that tag isn't there."
|
|
|
|
The distinction is load-bearing here: the driver's Test Connect probe falls back to a
|
|
name no controller defines, so **"tag not found" is the successful answer** when a
|
|
device has no authored tags yet. Before this patch the fixture reported a perfectly
|
|
healthy sim as a failed handshake, and — worse — the discrepancy was written down as
|
|
a fixture limitation, which hid a real driver defect behind it for as long as it
|
|
stood (`AbCipDriverProbe.IsReachableException` matched the native C spellings, which
|
|
the .NET wrapper never produces, so the reachable classification could not fire at
|
|
all). `AbCip_WithNoAuthoredTags_ReportsTheControllerReachable` in
|
|
`Host.IntegrationTests` is the pin, and it needs the fixture **rebuilt** from this
|
|
patch, not merely running.
|
|
|
|
### 0003 — member paths, and libplctag's pad byte
|
|
|
|
`patches/0003-ab_server-resolve-member-paths-against-dotted-flat-tags.patch` does two
|
|
things for the discovered-UDT-member write suite (`AbCipUdtMemberWriteTests`, Gitea #611):
|
|
|
|
- **Member paths.** A Logix member path (`Seg.Sub.Code`) arrives as one CIP symbolic
|
|
segment per member. ab_server has no structures, so the patch joins consecutive symbolic
|
|
segments with `.` and looks up a flat tag whose `--tag` name is that dotted path (the name
|
|
charset now admits `.`); element segments after the last member work as before
|
|
(`UdtSeg.Points[2]`). This proves the wire of a member read/write — not template offsets
|
|
and not BIT-member semantics (each `UdtSeg.*` BOOL is its own byte, not a bit of a hidden
|
|
host). The `controllogix` profile seeds `UdtSeg.ID` / `.Enable` / `.Ready` / `.RampTime` /
|
|
`.Sub.Code` / `.Points[4]`, modelled on the Z-35162 press's `UDT_TemperatureProfile_Segment`.
|
|
- **libplctag's pad byte.** libplctag pads an odd-length write payload (a 1-byte SINT / BOOL)
|
|
with one byte; stock ab_server counted it as data and refused every such write with CIP
|
|
`0x03` (libplctag `ErrorBadParam`), so no libplctag BOOL/SINT write could pass here. The
|
|
patch drops exactly one trailing pad byte when it alone would overrun an odd-sized target.
|
|
|
|
**Blind spots this patch introduces (accepted for a fixture):** (a) the pad trim also accepts a genuinely
|
|
wrong 2-byte payload aimed at a 1-byte tag, so ab_server can no longer catch a client over-writing an
|
|
odd-sized target by exactly one byte — the driver's own pre-write width check (`AbCipDriver.WriteWidthRefusal`)
|
|
covers our paths; (b) because `.` is legal in a `--tag` name, a malformed *single* symbolic segment
|
|
`"UdtSeg.ID"` (which a real Logix rejects) also resolves, via the original exact-match lookup —
|
|
`NativeAbCipTagRuntimeTests.A_member_path_write_names_the_member_and_sends_only_its_own_bytes` pins that our
|
|
encoder splits members into separate segments.
|
|
|
|
**Bumping `LIBPLCTAG_TAG` means re-checking every 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:
|
|
|
|
```powershell
|
|
# 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:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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.
|
|
|
|
**Writes are opt-in.** Every write-capable test (today the Logix Emulate ALMD
|
|
test, which pulses `SimulateAlarm`) also requires `AB_SERVER_ALLOW_WRITES=1`,
|
|
and refuses while `AB_DIFF_ENDPOINT` is set. Simulator / Emulate runs set it;
|
|
**never** set it when `AB_SERVER_ENDPOINT` points at production hardware. The
|
|
ab_server controllogix matrix itself is read-only and needs no opt-in.
|
|
|
|
**Read-only differential (Task 18).** `Differential/` compares libplctag and the
|
|
native transport against one endpoint, configured only by `AB_DIFF_*`
|
|
variables (see `Differential/DifferentialTarget.cs`). Always run it with
|
|
`--filter "FullyQualifiedName~Differential"`, and keep the sibling suites'
|
|
discovery probes off-network with `AB_SERVER_ENDPOINT=127.0.0.1:1` when the
|
|
target is a real controller.
|
|
|
|
## 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` `UdtSeg.*` | Widest-coverage; PR 9 baseline. UDT emulation missing from ab_server; the `UdtSeg.*` member tags (patch 0003) serve member-path writes only. `Pump_1_Speed`, `Line1_Count` and `Motor_Run` 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.
|
|
- **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`](../../../docs/drivers/AbServer-Test-Fixture.md)
|
|
for the full coverage map.
|
|
|
|
## References
|
|
|
|
- [libplctag on GitHub](https://github.com/libplctag/libplctag)
|
|
- [`docs/drivers/AbServer-Test-Fixture.md`](../../../docs/drivers/AbServer-Test-Fixture.md) — coverage map
|
|
- [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md) §Docker fixtures
|