OpcUaServer.IntegrationTests was 4F. Two distinct, previously-masked issues: 1. Cert-store gap: the in-test client SecurityConfiguration used a bare new SecurityConfiguration()/CertificateIdentifier(), so ValidateAsync threw 'TrustedIssuerCertificates StorePath must be specified' before any connect. Added TestClientSecurity helper building Directory own/issuer/trusted/rejected stores under a throwaway temp PKI dir (mirrors DefaultApplicationConfigurationFactory); wired into DualEndpointTests + SubscriptionSurvivalTests. 2. Fixing #1 unmasked an SDK-version drift: the 1.5.378 node-cache read path throws ServiceResultException(BadNodeIdUnknown) for a removed node instead of returning a bad DataValue. The two subscription-survival assertions now expect the throw via Should.ThrowAsync. Behavior under test (removed node -> unknown) is unchanged; only the delivery mechanism differs. Suite 4F -> 4/4 green. Integration-sweep follow-up #5.
10 KiB
Integration-test sweep — status + follow-ups (2026-07-14)
Context: ran the deferred *.IntegrationTests sweep (the round-2 STATUS.md "Full integration-suite
sweep" item that discharges R2-02 T18, R2-03 T11, R2-07 T5/T6, R2-08 T14, R2-11 T24).
How: tests run on the Mac (repo + .NET 10 SDK), pointing at driver fixtures brought up on the
docker host 10.100.0.35, one driver at a time (serial — the suites have a ~16 GB/run memory leak).
Master at run time: 30672888 (branch master, clean).
Docker host 10.100.0.35 (DOCKER) note: has dotnet 10.0.300, 22 cores, but only ~7 GB free RAM
(the rest is shared SQL/GLAuth/fixtures) and no repo checkout — so the tests can't run on it (OOM risk
to shared infra). Run on the Mac against its fixtures. Fixture dirs live at /opt/otopcua-<driver>/ (owned,
need sudo to create new ones — I deployed FOCAS to ~/otopcua-focas on the host instead).
Results
| Suite | Result | Root cause of failures |
|---|---|---|
| OpcUaClient | ✅ 4/4 | — (opc-plc :50000) |
| OpcUaClient.Browser | ✅ 1/1 | — |
| S7 | ✅ 3/3 (2 skip) | skips = outage tests needing S7_TIMEOUT_OUTAGE_* env |
| Modbus (profile=standard) | ✅ 6/6 | — |
| Host | ⚠️ 105/117 (7F, 5 skip) | 7 heavy 2-node deploy/failover E2E time out (20–45 s) under amd64-emulated SQL; 5 LDAP-backed skip |
| TwinCAT | ⏭️ 13 skip | no TC3 XAR fixture exists (expected clean-skip) |
| Modbus (dl205 / mitsubishi / s7_1500 / exception_injection) | ❌ 8F/5F/3F/8F | sim-behavior drift — see below |
| AbCip | ✅ 10P/3skip (was 6F/1P/3skip) | static-init-order bug FIXED 2026-07-15 (326b22a7) — see below |
| FOCAS | ❌ 9F/1P | tests assume a localhost-colocated mock — see below |
| AbLegacy | ❌ 4F | no AbLegacy sim fixture deployed to the host |
| OpcUaServer | ✅ 4/4 (was 4F) | cert-store gap + SDK read-throws drift FIXED 2026-07-15 — see below |
Headline conclusion
No OtOpcUa production regression. Every failure is pre-existing test/fixture infrastructure rot in
long-deferred suites. The drivers are fine: 4 suites fully green, Modbus-standard green, Host 105/117 green,
and all driver unit suites + the live /run gates pass. These integration suites were DEFERRED in round 2
and never run, so they've bit-rotted in several distinct ways.
Per-suite failure detail (evidence)
-
Modbus non-standard profiles — NOT stale fixtures (all
profiles/*.jsonregister maps byte-match the repo) and NOT an OtOpcUa regression (standard profile + all Modbus unit suites green). The sim container runs pymodbus 4.x-dev (logs: "ModbusServerContext is deprecated … removed in v4",pymodbus.simulator --json_file /fixtures/<profile>.json). Failures areStatusCodeassertion mismatches on BCD decode / coil mapping / DL205 addressing — a pymodbus-version behavioral drift vs when the tests were authored. Fix: pin pymodbus intests/.../Modbus.IntegrationTests/Docker/Dockerfileto the version the tests target, or update expected values for 4.x. -
AbCip — static-init-order bug in the test harness (always-fail, blocks the whole suite).
tests/.../AbCip.IntegrationTests/AbServerProfile.cs:KnownProfiles.All { get; } = [ControlLogix, …]is declared at line 30, before thestatic readonlyprofile fields (line 34+). C# runs static initializers in textual order →Allcaptures[null,null,null,null]→ForFamilyNREs atp.Family(line 61) andAbServerFixture..ctorgets null. Fix (trivial, safe): makeAllan expression-bodied property:public static IReadOnlyList<AbServerProfile> All => [ControlLogix, CompactLogix, Micro800, GuardLogix];(evaluated on access, after the fields are set). Unblocks the suite; then re-run to see if the actual sim tests pass. -
FOCAS — tests key on
focas://127.0.0.1:8193/Axes/X/AbsolutePosition(KeyNotFound) — they assume a localhost-colocated mock, not a remote docker fixture at10.100.0.35:8193. The repo fixture (tests/.../FOCAS.IntegrationTests/Docker/, publishes8193:8193) was NOT deployed to the host; I deployed it to~/otopcua-focasand it serves, but the tests' node-key/topology assumption (127.0.0.1) doesn't match a remote sim. Fix: reconcile the mock topology — either run the FOCAS mock on localhost, or make the test node-keys honorOTOPCUA_FOCAS_SIM_ENDPOINT/FOCAS_MOCK_HOST. -
AbLegacy — 4F on
StatusCodebecause no AbLegacy sim exists on the host (no/opt/otopcua-ablegacy, none deployed). Reads return Bad. Fix: deploy an AbLegacy fixture (checktests/.../AbLegacy.IntegrationTests/Docker/). -
OpcUaServer — ✅ FIXED 2026-07-15 (PR #447). Was 4F on
Opc.Ua.ServiceResultException : TrustedIssuerCertificates StorePath must be specified— the in-test clientSecurityConfigurationomitted the trusted-issuer/peer/rejected store paths thatValidateAsyncrequires. Fix:TestClientSecurityhelper rooting Directory stores at a throwaway PKI dir (mirrorsDefaultApplicationConfigurationFactory). Fixing it unmasked a second drift — the 1.5.378 node-cache read path throwsServiceResultException(BadNodeIdUnknown)for a removed node rather than returning a badDataValue; the two subscription-survival assertions now useShould.ThrowAsync. Suite 4F → 4/4 green. -
Host — largely healthy (105/117). 7 failures are the heavy 2-node deploy/failover/reconnect E2E tests (
DeployHappyPathTests,FailoverDuringDeployTests,DriverReconnectE2eTests,FleetDiagnosticsRoundTripTests,EquipmentNamespaceMaterializationTests,RoslynVirtualTagEvaluatorTests) taking 20–45 s — likely timing under the amd64-emulated SQL on the arm64 Mac (mssql:2022 has no arm64 image). 5 LDAP-backed tests skip becausebitnami/openldap:2.6is gone from Bitnami's catalog (manifest unknown — Bitnami deprecated their free image catalog in 2025). Fix: replace the openldap image with a maintained one; run Host E2E against real (non-emulated) SQL or raise deadlines. -
TwinCAT — 13 skip cleanly (no TC3 XAR fixture exists; unit-only per the driver's design). Expected.
Follow-up backlog (distinct, not one fix)
AbCip static-init bug — trivial always-fail test bug. Expression-body✅ DONE 2026-07-15 (commitKnownProfiles.All.326b22a7, PR #446). Live-verified against the controllogix fixture: suite went 6F/1P/3skip → 10 pass / 0 fail / 3 skip (skips = emulator-mode tests needing a different fixture mode).- Modbus non-standard profiles — pin pymodbus in the fixture Dockerfile (or update expected values for 4.x).
- FOCAS — reconcile the 127.0.0.1-mock topology with the remote-fixture model.
- AbLegacy — deploy an AbLegacy sim fixture (none on host).
OpcUaServer — supply cert-store StorePath in the harness config.✅ DONE 2026-07-15 (PR #447). AddedTestClientSecurityhelper building a validated clientSecurityConfigurationwith Directory own/issuer/trusted/rejected stores under a throwaway PKI dir (mirrorsDefaultApplicationConfigurationFactory); wired into bothDualEndpointTests+SubscriptionSurvivalTests. That unmasked a second drift: the 1.5.378 node-cache read path throwsServiceResultException(BadNodeIdUnknown)for a removed node instead of returning a badDataValue— updated the two subscription-survival assertions toShould.ThrowAsync. Suite 4F → 4/4.- Host — replace
bitnami/openldap:2.6(image gone); run E2E on real SQL or raise deadlines. - Fixture deployment gap — FOCAS + AbLegacy fixtures aren't on the host at
/opt/otopcua-*; the sweep requires deploying them (and/optneeds sudo — used~/otopcua-<driver>for FOCAS).
Fixture setup recipes (to re-run the sweep)
Bring up on the docker host (distinct ports, can coexist except Modbus profiles share :5020):
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-modbus && docker compose --profile standard up -d' # :5020
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-abcip && docker compose --profile controllogix up -d' # :44818
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-s7 && docker compose --profile s7_1500 up -d' # :1102
ssh dohertj2@10.100.0.35 'cd /opt/otopcua-opcuaclient && docker compose up -d' # :50000
# FOCAS/AbLegacy not on host — rsync tests/.../Docker/ to ~/otopcua-<driver> then `docker compose up -d --build`
Run per suite (env → project):
| Suite | Env | Project |
|---|---|---|
| OpcUaClient(+Browser) | OPCUA_SIM_ENDPOINT=opc.tcp://10.100.0.35:50000 |
tests/Drivers/…OpcUaClient(.Browser).IntegrationTests |
| S7 | S7_SIM_ENDPOINT=10.100.0.35:1102 |
tests/Drivers/…S7.IntegrationTests |
| Modbus | MODBUS_SIM_ENDPOINT=10.100.0.35:5020 MODBUS_SIM_PROFILE=<standard|dl205|mitsubishi|s7_1500|exception_injection> (swap the host sim to match) |
tests/Drivers/…Modbus.IntegrationTests |
| AbCip | AB_SERVER_ENDPOINT=10.100.0.35:44818 AB_SERVER_PROFILE=controllogix |
tests/Drivers/…AbCip.IntegrationTests |
| FOCAS | OTOPCUA_FOCAS_SIM_ENDPOINT=10.100.0.35:8193 (but tests assume 127.0.0.1 — see above) |
tests/Drivers/…FOCAS.IntegrationTests |
| Host | OTOPCUA_HARNESS_USE_SQL=1 (+ SQL :14331 via tests/Server/…Host.IntegrationTests/docker-compose.yml up sql) OTOPCUA_HARNESS_USE_LDAP=0 |
tests/Server/…Host.IntegrationTests |
Modbus profile cycling: the sim services share :5020, so to run a profile:
ssh … 'cd /opt/otopcua-modbus && docker compose down && docker compose --profile <p> up -d', then run with
MODBUS_SIM_PROFILE=<p> (each class skips unless its profile is active). Wait for :5020 before running —
TCP opens before the register map fully loads.
Current state (post-sweep)
- All fixtures torn down — modbus/abcip/s7/opcuaclient/focas on the host removed (ports 5020/44818/1102/
50000/8193 all closed); the local Host SQL fixture
down -v'd. Shared host is clean. - Repo:
master @ 30672888, clean. No sweep changes committed (this file is the only artifact). - Nothing merged for the sweep — it's a read-only test run; the follow-ups above are unstarted.