# Modbus test fixture Coverage map + gap inventory for the Modbus TCP driver's integration-test harness backed by `pymodbus` simulator profiles per PLC family. **TL;DR:** Modbus is the best-covered driver — a real `pymodbus` server on localhost with per-family seed-register profiles, plus a skip-gate when the simulator port isn't reachable. Covers DL205 / Mitsubishi MELSEC / Siemens S7-1500 family quirks end-to-end. Gaps are mostly error-path + alarm/history shaped (neither is a Modbus-side concept). ## What the fixture is - **Simulator**: `pymodbus` (Python, BSD) launched as a pinned Docker container at `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`. Docker is the only supported launch path. - **Lifecycle**: `ModbusSimulatorFixture` (collection-scoped) TCP-probes `localhost:5020` on first use. `MODBUS_SIM_ENDPOINT` env var overrides the endpoint so the same suite can target a real PLC. - **Profiles**: `DL205Profile`, `MitsubishiProfile`, `S7_1500Profile` — each composes device-specific register-format + quirk-seed JSON for pymodbus. Profile JSONs live under `Docker/profiles/` and are baked into the image. - **Compose services**: one per profile (`standard` / `dl205` / `mitsubishi` / `s7_1500`); only one binds `:5020` at a time. - **Tests skip** via `Assert.Skip(sim.SkipReason)` when the probe fails; no custom FactAttribute needed because `ModbusSimulatorCollection` carries the skip reason. ## What it actually covers ### DL205 (Automation Direct) - `DL205SmokeTests` — FC16 write → FC03 read round-trip on holding register - `DL205CoilMappingTests` — Y-output / C-relay / X-input address mapping (octal → Modbus offset) - `DL205ExceptionCodeTests` — Modbus exception 0x02 → OPC UA `BadOutOfRange` against the dl205 profile (natural out-of-range path) - `ExceptionInjectionTests` — every other exception code in the mapping table (0x01 / 0x03 / 0x04 / 0x05 / 0x06 / 0x0A / 0x0B) against the `exception_injection` profile on both read + write paths - `DL205FloatCdabQuirkTests` — CDAB word-swap float encoding - `DL205StringQuirkTests` — packed-string V-memory layout - `DL205VMemoryQuirkTests` — V-memory octal addressing - `DL205XInputTests` — X-register read-only enforcement ### Mitsubishi MELSEC - `MitsubishiSmokeTests` — read + write round-trip - `MitsubishiQuirkTests` — word-order, device-code mapping (D/M/X/Y ranges) ### Siemens S7-1500 (Modbus gateway flavor) - `S7_1500SmokeTests` — read + write round-trip - `S7_ByteOrderTests` — ABCD/DCBA/BADC/CDAB byte-order matrix ### Capability surfaces hit - `IReadable` + `IWritable` — full round-trip - `ISubscribable` — via the shared `PollGroupEngine` (polled subscription) - `IHostConnectivityProbe` — TCP-reach transitions ## What it does NOT cover ### 1. No `ITagDiscovery` Modbus has no symbol table — the driver requires a static tag map from `DriverConfig`. There is no discovery path to test + none in the fixture. ### 2. Error-path fuzzing `pymodbus` serves the seeded values happily; the fixture can't easily inject exception responses (code 0x01–0x0B) or malformed PDUs. The `AbCipStatusMapper`-equivalent for exception codes is unit-tested via `DL205ExceptionCodeTests` but the simulator itself never refuses a read. ### 3. Variant-specific quirks beyond the three profiles - FX5U / QJ71MT91 Mitsubishi variants — profile scaffolds exist, no tests yet - Non-S7-1500 Siemens (S7-1200 / ET200SP) — byte-order covered but connection-pool + fragmentation quirks untested - DL205-family cousins (DL06, DL260) — no dedicated profile ### 4. Subscription stress `PollGroupEngine` is unit-tested standalone but the simulator doesn't exercise it under multi-register packing stress (FC03 with 125-register batches, boundary splits, etc.). ### 5. Alarms / history Not a Modbus concept. Driver doesn't implement `IAlarmSource` or `IHistoryProvider`; no test coverage is the correct shape. ## When to trust the Modbus fixture, when to reach for a rig | Question | Fixture | Unit tests | Real PLC | | --- | --- | --- | --- | | "Does FC03/FC06/FC16 work end-to-end?" | yes | - | yes | | "Does DL205 octal addressing map correctly?" | yes | yes | yes | | "Does float CDAB word-swap round-trip?" | yes | yes | yes | | "Does the driver handle exception responses?" | no | yes | yes (required) | | "Does packing 125 regs into one FC03 work?" | no | no | yes (required) | | "Does FX5U behave like Q-series?" | no | no | yes (required) | ## Follow-up candidates 1. Add `MODBUS_SIM_ENDPOINT` override documentation to `docs/v2/test-data-sources.md` so operators can point the suite at a lab rig. 2. ~~Extend `pymodbus` profiles to inject exception responses~~ — **shipped** via the `exception_injection` compose profile + standalone `exception_injector.py` server. Rules in `Docker/profiles/exception_injection.json` map `(fc, address)` to an exception code; `ExceptionInjectionTests` exercises every code in `MapModbusExceptionToStatus` (0x01 / 0x02 / 0x03 / 0x04 / 0x05 / 0x06 / 0x0A / 0x0B) end-to-end on both read (FC03) and write (FC06) paths. 3. Add an FX5U profile once a lab rig is available; the scaffolding is in place. ## Key fixture / config files - `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs` - `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs` - `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs` - `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs` - `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/` — Dockerfile + compose + per-family JSON profiles