Files
lmxopcua/docs/drivers/Modbus-Test-Fixture.md
Joseph Doherty 1d3544f18e S7 integration fixture — python-snap7 server closes the wire-level coverage gap (#216) + per-driver fixture coverage docs for every driver in the fleet. Closes #216. Two shipments in one PR because the docs landed as I surveyed each driver's fixture + the S7 work is the first wire-level-gap closer pulled from that survey.
S7 integration — AbCip/Modbus already have real-simulator integration suites; S7 had zero wire-level coverage despite being a Tier-A driver (all unit tests mocked IS7Client). Picked python-snap7's `snap7.server.Server` over raw Snap7 C library because `pip install` beats per-OS binary-pin maintenance, the package ships a Python __main__ shim that mirrors our existing pymodbus serve.ps1 + *.json pattern structurally, and the python-snap7 project is actively maintained. New project `tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/` with four moving parts: (a) `Snap7ServerFixture` — collection-scoped TCP probe on `localhost:1102` that sets `SkipReason` when the simulator's not running, matching the `ModbusSimulatorFixture` shape one directory over (same S7_SIM_ENDPOINT env var override convention for pointing at a real S7 CPU on port 102); (b) `PythonSnap7/` — `serve.ps1` wrapper + `server.py` shim + `s7_1500.json` seed profile + `README.md` documenting install / run / known limitations; (c) `S7_1500/S7_1500Profile.cs` — driver-side `S7DriverOptions` whose tag addresses map 1:1 to the JSON profile's seed offsets (DB1.DBW0 u16, DB1.DBW10 i16, DB1.DBD20 i32, DB1.DBD30 f32, DB1.DBX50.3 bool, DB1.DBW100 scratch); (d) `S7_1500SmokeTests` — three tests proving typed reads + write-then-read round-trip work through real S7netplus + real ISO-on-TCP + real snap7 server. Picked port 1102 default instead of S7-standard 102 because 102 is privileged on Linux + triggers Windows Firewall prompt; S7netplus 0.20 has a 5-arg `Plc(CpuType, host, port, rack, slot)` ctor that lets the driver honour `S7DriverOptions.Port`, but the existing driver code called the 4-arg overload + silently hardcoded 102. One-line driver fix (S7Driver.cs:87) threads `_options.Port` through — the S7 unit suite (58/58) still passes unchanged because every unit test uses a fake IS7Client that never sees the real ctor. Server seed-type matrix in `server.py` covers u8 / i8 / u16 / i16 / u32 / i32 / f32 / bool-with-bit / ascii (S7 STRING with max_len header). register_area takes the SrvArea enum value, not the string name — a 15-minute debug after the first test run caught that; documented inline.

Per-driver test-fixture coverage docs — eight new files in `docs/drivers/` laying out what each driver's harness actually benchmarks vs. what's trusted from field deployments. Pattern mirrors the AbServer-Test-Fixture.md doc that shipped earlier in this arc: TL;DR → What the fixture is → What it actually covers → What it does NOT cover → When-to-trust table → Follow-up candidates → Key files. Ugly truth the survey made visible: Galaxy + Modbus + (now) S7 + AB CIP have real wire-level coverage; AB Legacy / TwinCAT / FOCAS / OpcUaClient are still contract-only because their libraries ship no fake + no open-source simulator exists (AB Legacy PCCC), no public simulator exists (FOCAS), the vendor SDK has no in-process fake (TwinCAT/ADS.NET), or the test wiring just hasn't happened yet (OpcUaClient could trivially loopback against this repo's own server — flagged as #215). Each doc names the specific follow-up route: Snap7 server for S7 (done), TwinCAT 3 developer-runtime auto-restart for TwinCAT, Tier-C out-of-process Host for FOCAS, lab rigs for AB Legacy + hardware-gated bits of the others. `docs/drivers/README.md` gains a coverage-map section linking all eight. Tracking tasks #215-#222 filed for each PR-able follow-up.

Build clean (driver + integration project + docs); S7.Tests 58/58 (unchanged); S7.IntegrationTests 3/3 (new, verified end-to-end against a live python-snap7 server: `driver_reads_seeded_u16_through_real_S7comm`, `driver_reads_seeded_typed_batch`, `driver_write_then_read_round_trip_on_scratch_word`). Next fixture follow-up is #215 (OpcUaClient loopback against own server) — highest ROI of the remaining set, zero external deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:29:15 -04:00

4.8 KiB
Raw Blame History

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) driven from PowerShell + per-family JSON profiles under tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/.
  • 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.
  • 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 → OPC UA StatusCode mapping
  • 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 — a JSON flag per register saying "next read returns exception 0x04."
  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/Pymodbus/ — simulator driver script + per-family JSON profiles