Files
lmxopcua/docs/drivers/S7-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

5.1 KiB

Siemens S7 test fixture

Coverage map + gap inventory for the S7 driver.

TL;DR: S7 now has a wire-level integration fixture backed by python-snap7's Server class (task #216). Atomic reads (u16 / i16 / i32 / f32 / bool-with-bit) + DB write-then-read round-trip are exercised end-to-end through S7netplus + real ISO-on-TCP on localhost:1102. Unit tests still carry everything else (address parsing, error-branch handling, probe-loop contract). Gaps remaining are variant-quirk-shaped: Optimized-DB symbolic access, PG/OP session types, PUT/GET-disabled enforcement — all need real hardware.

What the fixture is

Integration layer (task #216): tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ stands up a python-snap7 Server via PythonSnap7/serve.ps1 -Profile s7_1500 on localhost:1102. Snap7ServerFixture probes the port at collection init

  • skips with a clear message when unreachable (matches the pymodbus pattern). server.py reads a JSON profile + seeds DB/MB bytes at declared offsets; seeds are typed (u16 / i16 / i32 / f32 / bool / ascii for S7 STRING).

Unit layer: tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ covers everything the wire-level suite doesn't — address parsing, error branches, probe-loop contract. All tests tagged [Trait("Category", "Unit")].

The driver ctor change that made this possible: Plc(CpuType, host, port, rack, slot) — S7netplus 0.20's 5-arg overload — wires S7DriverOptions.Port through so the simulator can bind 1102 (non-privileged) instead of 102 (root / Firewall-prompt territory).

What it actually covers

Integration (python-snap7, task #216)

  • S7_1500SmokeTests.Driver_reads_seeded_u16_through_real_S7comm — DB1.DBW0 read via real S7netplus over TCP + simulator; proves handshake + read path
  • S7_1500SmokeTests.Driver_reads_seeded_typed_batch — i16, i32, f32, bool-with-bit in one batch call; proves typed decode per S7DataType
  • S7_1500SmokeTests.Driver_write_then_read_round_trip_on_scratch_wordDB1.DBW100 write → read-back; proves write path + buffer visibility

Unit

  • S7AddressParserTests — S7 address syntax (DB1.DBD0, M10.3, IW4, etc.)
  • S7DriverScaffoldTestsIDriver lifecycle (init / reinit / shutdown / health)
  • S7DriverReadWriteTests — error paths (uninitialized read/write, bad addresses, transport exceptions)
  • S7DiscoveryAndSubscribeTestsITagDiscovery.DiscoverAsync + polled ISubscribable contract with the shared PollGroupEngine

Capability surfaces whose contract is verified: IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe. Wire-level surfaces verified: IReadable, IWritable.

What it does NOT cover

1. Wire-level anything

No ISO-on-TCP frame is ever sent during the test suite. S7netplus is the only wire-path abstraction and it has no in-process fake mode; the shipping choice was to contract-test via IS7Client rather than patch into S7netplus internals.

2. Read/write happy path

Every S7DriverReadWriteTests case exercises error branches. A successful read returning real PLC data is not tested end-to-end — the return value is whatever the fake says it is.

3. Mailbox serialization under concurrent reads

The driver's SemaphoreSlim serializes S7netplus calls because the S7 CPU's comm mailbox is scanned at most once per cycle. Contention behavior under real PLC latency is not exercised.

4. Variant quirks

S7-1200 vs S7-1500 vs S7-300/400 connection semantics (PG vs OP vs S7-Basic) not differentiated at test time.

5. Data types beyond the scalars

UDT fan-out, STRING with length-prefix quirks, DTL / DATE_AND_TIME, arrays of structs — not covered.

When to trust the S7 tests, when to reach for a rig

Question Unit tests Real PLC
"Does the address parser accept X syntax?" yes -
"Does the driver lifecycle hang / crash?" yes yes
"Does a real read against an S7-1500 return correct bytes?" no yes (required)
"Does mailbox serialization actually prevent PG timeouts?" no yes (required)
"Does a UDT fan-out produce usable member variables?" no yes (required)

Follow-up candidates

  1. Snap7 serverSnap7 ships a C-library-based S7 server that could run in-CI on Linux. A pinned build + a fixture shape similar to ab_server would give S7 parity with Modbus / AB CIP coverage.
  2. Plcsim Advanced — Siemens' paid emulator. Licensed per-seat; fits a lab rig but not CI.
  3. Real S7 lab rig — cheapest physical PLC (CPU 1212C) on a dedicated network port, wired via self-hosted runner.

Without any of these, S7 driver correctness against real hardware is trusted from field deployments, not from the test suite.

Key fixture / config files

  • tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ — unit tests only, no harness
  • src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs — ctor takes IS7ClientFactory which tests fake; docstring lines 8-20 note the deferred integration fixture