Integration-sweep follow-up #6 (SQL leg). The sweep flagged 7 heavy 2-node deploy/failover/reconnect E2E tests as 'time out under amd64-emulated SQL' and suggested raising deadlines. That diagnosis was WRONG: run against real (native-amd64) SQL on the Docker host, they still hang — even at 4x deadline. Root cause is that these tests had never actually run against real SQL, only the EF in-memory provider, which does NOT enforce foreign keys. Two distinct defects, both hidden by in-memory: 1. Missing FK-parent seed (5 tests). The deploy records a NodeDeploymentState row per node, FK'd to ClusterNode (FK_NodeDeploymentState_ClusterNode_NodeId). The tests never seeded ServerCluster + ClusterNode, so on SQL each node-state INSERT throws and the deploy never seals (waits the full deadline regardless of size). Added TwoNodeClusterHarness.SeedDefaultClusterAsync (seeds a ServerCluster + a ClusterNode per node; Warm/NodeCount=2 to satisfy the SQL CHECK constraint CK_ServerCluster_RedundancyMode_NodeCount AND the ClusterEnabledNodeCountMismatch validator). Called it in DeployHappyPath x2 / Failover / FleetDiagnostics / EquipmentNamespace; fixed DriverReconnect to seed node B + NodeCount=2. 2. Stale EquipmentId (EquipmentNamespace test; pre-existing on BOTH providers). Seeded EquipmentId='eq-1', but the later-added EquipmentIdNotDerived validator requires EquipmentId == DeriveEquipmentId(EquipmentUuid) ('EQ-'+first 12 hex). Fixed the seed to a canonical id + matching UUID. Only surfaced once the FK fix let the deploy reach equipment validation. Also added OTOPCUA_HARNESS_SQL_HOST / OTOPCUA_HARNESS_LDAP_HOST overrides so the harness fixtures can run on the native-amd64 Docker host (avoids arm64 mssql emulation entirely). Verified: the 5 SQL-backed classes go 11/11 green vs native SQL; in-memory default unregressed (12/12). RoslynVirtualTagEvaluatorTests racing test is a pre-existing flaky race (not SQL-backed; passes in isolation) — left as-is.
19 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 E2E time out under amd64-emulated SQL (OPEN); LDAP image swapped OpenLDAP→GLAuth 2026-07-15 (PR #451). The 5 skips are driver-fixture-gated E2E, not LDAP |
| TwinCAT | ⏭️ 13 skip | no TC3 XAR fixture exists (expected clean-skip) |
| Modbus (dl205 / mitsubishi / s7_1500 / exception_injection) | ✅ 16/13/10/17 (was 8F/5F/3F/8F) | NOT drift — fixture-cycling artifact, re-verified GREEN 2026-07-15 — see below |
| AbCip | ✅ 10P/3skip (was 6F/1P/3skip) | static-init-order bug FIXED 2026-07-15 (326b22a7) — see below |
| FOCAS | ✅ 10/10 (was 9F/1P) | topology + mock-timer-endianness FIXED 2026-07-15 — see below |
| AbLegacy | ✅ slc500 2/2, micrologix 1/1, plc5 1/1 (was 4F) | fixture-deployment gap FIXED 2026-07-15 — see below |
| 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 — or, in the Modbus case, a fixture-cycling false alarm (see below). The drivers are
fine: 4 suites fully green, all Modbus profiles green (standard + dl205/mitsubishi/s7_1500/exception once
correctly bound — the sweep's non-standard "failures" were the stale-container artifact), 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'd bit-rotted in several distinct ways.
Follow-up progress (2026-07-15): ALL 7 CLOSED. #1 AbCip (PR #446), #5 OpcUaServer (PR #447), #2 Modbus (PR #448 — false alarm), #4 AbLegacy (PR #449 — fixture gap), #3 FOCAS (PR #450 — topology + mock-timer), #7 (fixture deploys), #6 LDAP-image (PR #451 — OpenLDAP→GLAuth), and #6 SQL-deploy-E2E (PR #452 — misdiagnosed "emulation timing"; really missing FK-parent seeds + a stale EquipmentId, hidden by the in-memory provider). Every driver + server integration suite is now verified GREEN. Zero OtOpcUa production regressions across the entire sweep — every failure was fixture/harness/test rot.
Per-suite failure detail (evidence)
-
Modbus non-standard profiles — ✅ RE-VERIFIED GREEN 2026-07-15 (PR #448). The sweep's 8F/5F/3F/8F was a false alarm — a fixture-cycling artifact, NOT pymodbus drift and NOT an OtOpcUa regression. Correcting the sweep's original diagnosis:
- The image genuinely runs pymodbus 3.13.0 (
docker run --entrypoint python ... -c 'import pymodbus; print(pymodbus.__version__)'→3.13.0; the Dockerfile already pinspymodbus[simulator]==3.13.0). The "…removed in v4" log line is 3.13.0's forward-deprecation warning, not evidence it runs 4.x — the original sweep note misread it. - Root cause: each Modbus profile is a separate profile-gated compose service sharing
:5020. A plaindocker compose down(no--profile) does not stop the running profile container, so it keeps holding:5020; the next--profile … upcontainer silently fails to bind (compose still says "Started"). Every later profile run then hit the stale container. Proven by a raw Modbus probe: the "mitsubishi" sim served DL205's0xCAFEat HR0 (DL205's V0 marker) and Illegal-Data-Address for mitsubishi-only addresses, whiledocker psshowed the mitsubishi container up with no port mapping and DL205 still holding:5020. - After force-removing all modbus containers and bringing up each profile cleanly (verifying it actually
published
:5020), all four pass: dl205 16/16, mitsubishi 13/13, s7_1500 10/10, exception_injection 17/17 (the injector needs--force-recreate— it uses a different container name). No code change — added a profile-cycling gotcha + reliable-cycle recipe toDocker/README.md.
- The image genuinely runs pymodbus 3.13.0 (
-
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 — ✅ FIXED 2026-07-15 (PR #450). Two test/fixture bugs, no OtOpcUa driver change.
- Topology:
FocasSimFixturealready reads an env-overridable endpoint (OTOPCUA_FOCAS_SIM_ENDPOINT, defaultlocalhost:8193) and exposesHost/Port, butWireBackendTests+WireBackendCoverageTestshardcodedfocas://127.0.0.1:8193. Against a remote fixture the skip-gate passed (remote reachable) but the driver dialed localhost → KeyNotFound. Fix: added aDeviceUriproperty to the fixture and derived each test'sDeviceHostfrom it — recovered 8 of 9. - Mock timer endianness (newly surfaced): the last failure (
Timers_populate) read60397977600for a3600 spower-on timer (=0x3C000000 × 60). Root cause: the focas-mock's_wire_timerencoded theminute/msecfields big-endian (_u32), butcnc_rdtimer's timer fields are little-endian on real hardware — a documented, live-31i-B-validated quirk (FocasWireClient.ParseTimerdecodes them LE, perdocs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md). The driver was correct; the mock was wrong. Fix:_wire_timernow emits little-endian (_u32_le) → suite 10/10. Fixture deployed at~/otopcua-focas.
- Topology:
-
AbLegacy — ✅ FIXED 2026-07-15 (PR #449). Fixture-deployment gap, not a code defect. The AbLegacy suite needs
ab_serverin PCCC mode (--plc=SLC500|Micrologix|PLC/5), which reuses the AbCip image (otopcua-ab-server:libplctag-release, already on the host) and binds:44818. Two issues: (a) no PCCC container was deployed (~/otopcua-ablegacydidn't exist); (b) the sweep's 4F-not-skip means:44818was reachable but answering CIP (a leftover AbCip container) — the PCCC reads returned Bad. Same shared-port contamination class as the Modbus false alarm. Deployed the compose to~/otopcua-ablegacyon the host (image already present, no build needed) and ran each family withAB_LEGACY_COMPOSE_PROFILEset to match the running container (required — without it the[Theory]parametrises over all 3 families and 2 fail against a single-family container): slc500 2/2, micrologix 1/1 (+1 skip), plc5 1/1 (+1 skip) — the skips are family-inapplicable file types (e.g. noF8float file seeded on micrologix/plc5). No code change. -
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). Two independent issues:
- LDAP image — ✅ FIXED 2026-07-15 (PR #451). The opt-in real-LDAP mode (
OTOPCUA_HARNESS_USE_LDAP=1) usedbitnami/openldap:2.6, gone since Bitnami deprecated their free catalog in 2025. Replaced it with GLAuth — the same LDAP server the rest of the project uses (docker-dev + the live OPC UA data-plane auth, both against the shared GLAuth on:3893), removing the lone OpenLDAP outlier. Added a harnessglauth/config.toml(base DNdc=zb,dc=local, search-capableserviceaccount, dev users alice=full / bob=read-only, role groups with the same gidnumbers asscadaproj/infra/glauth) + repointed the harness's service-account override tocn=serviceaccount. Live-verified all three bind legs against a deployed container on:3894(serviceaccount search returns alice + 5 memberOf groups; alice/bob bind OK; wrong password →Invalid credentials (49)). NB: the real-LDAP mode is opt-in — the default Host run usesStubLdapAuthService, so this never blocked the default suite (the sweep's "5 LDAP skips" were actually the driver-fixture-gated E2E tests, not LDAP). - SQL-backed deploy E2E — ✅ FIXED 2026-07-15 (PR #452). The sweep's "amd64-emulated-SQL timing" diagnosis
was WRONG. Running the 7 heavy E2E against real (native-amd64) SQL on the Docker host proved they don't
time out for speed — they had never actually run against real SQL, only the EF in-memory provider (which
ignores FK constraints). Two distinct root causes, both hidden by in-memory:
- Missing FK-parent seed (5 tests): the deploy writes a
NodeDeploymentStaterow per node, which FKs toClusterNode(FK_NodeDeploymentState_ClusterNode_NodeId). The tests never seededServerCluster+ClusterNoderows, so on SQL each node-state INSERT throws → the deploy never seals (waits the full deadline regardless of how high — proven at 4× it still hung). AddedTwoNodeClusterHarness.SeedDefaultClusterAsync(seeds aServerCluster+ aClusterNodeper node;Warm/NodeCount=2 to satisfy both the SQL CHECK constraintCK_ServerCluster_RedundancyMode_NodeCountand theClusterEnabledNodeCountMismatchvalidator) and called it inDeployHappyPathTests×2 /FailoverDuringDeployTests/FleetDiagnosticsRoundTripTests/EquipmentNamespaceMaterializationTests; fixedDriverReconnectE2eTeststo seed node B + declare NodeCount=2. - Stale
EquipmentId(1 test, pre-existing on BOTH providers):EquipmentNamespaceMaterializationTestsseededEquipmentId="eq-1", but theEquipmentIdNotDerivedvalidator (added later) requiresEquipmentId == DeriveEquipmentId(EquipmentUuid)=EQ-+ first 12 hex of the UUID. Fixed the seed to a canonical id + matching UUID. (Was failing on in-memory too; only surfaced once the FK fix let the deploy reach equipment validation.) - Added
OTOPCUA_HARNESS_SQL_HOST/OTOPCUA_HARNESS_LDAP_HOSTenv overrides so the fixtures can live on the native-amd64 Docker host (docker compose up -d sqlthere; run withOTOPCUA_HARNESS_SQL_HOST=10.100.0.35,14331). - Result: the 5 SQL-backed classes go 11/11 green vs native SQL, and the in-memory default is unregressed
(12/12).
RoslynVirtualTagEvaluatorTests.Evaluate_racing_ClearCompiledScripts…is a pre-existing flaky race test (not SQL-backed; passes in isolation, flaked once under heavy parallel load) — left as-is.
- Missing FK-parent seed (5 tests): the deploy writes a
- LDAP image — ✅ FIXED 2026-07-15 (PR #451). The opt-in real-LDAP mode (
-
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.✅ RESOLVED 2026-07-15 (PR #448) — false alarm, no code change. Image already runs the pinned pymodbus 3.13.0; the 8F/5F/3F/8F was a fixture-cycling artifact (stale profile container holding:5020). Re-verified all four profiles GREEN (16/13/10/17). Added a profile-cycling gotcha + reliable-cycle recipe toDocker/README.md.FOCAS — reconcile the 127.0.0.1-mock topology with the remote-fixture model.✅ DONE 2026-07-15 (PR #450). Tests now deriveDeviceHostfrom the fixture endpoint (recovered 8/9); also fixed a newly-surfaced mock bug —_wire_timerencoded the timer big-endian but real hardware + driver use little-endian. Suite 10/10. No OtOpcUa driver change.AbLegacy — deploy an AbLegacy sim fixture (none on host).✅ DONE 2026-07-15 (PR #449) — fixture-deployment gap, no code defect. Deployed the PCCC compose to~/otopcua-ablegacy(reuses the AbCip image, already on host). All 3 families GREEN (slc500 2/2, micrologix 1/1, plc5 1/1). Recipe added below;AB_LEGACY_COMPOSE_PROFILEMUST match the running container.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 — ⬐ split into two:
replace✅ DONE 2026-07-15 (PR #451) — swapped the harness LDAP to GLAuth (unifies on the LDAP the rest of the project uses); live-verified the binds onbitnami/openldap:2.6(image gone):3894.amd64-emulated-SQL timing✅ DONE 2026-07-15 (PR #452) — misdiagnosed. Not a timing/emulation issue: the deploy E2E tests had never run against real SQL (in-memory ignores FKs) and lacked theServerCluster/ClusterNodeseed theNodeDeploymentStateFK requires → deploy never seals. Added a harness seeding helper +OTOPCUA_HARNESS_SQL_HOSToverride; also fixed a staleEquipmentIdin one test. 11/11 green vs native SQL, in-memory unregressed. (Roslyn racing test = pre-existing flaky, left as-is.)
Fixture deployment gap — FOCAS + AbLegacy fixtures aren't on the host.✅ RESOLVED 2026-07-15. Both now deployed under the home dir:~/otopcua-ablegacy(PR #449) +~/otopcua-focas(PR #450). (/optstill needs sudo; the home-dir deploys work fine.)
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
ssh dohertj2@10.100.0.35 'cd ~/otopcua-ablegacy && docker compose --profile slc500 up -d' # :44818 (PCCC; reuses AbCip image)
ssh dohertj2@10.100.0.35 'cd ~/otopcua-focas && docker compose up -d --build' # :8193
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 |
| AbLegacy | AB_LEGACY_ENDPOINT=10.100.0.35:44818 AB_LEGACY_COMPOSE_PROFILE=<slc500|micrologix|plc5> (must match the running PCCC container — else the [Theory] runs all 3 families and 2 fail) |
tests/Drivers/…AbLegacy.IntegrationTests |
| FOCAS | OTOPCUA_FOCAS_SIM_ENDPOINT=10.100.0.35:8193 (tests now honour this — no longer hardcoded to 127.0.0.1) |
tests/Drivers/…FOCAS.IntegrationTests |
| Host | OTOPCUA_HARNESS_USE_SQL=1 (+ SQL :14331 via the harness docker-compose.yml up sql) — set OTOPCUA_HARNESS_USE_LDAP=1 to exercise real binds against the GLAuth service (up ldap, host :3894; users alice/bob, svc cn=serviceaccount/serviceaccount123) |
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.