Modbus — deletes tests/.../Modbus.IntegrationTests/Pymodbus/ (serve.ps1, standard.json, dl205.json, mitsubishi.json, s7_1500.json, README.md). Profile JSONs live only under Docker/profiles/ now. Docker/README.md loses its "Native-Python fallback" section; docs/drivers/Modbus-Test-Fixture.md "What the fixture is" bullet flipped from "primary launcher is Docker, native fallback under Pymodbus/" to "Docker is the only supported launch path". S7 — deletes tests/.../S7.IntegrationTests/PythonSnap7/ (server.py, s7_1500.json, serve.ps1, README.md). Docker/README.md loses "Native-Python fallback"; docs/drivers/S7-Test-Fixture.md updated to match. AB CIP — the biggest simplification because the native-binary spawn had the most code. AbServerFixture.cs rewrites: drops Process management (no more Process _proc + Kill/WaitForExit), drops LocateBinary() PATH lookup, drops the IAsyncLifetime initialize-spawns-server behavior. Fixture is now a thin TCP probe against localhost:44818 (or AB_SERVER_ENDPOINT override) — same shape as Snap7ServerFixture / ModbusSimulatorFixture / OpcPlcFixture. IsServerAvailable() simplifies to a single 500 ms probe. AbServerProfile.cs drops AbServerPlcArg + SeedTags + BuildCliArgs + ToCliSpec + the entire AbServerSeedTag record — the compose file is the canonical source of truth for which tags + which --plc mode each family gets; the profile record now carries just Family + ComposeProfile (matches the docker-compose service key) + Notes. KnownProfiles.ForFamily + .All stay for tests that iterate families. AbServerProfileTests.cs rewrites to match: drops BuildCliArgs_* + ToCliSpec_* + SeedTags_* tests; keeps the family-coverage contract tests + verifies the ComposeProfile strings match compose-file service names (a typo in either surfaces as a unit-test failure, not a silent "wrong family booted" at runtime). Docker/README.md loses "Native-binary fallback" section; docs/drivers/AbServer-Test-Fixture.md "What the fixture is" flipped to Docker-only with clearer skip rules. dev-environment.md §Docker fixtures — the "Native fallbacks" subsection goes away; replaced with a one-line note that Docker is the only supported path for these four fixtures + a fresh clone needs Docker Desktop and nothing else. Verified: whole-solution build 0 errors, AB CIP profile unit tests 6/6, AB CIP Docker smoke 4/4 (all family theory rows), S7 Docker smoke 3/3. Container lifecycle clean. The deleted native code surface was already redundant — every fixture the native paths served is now covered by Docker; keeping them invited drift between the two paths (the original AB CIP native profile had three undetected bugs per the #162 commit message: case-sensitive --plc, bracket tag notation, --path=1,0 requirement — noise the Docker path now avoids by never running the buggy code). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.2 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 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/asciifor 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 pathS7_1500SmokeTests.Driver_reads_seeded_typed_batch— i16, i32, f32, bool-with-bit in one batch call; proves typed decode per S7DataTypeS7_1500SmokeTests.Driver_write_then_read_round_trip_on_scratch_word—DB1.DBW100write → read-back; proves write path + buffer visibility
Unit
S7AddressParserTests— S7 address syntax (DB1.DBD0,M10.3,IW4, etc.)S7DriverScaffoldTests—IDriverlifecycle (init / reinit / shutdown / health)S7DriverReadWriteTests— error paths (uninitialized read/write, bad addresses, transport exceptions)S7DiscoveryAndSubscribeTests—ITagDiscovery.DiscoverAsync+ polledISubscribablecontract with the sharedPollGroupEngine
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
- Snap7 server — Snap7 ships a
C-library-based S7 server that could run in-CI on Linux. A pinned build +
a fixture shape similar to
ab_serverwould give S7 parity with Modbus / AB CIP coverage. - Plcsim Advanced — Siemens' paid emulator. Licensed per-seat; fits a lab rig but not CI.
- 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 harnesssrc/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs— ctor takesIS7ClientFactorywhich tests fake; docstring lines 8-20 note the deferred integration fixture