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>
5.9 KiB
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):
- Install ModbusPal, load the per-device
.xmppprofile fromtests/Driver.Modbus.IntegrationTests/ModbusPal/(TBD directory). - Start the simulator listening on
localhost:502(or override viaMODBUS_SIM_ENDPOINTenv var). dotnet testthe 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(orABCD, 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.ProbeAddressdefault 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
BadNotWritablerather thanBadInternalError.
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_Float32is easier to diagnose on failure than a genericFloat32_roundtrip. TheDL205_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 aSkipReasonstring, and have each test callAssert.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-memoryFakeTransportfrom 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.IntegrationTestswithModbusSimulatorFixture(TCP-probe, skips with a clearSkipReasonwhen the endpoint is unreachable),DL205/DL205Profile.cs(tag map stub — one writable holding register at address 100), andDL205/DL205SmokeTests.cs(write-then-read round-trip).ModbusPal/directory holds the README pointing at the to-be-committedDL205.xmppprofile. - 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.xmppprofile intoModbusPal/alongside the first quirk PR.