Files
lmxopcua/docs/drivers/Modbus-Test-Fixture.md
Joseph Doherty 969b0847a1 docs: update path references for module-folder reorganization
Rewrite src/ and tests/ project paths in docs, CLAUDE.md, README.md, and
test-fixture READMEs to the new module-folder layout (Core/Server/Drivers/
Client/Tooling). References to retired v1 projects (Galaxy.Host/Proxy/Shared,
the legacy monolithic test projects) are left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:10:29 -04:00

124 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/Drivers/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 0x010x0B) 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/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs`
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs`
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Mitsubishi/MitsubishiProfile.cs`
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/S7/S7_1500Profile.cs`
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Docker/`
Dockerfile + compose + per-family JSON profiles