Files
lmxopcua/docs/drivers/S7-Test-Fixture.md
T
Joseph Doherty 2eb3ceb961 docs(audit): S7-Test-Fixture.md — accuracy pass
STALE-STATUS (Snap7ServerFixture.cs:40):
- TL;DR + "What the fixture is": localhost:1102 → 10.100.0.35:1102 (shared
  Docker host migrated 2026-04-28; fixture already defaults to 10.100.0.35)

CODE-REALITY (S7_1500SmokeTests.cs exists and sends real S7comm):
- "What it does NOT cover" §1 ("No ISO-on-TCP frame is ever sent") was
  simply wrong — the integration suite DOES send real S7comm. Rewritten
  to clarify that the unit suite uses IS7Client fakes while the integration
  suite exercises the full wire path.
- "What it does NOT cover" §2 ("successful read not tested end-to-end")
  was also wrong — Driver_reads_seeded_u16_through_real_S7comm does exactly
  that. Rewritten to scope the error-branch-only claim to unit tests.
- "When to trust" table: added Integration (python-snap7) column reflecting
  what the existing S7_1500SmokeTests actually answer.
- "Follow-up candidates" §1: removed the suggestion to build a Snap7 server
  fixture — python-snap7 fixture (task #216) already ships. Follow-ups now
  correctly list Plcsim Advanced and real lab rig only.

INLINE COMPLETENESS:
- "Key fixture / config files": was missing all integration test artefacts.
  Added Snap7ServerFixture.cs, S7_1500SmokeTests.cs, Docker/docker-compose.yml,
  and Docker/profiles/s7_1500.json with descriptions matching file contents.

STRUCTURAL: no broken links in links-report.md for this doc.
VERIFY: check_links.py — 0 rows for S7-Test-Fixture.md.
2026-06-03 16:01:58 -04:00

6.4 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 10.100.0.35:1102 (the shared Docker host; override via S7_SIM_ENDPOINT). 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/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ stands up a python-snap7 Server via Docker/docker-compose.yml --profile s7_1500 on 10.100.0.35:1102 (the shared Docker host; override via S7_SIM_ENDPOINT; 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/Drivers/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 (unit tests only)

The unit suite (S7DriverReadWriteTests, etc.) sends no real ISO-on-TCP frames. S7netplus has no in-process fake mode; units contract-test via the IS7Client abstraction. The integration suite (S7_1500SmokeTests, task #216) does send real S7comm over ISO-on-TCP against the python-snap7 container and covers the basic read / write / typed-batch path.

2. Error-branch unit tests vs. real round-trips

S7DriverReadWriteTests (unit) exercises error paths only; return values come from the fake. The integration suite exercises the successful read / write round-trip, but only against the python-snap7 emulator — not a real Siemens CPU.

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 Integration (python-snap7) Real PLC
"Does the address parser accept X syntax?" yes - -
"Does the driver lifecycle hang / crash?" yes yes yes
"Does a real read against an S7-1500 return correct bytes?" no yes (basic scalars) yes (required for full type matrix)
"Does mailbox serialization actually prevent PG timeouts?" no no yes (required)
"Does a UDT fan-out produce usable member variables?" no no yes (required)

Follow-up candidates

The python-snap7 fixture (task #216) covers scalar read / write / typed-batch. Remaining gaps need one of:

  1. Plcsim Advanced — Siemens' paid emulator; gives Optimized-DB symbolic access + PG/OP/S7-Basic session differentiation without real hardware. Licensed per-seat; fits a lab rig but not CI.
  2. Real S7 lab rig — cheapest physical PLC (CPU 1212C) on a dedicated network port, wired via self-hosted runner. Only path for mailbox serialization / PUT-GET enforcement verification.

Without either, S7 driver correctness for variant-quirk edge cases is trusted from field deployments, not from the test suite.

Key fixture / config files

  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ — unit tests only, no harness
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Snap7ServerFixture.cs — collection fixture; parses S7_SIM_ENDPOINT (default 10.100.0.35:1102), TCP-probes at collection init, records SkipReason when unreachable
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/S7_1500/S7_1500SmokeTests.cs — wire-level test suite (3 [Fact] methods: u16 read, typed batch, write-then-read)
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/docker-compose.yml — one service per profile (s7_1500); binds 1102:1102 on the Docker host
  • tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/profiles/s7_1500.json — DB1 + MB seed layout with typed seeds at known offsets
  • src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs — ctor takes IS7ClientFactory which tests fake