Files
lmxopcua/docs/drivers/AbServer-Test-Fixture.md
T

13 KiB
Raw Blame History

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/Dockerfile clones libplctag at a pinned tag and builds the ab_server CMake target in a multi-stage build.
  • Launcher: Docker (only supported path). Docker/Dockerfile multi-stage-builds ab_server from source by cloning libplctag at a pinned tag + copies the binary into a slim runtime image. Docker/docker-compose.yml has per-family services (controllogix / compactlogix / micro800 / guardlogix); all bind :44818.
  • Lifecycle: AbServerFixture TCP-probes 10.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} in AbServerProfile.cs — thin Family + ComposeProfile + Notes records; the compose file is the canonical source of truth for which tags get seeded + which --plc mode 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:port points 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=1 and refuse while AB_DIFF_ENDPOINT is 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" and AB_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 in scripts/ab-capture/client); fixtures live in tests/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 controllogix and --plc compactlogix mode 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 (AbCipStructureMember fan-out)
  • #179 Template Object shape reader (CipTemplateObjectDecoder + FetchUdtShapeAsync)
  • #194 whole-UDT read optimization (AbCipUdtReadPlanner + AbCipUdtMemberLayout + the ReadGroupAsync path in AbCipDriver)

Unit coverage: AbCipFetchUdtShapeTests, CipTemplateObjectDecoderTests, AbCipUdtMemberLayoutTests, AbCipUdtReadPlannerTests, AbCipDriverWholeUdtReadTests — all with golden Template-Object byte buffers

  • offset-keyed FakeAbCipTag values.

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.WriteAsync
  • ITagDiscovery.DiscoverAsync (@tags walker)
  • ISubscribable.SubscribeAsync (poll-group engine)
  • IHostConnectivityProbe state transitions under wire failure
  • IPerCallHostResolver multi-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 bytes
  • AbCipEmulateAlmdTests.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 up equivalent; 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:

  1. 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.
  2. Extend ab_server upstream — the project accepts PRs + already carries a CIP framing layer that UDT emulation could plug into.
  3. Stand up a lab rig — physical 1756-L7x / 5069-L3x / 2080-LC30 / 1756-L8xS controllers. 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.cs
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfile.cs
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbServerProfileGate.cs — AB_SERVER_PROFILE tier gate
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker/ — ab_server image + compose
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Emulate/ — Logix Emulate tier tests
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/LogixProject/README.md — L5X project state the Emulate tier expects
  • docs/v2/test-data-sources.md §2 — the broader test-data-source picking rationale this fixture slots into