13 KiB
ab_server test fixture
Coverage map + gap inventory for the AB CIP integration fixture backed by
libplctag's ab_server simulator.
TL;DR: ab_server is a connectivity + atomic-read smoke harness for the AB
CIP driver. It does not benchmark UDTs, alarms, or any family-specific
quirk. UDT / alarm / quirk behavior is verified only by unit tests with
FakeAbCipTagRuntime.
What the fixture is
- Binary:
ab_server— a C program from the upstream libplctag/libplctag repository (MIT license). It is not part of this repo's source tree;Docker/Dockerfileclones libplctag at a pinned tag and builds theab_serverCMake target in a multi-stage build. - Launcher: Docker (only supported path).
Docker/Dockerfilemulti-stage-buildsab_serverfrom source by cloning libplctag at a pinned tag + copies the binary into a slim runtime image.Docker/docker-compose.ymlhas per-family services (controllogix/compactlogix/micro800/guardlogix); all bind:44818. - Lifecycle:
AbServerFixtureTCP-probes10.100.0.35:44818(the shared Docker host) at collection init + records a skip reason when unreachable. Tests skip via[AbServerFact]/[AbServerTheory]which check the same probe. - Profiles:
KnownProfiles.{ControlLogix, CompactLogix, Micro800, GuardLogix}inAbServerProfile.cs— thin Family + ComposeProfile + Notes records; the compose file is the canonical source of truth for which tags get seeded + which--plcmode the simulator boots in. - Tests: one smoke,
AbCipReadSmokeTests.Driver_reads_seeded_DInt_from_ab_server, parametrized over all four profiles via[AbServerTheory]+[MemberData]. - Endpoint override:
AB_SERVER_ENDPOINT=host:portpoints the fixture at a real PLC instead of the local container.
Per-transport suites and capture recipe
Every integration test in AbCip.IntegrationTests now runs once per transport (TransportMatrix:
Libplctag and Native). Run one compose profile at a time and name it with AB_SERVER_COMPOSE_PROFILE
(all profiles bind :44818, so set it to the running profile). On the 2026-10-07 run native passed everything
libplctag passed on all seven profiles; on micro800 libplctag failed on the empty-path defect described in
AbCip.md and native passed. That defect is now fixed: a re-run of the micro800
smoke test passes on both transports. Evidence:
native-ab-transport-gate-results.
- Write-capable tests (the Logix Emulate ALMD test) require
AB_SERVER_ALLOW_WRITES=1and refuse whileAB_DIFF_ENDPOINTis set. - Differential harness (
AbCip.IntegrationTests/Differential,LiveIntegration) compares libplctag and native on browse, template and read parity against one endpoint. It is read-only by construction, aborts on the first connection failure, never retries, and caps the native phase at one Forward Open. Env:AB_DIFF_ENDPOINT,AB_DIFF_PATH,AB_DIFF_FAMILY,AB_DIFF_TAGS,AB_DIFF_MAX_TAGS(at most 500),AB_DIFF_MAX_TEMPLATES,AB_DIFF_PACE_MS(default 20),AB_DIFF_COUNT_OPENS. Always run with--filter "FullyQualifiedName~Differential"andAB_SERVER_ENDPOINT=127.0.0.1:1. - ab_server serves no UDTs and refuses 1-byte writes, so template parity is vacuous there and BOOL/SINT parity is weak evidence. Native writes remain simulator-only.
- Golden-frame capture:
scripts/ab-capture/capture.sh(capture client inscripts/ab-capture/client); fixtures live intests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Captures/with a README.
What it actually covers
- Read path: driver → libplctag → CIP-over-EtherNet/IP → simulator → back.
- Atomic Logix types per seed:
DINT,REAL,BOOL,SINT,STRING. - One
DINT[16]array tag (ControlLogix profile only). --plc controllogixand--plc compactlogixmode dispatch.- The skip-on-missing-binary behavior (
AbServerFactAttribute) so a fresh clone without the simulator stays green.
What it does NOT cover
Each gap below is either stated explicitly in the profile's Notes field or
inferable from the seed-tag set + smoke-test surface.
1. UDTs / CIP Template Object (class 0x6C)
ControlLogix profile Notes: "ab_server lacks full UDT emulation."
Unverified against ab_server:
- PR 6 structured read/write (
AbCipStructureMemberfan-out) - #179 Template Object shape reader (
CipTemplateObjectDecoder+FetchUdtShapeAsync) - #194 whole-UDT read optimization (
AbCipUdtReadPlanner+AbCipUdtMemberLayout+ theReadGroupAsyncpath inAbCipDriver)
Unit coverage: AbCipFetchUdtShapeTests, CipTemplateObjectDecoderTests,
AbCipUdtMemberLayoutTests, AbCipUdtReadPlannerTests,
AbCipDriverWholeUdtReadTests — all with golden Template-Object byte buffers
- offset-keyed
FakeAbCipTagvalues.
2. ALMD / ALMA alarm projection (#177)
Depends on the ALMD UDT shape, which ab_server cannot emulate. The
OnAlarmEvent raise/clear path + ack-write semantics are not exercised
end-to-end.
Unit coverage: AbCipAlarmProjectionTests — fakes feed InFaulted /
Severity via ValuesByOffset + assert the emitted AlarmEventArgs.
3. Micro800 unconnected-only path
Micro800 profile Notes: "--plc=Micro800 mode (unconnected-only, empty path).
Driver-side enforcement verified in the unit suite."
The compose service boots ab_server --plc=Micro800 with an empty routing path.
The unconnected-session requirement (PR 11) is validated at the driver unit-test
level via FakeAbCipTagRuntime; the wire-level contract (what happens when
a connected-send arrives at a real Micro800 backplane) is not exercised by the
simulator. Real Micro800 (2080-series) on a lab rig would be the authoritative
benchmark.
4. GuardLogix safety subsystem
GuardLogix profile Notes: "ab_server doesn't emulate the safety
subsystem."
Only the _S-suffix naming classifier (PR 12, SecurityClassification.ViewOnly
forced on safety tags) runs. Actual safety-partition write rejection — what
happens when a non-safety write lands on a real 1756-L8xS — is not exercised.
5. CompactLogix connection size and the regular-open fallback
Since T21 the CompactLogix profile defaults to a Large Forward Open (4002), matching what libplctag
opens for plc=compactlogix. The old 504 default was cosmetic under libplctag, which never received it,
and cost the native transport ~3× the round trips per poll. A narrower 5069-L3x / pre-v20 target that refuses
the Large open is handled by CipConnection's fallback (a regular open at min(size, 504) in the same
throttled attempt). That fallback is pinned by unit tests against a fake target only: ab_server accepts
any size, so a refusing controller is never exercised here.
6. BOOL-within-DINT read-modify-write (#181)
The AbCipDriver.WriteBitInDIntAsync RMW path + its per-parent SemaphoreSlim
serialization is unit-tested only (AbCipBoolInDIntRmwTests). ab_server
seeds a plain TestBOOL tag; the .N bit-within-DINT syntax that triggers
the RMW path is not exercised end-to-end.
7. Capability surfaces beyond read
No smoke test for:
IWritable.WriteAsyncITagDiscovery.DiscoverAsync(@tagswalker)ISubscribable.SubscribeAsync(poll-group engine)IHostConnectivityProbestate transitions under wire failureIPerCallHostResolvermulti-device routing
The driver implements all of these + they have unit coverage, but the only
end-to-end path ab_server validates today is atomic ReadAsync.
Logix Emulate golden-box tier
Rockwell Studio 5000 Logix Emulate sits above ab_server in fidelity + below real hardware. When an operator has Emulate running on a reachable Windows box + sets two env vars, the suite promotes several behaviours from unit-only to end-to-end wire-level coverage:
$env:AB_SERVER_PROFILE = 'emulate'
$env:AB_SERVER_ENDPOINT = '<emulate-pc-ip>:44818'
dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests
With AB_SERVER_PROFILE unset or abserver, the Emulate-tier classes
skip cleanly + the ab_server Docker fixture runs as usual.
| Gap this fixture doc calls out | ab_server | Logix Emulate | Real hardware |
|---|---|---|---|
| UDT / CIP Template Object (task #194) | no | yes | yes |
| ALMD alarm projection (task #177) | no | yes | yes |
@tags Symbol Object walk with Program: scope |
partial | yes | yes |
| Add-On Instructions | no | yes | yes |
| GuardLogix safety-partition write rejection | no | yes (Emulate 5580) | yes |
| CompactLogix Large-open refusal → regular-open fallback | no | yes (5370 firmware) | yes |
| EtherNet/IP embedded-switch behaviour | no | no | yes |
| Redundant chassis failover (1756-RM) | no | no | yes |
| Motion control timing | no | no | yes |
Tests that promote to Emulate (gated on AB_SERVER_PROFILE=emulate
via AbServerProfileGate.SkipUnless):
AbCipEmulateUdtReadTests.WholeUdt_read_decodes_each_member_at_its_Template_Object_offset— #194 whole-UDT optimization, verified against real Template Object bytesAbCipEmulateAlmdTests.Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection— #177 ALMD projection, verified against the real ALMD instruction
Required Studio 5000 project state is documented in
tests/…/AbCip.IntegrationTests/LogixProject/README.md;
the .L5X export lands there once the Emulate PC is on-site + the
project is authored.
Costs to accept:
- Rockwell TechConnect or per-seat license — not redistributable; not CI-runnable. Each operator licenses their own Emulate install.
- Windows-only + Hyper-V conflict — Emulate can't coexist with
Docker Desktop's WSL 2 backend on the same OS, same way TwinCAT XAR
can't (see
docs/v2/dev-environment.md§Integration host). - Manual lifecycle — no
docker compose upequivalent; operator opens Emulate, loads the L5X, clicks Run. The L5X in the repo keeps project state reproducible, runtime-start is human.
When to trust ab_server, when to reach for a rig
| Question | ab_server | Unit tests | Logix Emulate | Lab rig |
|---|---|---|---|---|
| "Does the driver talk CIP at all?" | yes | - | yes | - |
| "Is my atomic read path wired correctly?" | yes | yes | yes | yes |
| "Does whole-UDT grouping work?" | no | yes | yes | yes |
| "Do ALMD alarms raise + clear?" | no | yes | yes | yes |
| "Is Micro800 unconnected-only enforced wire-side?" | partial (--plc=Micro800 boots, but wire rejection untested) | partial | yes | yes (required) |
| "Does GuardLogix reject non-safety writes on safety tags?" | no | no | yes (Emulate 5580) | yes |
| "Does CompactLogix refuse a Large Forward Open?" | no | partial | yes (5370 firmware) | yes |
| "Does BOOL-in-DINT RMW race against concurrent writers?" | no | yes | partial | yes (stress) |
| "Does EtherNet/IP embedded-switch behave correctly?" | no | no | no | yes (required) |
| "Does redundant-chassis failover work?" | no | no | no | yes (required) |
Follow-up candidates
If integration-level UDT / alarm / quirk proof becomes a shipping gate, the options are roughly:
- Logix Emulate golden-box tier (scaffolded; see the section above) — highest-fidelity path short of real hardware. Closes UDT / ALMD / AOI / optimized-DB gaps in one license + one Windows PC.
- Extend
ab_serverupstream — the project accepts PRs + already carries a CIP framing layer that UDT emulation could plug into. - Stand up a lab rig — physical
1756-L7x/5069-L3x/2080-LC30/1756-L8xScontrollers. The only path that covers safety partitions across nodes, redundant chassis, embedded-switch behaviour, and motion timing.
See also:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerFixture.cstests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cstests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs—AB_SERVER_PROFILEtier gatetests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cstests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/— ab_server image + composetests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/— Logix Emulate tier teststests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md— L5X project state the Emulate tier expectsdocs/v2/test-data-sources.md§2 — the broader test-data-source picking rationale this fixture slots into