# Modbus integration-test fixture — pymodbus simulator The Modbus driver's integration tests talk to a [`pymodbus`](https://pymodbus.readthedocs.io/) simulator running as a pinned Docker container. One image, per-profile service in compose, same port binding (`5020`) regardless of which profile is live. Docker is the only supported launch path — a fresh clone needs Docker Desktop and nothing else. | File | Purpose | |---|---| | [`Dockerfile`](Dockerfile) | `python:3.12-slim-bookworm` + `pymodbus[simulator]==3.13.0` + every profile JSON + `exception_injector.py` | | [`docker-compose.yml`](docker-compose.yml) | One service per profile (`standard` / `dl205` / `mitsubishi` / `s7_1500` / `exception_injection`); all bind `:5020` so only one runs at a time | | [`profiles/*.json`](profiles/) | Same seed-register definitions the native launcher uses — canonical source | | [`exception_injector.py`](exception_injector.py) | Pure-stdlib Modbus/TCP server that emits arbitrary exception codes per rule — used by the `exception_injection` profile | ## Run From the repo root: ```powershell # Build + start the standard profile docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile standard up # DL205 quirks docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile dl205 up # Mitsubishi MELSEC quirks docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile mitsubishi up # Siemens S7-1500 MB_SERVER quirks docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile s7_1500 up # Exception-injection — end-to-end coverage of every Modbus exception code # (01/02/03/04/05/06/0A/0B), not just the 02 + 03 pymodbus emits naturally docker compose -f tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests\Docker\docker-compose.yml --profile exception_injection up ``` Detached + stop: ```powershell docker compose -f tests\...\Docker\docker-compose.yml --profile dl205 up -d docker compose -f tests\...\Docker\docker-compose.yml --profile dl205 down ``` Only one profile binds `:5020` at a time; switch by stopping the current service + starting another. The integration tests discriminate by a separate `MODBUS_SIM_PROFILE` env var so they skip correctly when the wrong profile is live. ## Endpoint - Default: `localhost:5020` - Override with `MODBUS_SIM_ENDPOINT` (e.g. a real PLC on `:502`). ## Run the integration tests In a separate shell with one profile live: ```powershell cd C:\Users\dohertj2\Desktop\lmxopcua dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests ``` `ModbusSimulatorFixture` probes `localhost:5020` at collection init + records a `SkipReason` when unreachable, so tests stay green on a fresh clone without Docker running. ## Exception injection pymodbus's simulator naturally emits only Modbus exception codes `0x02` (Illegal Data Address, on reads outside its configured ranges) and `0x03` (Illegal Data Value, on over-length requests). The driver's `MapModbusExceptionToStatus` table translates eight codes: `0x01`, `0x02`, `0x03`, `0x04`, `0x05`, `0x06`, `0x0A`, `0x0B`. Unit tests lock the translation function; the integration side previously only proved the wire-to-status path for `0x02`. The `exception_injection` profile runs [`exception_injector.py`](exception_injector.py) — a tiny standalone Modbus/TCP server written against the Python stdlib (zero dependencies outside what's in the base image). It speaks the wire protocol directly (FC 01/02/03/04/05/06/15/16) and looks up each incoming `(fc, address)` against the rules in [`profiles/exception_injection.json`](profiles/exception_injection.json); a matching rule makes the server reply with `[fc | 0x80, exception_code]` instead of the normal response. Current rules (see the JSON file for the canonical list): - `FC03 @1000..1007` — one per exception code (`0x01`/`0x02`/`0x03`/`0x04`/`0x05`/`0x06`/`0x0A`/`0x0B`) - `FC06 @2000..2001` — `0x04` Server Failure, `0x06` Server Busy (write-path coverage) - `FC16 @3000` — `0x04` Server Failure (multi-register write path) Adding more coverage is append-only: drop a new `{fc, address, exception, description}` entry into the JSON, restart the service, add an `[InlineData]` row in `ExceptionInjectionTests`. ## References - [`docs/drivers/Modbus-Test-Fixture.md`](../../../docs/drivers/Modbus-Test-Fixture.md) — coverage map + gap inventory - [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md) §Docker fixtures — full fixture inventory