ModbusSimulatorFixture is a collection fixture so the 2s TCP probe runs once per run, not per test; SkipReason gets a clear operator-facing message ('start ModbusPal or override MODBUS_SIM_ENDPOINT'). Tests call Assert.Skip(sim.SkipReason) rather than silently returning — matches the test-plan convention and reads cleanly in CI logs. DL205Profile.BuildOptions deliberately disables the background probe loop since integration tests drive reads explicitly and the probe would race with assertions. Tag naming uses the DL205_ prefix so filter 'DisplayName~DL205' surfaces device-specific failures at a glance.
Project references: xunit.v3 + Shouldly + Microsoft.NET.Test.Sdk + xunit.runner.visualstudio (matches the existing Driver.Modbus.Tests unit project), project ref to src/Driver.Modbus. Registered in ZB.MOM.WW.OtOpcUa.slnx under tests/. ModbusPal/README.md documents the dev loop (install ModbusPal jar, load profile, start simulator, dotnet test), explains MODBUS_SIM_ENDPOINT override for real-PLC benchwork, and flags DL205.xmpp as the first profile to add in a follow-up PR.
dotnet test run against the scaffold (no simulator running) skips cleanly: 0 failed, 0 passed, 1 skipped, with the SkipReason surfaced. dotnet build clean (0 warnings, 0 errors). Updated docs/v2/modbus-test-plan.md to mark the scaffold PR done and renumbered future PRs from 'PR 27+' to 'PR 31+' to stay in sync with the actual PR chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
5.9 KiB
Markdown
109 lines
5.9 KiB
Markdown
# Modbus driver — test plan + device-quirk catalog
|
||
|
||
The Modbus TCP driver unit tests (PRs 21–24) cover the protocol surface against an
|
||
in-memory fake transport. They validate the codec, state machine, and function-code
|
||
routing against a textbook Modbus server. That's necessary but not sufficient: real PLC
|
||
populations disagree with the spec in small, device-specific ways, and a driver that
|
||
passes textbook tests can still misbehave against actual equipment.
|
||
|
||
This doc is the harness-and-quirks playbook. The project it describes lives at
|
||
`tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/` — scaffolded in PR 30 with
|
||
the simulator fixture, DL205 profile stub, and one write/read smoke test. Each
|
||
confirmed DL205 quirk lands in a follow-up PR as a named test in that project.
|
||
|
||
## Harness
|
||
|
||
**Chosen simulator: ModbusPal** (Java, scriptable). Rationale:
|
||
- Scriptable enough to mimic device-specific behaviors (non-standard register
|
||
layouts, custom exception codes, intentional response delays).
|
||
- Runs locally, no CI dependency. Tests skip when `localhost:502` (or the configured
|
||
simulator endpoint) isn't reachable.
|
||
- Free + long-maintained — physical PLC bench is unavailable in most dev
|
||
environments, and renting cloud PLCs isn't worth the per-test cost.
|
||
|
||
**Setup pattern** (not yet codified in a script — will land alongside the integration
|
||
test project):
|
||
1. Install ModbusPal, load the per-device `.xmpp` profile from
|
||
`tests/Driver.Modbus.IntegrationTests/ModbusPal/` (TBD directory).
|
||
2. Start the simulator listening on `localhost:502` (or override via
|
||
`MODBUS_SIM_ENDPOINT` env var).
|
||
3. `dotnet test` the integration project — tests auto-skip when the endpoint is
|
||
unreachable, so forgetting to start the simulator doesn't wedge CI.
|
||
|
||
## Per-device quirk catalog
|
||
|
||
### AutomationDirect DL205
|
||
|
||
First known target device. Quirks to document and cover with named tests (to be
|
||
filled in when user validates each behavior in ModbusPal with a DL205 profile):
|
||
|
||
- **Word order for 32-bit values**: _pending_ — confirm whether DL205 uses ABCD
|
||
(Modbus TCP standard) or CDAB (Siemens-style word-swap) for Int32/UInt32/Float32.
|
||
Test name: `DL205_Float32_word_order_is_CDAB` (or `ABCD`, whichever proves out).
|
||
- **Register-zero access**: _pending_ — some DL205 configurations reject FC03 at
|
||
register 0 with exception code 02 (illegal data address). If confirmed, the
|
||
integration test suite verifies `ModbusProbeOptions.ProbeAddress` default of 0
|
||
triggers the rejection and operators must override; test name:
|
||
`DL205_FC03_at_register_0_returns_IllegalDataAddress`.
|
||
- **Coil addressing base**: _pending_ — DL205 documentation sometimes uses 1-based
|
||
coil addresses; verify the driver's zero-based addressing matches the physical
|
||
PLC without an off-by-one adjustment.
|
||
- **Maximum registers per FC03**: _pending_ — Modbus spec caps at 125; some DL205
|
||
models enforce a lower limit (e.g., 64). Test name:
|
||
`DL205_FC03_beyond_max_registers_returns_IllegalDataValue`.
|
||
- **Response framing under sustained load**: _pending_ — the driver's
|
||
single-flight semaphore assumes the server pairs requests/responses by
|
||
transaction id; at least one DL205 firmware revision is reported to drop the
|
||
TxId under load. If reproduced in ModbusPal we add a retry + log-and-continue
|
||
path to `ModbusTcpTransport`.
|
||
- **Exception code on coil write to a protected bit**: _pending_ — some DL205
|
||
setups protect internal coils; the driver should surface the PLC's exception
|
||
PDU as `BadNotWritable` rather than `BadInternalError`.
|
||
|
||
_User action item_: as each quirk is validated in ModbusPal, replace the _pending_
|
||
marker with the confirmed behavior and file a named test in the integration suite.
|
||
|
||
### Future devices
|
||
|
||
One section per device class, same shape as DL205. Quirks that apply across
|
||
multiple devices (e.g., "all AB PLCs use CDAB") can be noted in the cross-device
|
||
patterns section below once we have enough data points.
|
||
|
||
## Cross-device patterns
|
||
|
||
Once multiple device catalogs accumulate, quirks that recur across two or more
|
||
vendors get promoted into driver defaults or opt-in options:
|
||
|
||
- _(empty — filled in as catalogs grow)_
|
||
|
||
## Test conventions
|
||
|
||
- **One named test per quirk.** `DL205_word_order_is_CDAB_for_Float32` is easier to
|
||
diagnose on failure than a generic `Float32_roundtrip`. The `DL205_` prefix makes
|
||
filtering by device class trivial (`--filter "DisplayName~DL205"`).
|
||
- **Skip with a clear SkipReason.** Follow the pattern from
|
||
`GalaxyRepositoryLiveSmokeTests`: check reachability in the fixture, capture
|
||
a `SkipReason` string, and have each test call `Assert.Skip(SkipReason)` when
|
||
it's set. Don't throw — skipped tests read cleanly in CI logs.
|
||
- **Use the real `ModbusTcpTransport`.** Integration tests exercise the wire
|
||
protocol end-to-end. The in-memory `FakeTransport` from the unit test suite is
|
||
deliberately not used here — its value is speed + determinism, which doesn't
|
||
help reproduce device-specific issues.
|
||
- **Don't depend on ModbusPal state between tests.** Each test resets the
|
||
simulator's register bank or uses a unique address range. Avoid relying on
|
||
"previous test left value at register 10" setups that flake when tests run in
|
||
parallel or re-order.
|
||
|
||
## Next concrete PRs
|
||
|
||
- **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**.
|
||
Shipped `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with
|
||
`ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the
|
||
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub — one
|
||
writable holding register at address 100), and `DL205/DL205SmokeTests.cs`
|
||
(write-then-read round-trip). `ModbusPal/` directory holds the README
|
||
pointing at the to-be-committed `DL205.xmpp` profile.
|
||
- **PR 31+**: one PR per confirmed DL205 quirk, landing the named test + any
|
||
driver-side adjustment (e.g., retry on dropped TxId) needed to pass it. Drop
|
||
the `DL205.xmpp` profile into `ModbusPal/` alongside the first quirk PR.
|