# 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](https://github.com/gijzelaerr/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 `Docker/docker-compose.yml --profile s7_1500` on `localhost:1102` (pinned `python:3.12-slim-bookworm` base + `python-snap7>=2.0`). Docker is the only supported launch path. `Snap7ServerFixture` probes the port at collection init + skips with a clear message when unreachable (matches the pymodbus pattern). `server.py` (baked into the image under `Docker/`) 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_word` — `DB1.DBW100` write → read-back; proves write path + buffer visibility - `S7_1500DiagnosticsTests.Driver_exposes_negotiated_pdu_size_post_init` — asserts `DriverHealth.Diagnostics["S7.NegotiatedPduSize"]` is non-zero after `InitializeAsync`; proves the negotiated PDU size surfaces in driver health (Snap7 fixture pins this at 240 bytes — see fixture README) ### Unit - `S7AddressParserTests` — S7 address syntax (`DB1.DBD0`, `M10.3`, `IW4`, etc.) - `S7DriverScaffoldTests` — `IDriver` lifecycle (init / reinit / shutdown / health) - `S7DriverReadWriteTests` — error paths (uninitialized read/write, bad addresses, transport exceptions) - `S7DiscoveryAndSubscribeTests` — `ITagDiscovery.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. **Optimized DB / S7Plus** is the variant-shaped gap with the biggest field impact. snap7 happens to behave like a classic-S7comm-only PLC, so the integration suite cannot reproduce the shape that an S7-1500 with default "Optimized block access" checked would return (`BadDeviceFailure` on every absolute-offset read). The decision is documented at [`docs/v2/s7.md` § Optimized DB constraint (S7Plus)](../v2/s7.md#optimized-db-constraint-s7plus) and tracked in [`docs/featuregaps.md`](../featuregaps.md) row #1; the project ships **Track 1** (operator unchecks Optimized block access in TIA Portal) and **Track 3** (bridge via the `OpcUaClient` driver against the CPU's onboard OPC UA server). A custom S7Plus implementation is out of scope. ### 5. Data types beyond the scalars `STRING` with length-prefix quirks, `DTL` / `DATE_AND_TIME`, arrays of structs — not covered. UDT fan-out IS covered (PR-S7-D2 / #300) via the `udt_layout` meta-seed in `Docker/profiles/s7_1500.json` and the `Driver_fans_out_udt_into_member_tags` integration test. ### 6. SZL (System Status List) — `@System.*` virtual addresses PR-S7-E1 / [#302](https://github.com/dohertj2/dohertj2/lmxopcua/issues/302) adds a virtual `@System.*` address surface (CPU type, firmware, scan-cycle stats, diagnostic-buffer ring) backed by SZL reads. **snap7 does not implement SZL** — the simulator answers every SZL request with a function- not-supported error, so the integration profile exercises only the not-supported semantics (`@System.CpuType` against snap7 returns `BadNotSupported`). Live-firmware SZL coverage is parked behind a `[Fact(Skip = ...)]` until either S7netplus exposes a public `ReadSzlAsync` or we ship a raw S7comm PDU helper. See [`docs/v2/s7.md` "CPU diagnostics (SZL)"](../v2/s7.md#cpu-diagnostics-szl) for the wire-status detail. ### 7. Password / protection levels — not modelled by snap7 PR-S7-E2 / [#303](https://github.com/dohertj2/lmxopcua/issues/303) adds `Password` + `ProtectionLevel` options that emit a connection-level password right after `OpenAsync`. **snap7 does not model S7 protection levels** — the simulator accepts every connection regardless of the password set on the client, so the integration profile cannot distinguish "password sent correctly" from "password ignored". Coverage stays at the unit-test seam: `S7PasswordOptionsTests` injects a fake `IS7PlcAuthGate` to assert the dispatch contract (Password=null skips the call; Password+SupportsSendPassword calls the gate; auth-failed wraps to a clean `InvalidOperationException`), plus the no-log invariant on `S7DriverOptions.ToString()`. The wire path is also fundamentally limited until S7netplus 0.20 exposes a public `SendPassword` — the driver currently logs a warning and continues when the API is missing. See [`docs/v2/s7.md` "PLC password / protection levels"](../v2/s7.md#plc-password--protection-levels) for the library-limitation note. Live-firmware coverage of the unlock path requires a hardened S7-1500 lab rig with TIA Portal "Protection & Security" configured, which is parked as a follow-up. ## 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?" | yes (Snap7 + `udt_layout` meta-seed) | yes | ## Follow-up candidates 1. **Snap7 server** — [Snap7](https://snap7.sourceforge.net/) 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. 4. **PR-S7-C5 — PUT/GET-disabled pre-flight rejection.** Snap7 does *not* model the hardened-CPU PUT/GET response (it accepts every read once the COTP handshake completes), so the **failure** path of the pre-flight probe — `S7PutGetDisabledException` thrown from `InitializeAsync` when the PLC rejects the probe read with `ErrorCode.WrongCPU_Type` / `ErrorCode.ReadData` — needs a real S7-1500 with PUT/GET disabled in TIA Portal. The integration suite covers the *happy* path (`Driver_preflight_passes_when_probe_address_seeded`); the failure path should be added as a `--with-real-plc` opt-in test that the self-hosted runner with the lab rig executes. The classifier branch (`S7PreflightClassifier.IsPutGetDisabled`) is unit-tested without a network in `S7PreflightTests.Classifier_matches_only_PUT_GET_disabled_error_codes`. 5. **Live-firmware Optimized-block-access toggle (PR-S7-F / [#304](https://github.com/dohertj2/lmxopcua/issues/304)).** snap7 happens to behave like a classic-S7comm CPU, so the integration profile cannot reproduce the failure that a default new TIA Portal V14+ project produces (`BadDeviceFailure` on `DB1.DBW0` against an Optimized DB). A manual smoke test on the lab rig, gated behind `--with-real-plc`, would close that loop. Suggested checklist on a real S7-1500 V2.5+: 1. Create `DB1` in TIA Portal with three INT members at offsets 0, 2, 4. Leave **Optimized block access checked** (the default). 2. Compile + download to the PLC. 3. Drive the OtOpcUa S7 driver against `DB1.DBW0` — assert that the read returns `BadDeviceFailure` (the Track-1-not-applied symptom). This is the failure shape the docs warn about. 4. Open `DB1`'s properties → **uncheck Optimized block access** → compile → download. Re-run the read; assert it returns the seeded INT value at offset 0. (Track 1 verified end-to-end.) 5. **Track 3 verification (separate run on the same rig):** with Optimized access re-enabled on `DB1`, activate the CPU's onboard OPC UA server in TIA Portal, expose `DB1.` through a Server interface, register an `OpcUaClient` driver against `opc.tcp://:4840`, and assert the symbolic read returns the same seeded value. This proves the bridge path against a real Optimized DB without the operator having to disable Optimized access. The test must stay manual: TIA Portal compile + download cannot be automated from CI without a Siemens engineering toolchain license, and download-with-CPU-stop is destructive on a shared lab rig. Document results inline in PR descriptions when the rig is available. 6. **PR-S7-E1 — live SZL test against a real S7-1500.** snap7 doesn't implement SZL at all, and S7netplus 0.20 doesn't expose a public `ReadSzlAsync`, so the `@System.*` virtual address surface currently answers `BadNotSupported` against every backend. The parser (`S7SzlParser`) is unit-tested against golden bytes; flipping the wire path on requires either an S7netplus PR or a raw-PDU helper. Once that's in, [`S7_1500SzlTests.System_CpuType_against_live_S7_1500_returns_non_empty_string`](../../tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SzlTests.cs) should be flipped from `[Fact(Skip = ...)]` to env-var-gated against the self-hosted runner with the lab rig. 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