Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker

S7 integration-test fixture — python-snap7

python-snap7 Server class wrapped in a pinned python:3.12-slim-bookworm image. Docker is the only supported launch path — a fresh clone needs Docker Desktop and nothing else.

File Purpose
Dockerfile python:3.12-slim-bookworm + python-snap7>=2.0 + the server shim + the profile JSONs
docker-compose.yml One service per profile; currently only s7_1500
server.py Same Python shim the native fallback uses — copy kept in the build context
profiles/*.json Area-seed definitions (DB1 / MB layouts with typed seeds)

Run

From the repo root:

docker compose -f tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests\Docker\docker-compose.yml --profile s7_1500 up

Detached + stop:

docker compose -f tests\...\Docker\docker-compose.yml --profile s7_1500 up -d
docker compose -f tests\...\Docker\docker-compose.yml --profile s7_1500 down

⚠️ Editing a profile requires --build

Dockerfile COPYs profiles/ into the image, so the seeds are baked in at build time — they are not bind-mounted. A plain up -d, and even up -d --force-recreate, reuses the existing otopcua-python-snap7:1.0 image and silently keeps serving the OLD seeds. After editing a profile JSON:

docker compose --profile s7_1500 up -d --build --force-recreate

Confirm it took: docker logs otopcua-python-snap7-s7_1500 prints seeded DB1 size=1024 seeds=<n> at startup — check <n> matches the seed count you just wrote. This is the fastest way to tell a stale image from a bad address.

On the shared Docker host the deployment mirror at /opt/otopcua-s7/ is root-owned, so a plain rsync fails with status 23 and leaves the old files in place while still printing the file list. Push with:

rsync -av --rsync-path="sudo rsync" \
    tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/Docker/ \
    dohertj2@10.100.0.35:/opt/otopcua-s7/

Endpoint

  • Default: localhost:1102 (non-privileged; sidesteps Windows Firewall prompt + Linux's root-required bind on port 102).
  • Override with S7_SIM_ENDPOINT to point at a real S7 CPU on :102.
  • The driver's S7DriverOptions.Port flows through S7netplus's 5-arg Plc(CpuType, host, port, rack, slot) ctor so the non-standard port works end-to-end.

Run the integration tests

In a separate shell with the container up:

cd C:\Users\dohertj2\Desktop\lmxopcua
dotnet test tests\Drivers\ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests

Snap7ServerFixture probes localhost:1102 at collection init + records a SkipReason when unreachable, so tests stay green on a fresh clone without Docker running.

What's encoded in profiles/s7_1500.json

DB1 (1024 bytes) + MB (256 bytes) with typed seeds at known offsets:

Address Type Seed Purpose
DB1.DBW0 u16 4242 read-back probe
DB1.DBW10 i16 -12345 smoke i16 read
DB1.DBD20 i32 1234567890 smoke i32 read
DB1.DBD30 f32 3.14159 smoke f32 read (big-endian)
DB1.DBX50.3 bool true smoke bool read at bit 3
DB1.DBW100 u16 0 scratch for write-then-read
DB1.STRING[200] S7 STRING "Hello" S7 STRING read
MW0 u16 1 S7ProbeOptions.ProbeAddress default

Seed types supported: u8, i8, u16, i16, u32, i32, f32, bool (with "bit": 0..7), ascii (S7 STRING).

Span-planner gate seeds (offsets 300+, S7_1500SpanReadTests)

These exist to give S7ReadSpanPlanner a layout with a known partition under the shipped 16-byte-gap / 220-byte-span defaults — the offsets are the test, so moving one changes what the gate proves.

Address Type Seed Role in the partition
DB1.DBW300 u16 51001 cluster A — merges with the four below into ONE 300..320 span
DB1.DBW302 i16 -21001 cluster A (gap 0)
DB1.DBD304 i32 305419896 cluster A (gap 0)
DB1.DBX310.5 bool true cluster A (gap 2)
DB1.DBD316 f32 2.7182817 cluster A (gap 5); seals the span at 20 bytes
DB1.DBW400 u16 40201 cluster B — 80 bytes on, so the gap budget forces a SECOND span
DB1.DBW402 u16 40202 cluster B's second member (keeps it off the PrefersPerTagRead demotion)
DB1.DBW500 u16 60001 isolated ⇒ demoted single-member span ⇒ multi-var leftover
DB1.DBW600 i16 -32001 isolated ⇒ multi-var leftover
MW10 u16 4097 multi-var leftover in a different area — a block read can't cross (area, DB), multi-var can

Offsets 308, 311 and 312 carry poison (0xDEAD, 0x5A, 0xDEADBEEF) inside the holes cluster A's span reads through. No tag maps to them; they are there so a mis-computed slice offset returns garbage instead of a zero that could match by luck. Every seed above is distinct and non-zero for the same reason.

Known limitations

From the snap7.server.Server docstring upstream:

"Legacy S7 server implementation. Emulates a Siemens S7 PLC for testing and development purposes. [...] pure Python emulator implementation that simulates PLC behaviour for protocol compliance testing rather than full industrial-grade functionality."

Not exercised here — needs a lab rig:

  • S7-1500 Optimized-DB symbolic access
  • PG / OP / S7-Basic session-type differentiation
  • PUT/GET-disabled-by-default enforcement

See docs/drivers/S7-Test-Fixture.md for the full coverage map.

References