#816 finding: the SDK checks that the server certificate names the host only when a session is created with checkDomain:true. The driver, the AdminUI browser and Client.Shared all called the 8-argument DefaultSessionFactory.CreateAsync, which forwards checkDomain:false — so no host-name check ever ran (why a trusted NAT cert for *.invalid browsed fine over an IP in #813). When enabled the SDK (Session.OpenAsync -> ValidateDomains -> FindDomain) compares ConfiguredEndpoint.EndpointUrl.IdnHost — the CONFIGURED host, because OpcUaEndpointSelection/EndpointSelector rewrote the advertised one — against the cert's DNS/IP SANs; secured sessions only, and the auto-accept handler accepts a mismatch. Policy: certificateValidation.validateServerHostName, tri-state. Unset = OpcUaCertificateValidationOptions.DefaultValidateServerHostName = false, the pre-#816 behaviour, so nothing changes on upgrade; true = validate the configured host; false = explicit NAT/port-publish opt-out. Threaded into the driver, the AdminUI browser (same JSON) and the AdminUI form (a select that keeps "unset" unset), Client.Shared ConnectionSettings.ValidateServerHostName and the CLI --validate-host-name. A driver refusal names the opt-out. #818: the NAT fixture's /app/pki is on the opc-plc-nat-pki volume (verified: same thumbprint across --force-recreate), its PUBLIC cert is committed as Docker/nat/opc-plc-nat-server.der, and NatPublishedEndpointBrowseTests run every secure browse on a throwaway PKI (internal browser seam): refusal with nothing trusted, positive with the cert trusted (unset/false host check), and a BadCertificateHostNameInvalid refusal with the check on. The trusted cert is OPCUA_NAT_SERVER_CERT or the committed one (skip on thumbprint mismatch); OPCUA_NAT_SERVER_CERT_TRUSTED is retired. CI exports its throwaway NAT container's public cert. ServerHostNameValidationTests pin the driver against the plain opc-plc. MIN_EXECUTED not raised (needs a hosted run): +6 in v2-integration (440 -> 446), documented in scripts/ci/README.md.
6.5 KiB
Whole-solution CI
The v2-ci workflow restores, builds and tests in one job so the test step uses
that job's Release binaries. Restore disables parallel package work; restore and
build use at most two MSBuild workers. Testing uses one MSBuild worker, disables
parallel target-framework runs and loads ci.runsettings: one VSTest process and
two xUnit collection workers with conservative scheduling. Assemblies that disable
parallelization keep that setting.
The workflow retains the E2E/LiveIntegration exclusions, includes SlowUnit, keeps Akka's test time factor at three, and requires at least 12,000 executed tests. Runner capacity and TRX files remain available for diagnosing failures. These limits reduce contention; they do not prove every timing failure has one cause. There are no automatic test retries or changed assertion deadlines.
Gitea 1.26 supports the repository-wide lmxopcua-v2-ci concurrency group. Only
one run is active; an active run is not cancelled, but a newer pending run may
supersede an older pending run. The group does not serialize other repositories
using the same host.
Stall guards (#718): the job has timeout-minutes: 120 (healthy runs take about 60 minutes)
and the test step passes --blame-hang --blame-hang-timeout 20m --blame-hang-dump-type none, so a
test that stops making progress is killed and named in the log instead of leaving hours of silence
(run 959 stalled 3h12m until the server reaper failed it).
To reproduce the bounded test leg after restore and Release build:
AKKA_TEST_TIMEFACTOR=3 dotnet test ZB.MOM.WW.OtOpcUa.slnx \
--configuration Release --no-build -m:1 -p:TestTfmsInParallel=false \
--settings scripts/ci/ci.runsettings \
--filter "Category!=E2E&Category!=LiveIntegration" \
--logger trx --results-directory artifacts/trx
MIN_EXECUTED=12000 bash scripts/ci/assert-not-all-skipped.sh artifacts/trx/*.trx
The guard uses the existing TRX Counters/@executed value for the aggregate
floor. Its skipped diagnostic counts individual Results/UnitTestResult
records whose outcome is NotExecuted, using the document's XML namespace.
Some VSTest reports set summary notExecuted to zero even when these records
are present. Skip records are counted per file, including repeated runs; they
are not a count of unique test methods and do not affect the executed floor.
For malformed XML, the diagnostic retains the old summary-counter fallback;
this guard does not add XML validation to its existing gate policy.
Run the deterministic script fixtures without building or running the solution:
python3 scripts/ci/test-assert-not-all-skipped.py
Verified hosted baseline: Gitea run 975
completed successfully on 2026-10-13 for commit
76bdca90e94e814e06ef10b78271e2b492b2ba32 (merged by PR #711).
Restore, Release build, README drift check, the serialized whole-solution test
step, the 12,000-test floor and TRX upload all passed. The floor counted
14,630 executed tests across 62 TRX files. This baseline predates the individual
skip-reporting correction; its printed zero skip diagnostic is not skip evidence.
Fixture-backed integration and guarded E2E workflows (#757)
Both are UNVERIFIED on a hosted Gitea runner until their first run; they were validated locally only
(YAML parse, the exact dotnet test commands and fixture script against local throwaway containers).
Neither touches v2-ci.yml; each has its own concurrency group (lmxopcua-v2-integration,
lmxopcua-v2-e2e, cancel-in-progress: false) so a pending run of one cannot supersede a pending
lmxopcua-v2-ci run.
v2-integration.yml(push tomaster, path-filtered PRs, manual;timeout-minutes: 60).integration-fixtures.sh upstarts throwaway SQL Server 2022, PostgreSQL 17 (seeded fromseed-pg.sql), QuestDB and opc-plc — plus a port-NAT opc-plc (otopcua-ci-opcplc-nat, host 50110 → 50000, private.invalidadvertised host,OPCUA_NAT_ENDPOINT, #813) — (otopcua-ci-*, non-default host ports), exports the env vars the fixtures read, anddownremoves them. It uses plaindocker run, notservices:(act_runner support is undocumented and Postgres needs a seed step); the only runner requirement is a docker daemon. Runs the Sql, Historian.SqlServer, Historian.QuestDb, OpcUaClient and OpcUaClient.Browser*.IntegrationTestswithMIN_EXECUTED=440. Local run: 468 executed / 8 skipped (Oracle x2, the blackholedocker pausegate, one QuestDB stress test, 4 others), about 4.5 min of test time, QuestDB the longest (~3.3 min). Not covered (no pullable image or needs credentials): Modbus/S7/AB/FOCAS/TwinCAT sims, Mqtt, MTConnect, Oracle, Host real-infra mode. #816/#818 added 6 executing tests here, floor NOT yet raised:ServerHostNameValidationTests(3, vs the plainotopcua-ci-opcplc) and the NAT positive-secure / host-name cases (3, which run becauseintegration-fixtures.shnow exports the NAT container's public certificate asOPCUA_NAT_SERVER_CERT). Locally (2026-11-11, shared host) the two OpcUaClient projects went 13 → 19 executed with 0 skipped, andassert-not-all-skipped.shcounted them. Once a hosted run's TRX showsDriver.OpcUaClient.trxexecuted=8 andDriver.OpcUaClient.Browser.trxexecuted=11, raiseMIN_EXECUTEDinv2-integration.yml440 → 446. If the NAT three show as skipped there, the certificate hand-off (docker exec … /app/pki/own/certs/*.der) did not work on the runner — fix that, do not lower the bump.v2-e2e.yml(manual + weekly cron only;timeout-minutes: 90).probe-e2e-fixtures.shTCP-probes 10.100.0.35 ports 5020/1102/50000; if any is closed every later step is skipped and the run is green with a notice. Otherwise runs the Modbus, S7, OpcUaClient and OpcUaClient.Browser integration projects against the shared fleet withMIN_EXECUTED=35(OPCUA_NAT_ENDPOINTpoints at the shared NAT stack on :50010; its 6 tests skip if it is down, so it is not a probe-required port — and its 3 certificate-trusting ones also skip if the stack's certificate no longer matches the committedDocker/nat/opc-plc-nat-server.der, so do not count them in the floor; #816's 3ServerHostNameValidationTestsneed only the probe-required :50000, so this floor may go 35 → 38 after a hosted run shows them executed) (local: 42 executed / 38 skipped; Modbus skips the profiles that are not up). No test carriesCategory=E2Etoday, so the zero-match guard is this executed floor.