Catch-all commit for pending work on the task-galaxy-e2e branch that
wasn't part of the FOCAS migration. Grouping by topic so future per-topic
commits can be cherry-picked if needed.
TwinCAT
- src/.../Driver.TwinCAT/AdsTwinCATClient.cs + TwinCATDriverFactoryExtensions.cs:
factory-registration extensions + ADS client refinements.
- src/.../Driver.TwinCAT.Cli/Commands/BrowseCommand.cs: new browse command
for the TwinCAT test-client CLI.
- tests/.../Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs + TwinCatProject/:
fixture scaffold with a minimal POU + README pointing at the TCBSD/ESXi
VM for e2e.
- docs/Driver.TwinCAT.Cli.md + docs/drivers/TwinCAT-Test-Fixture.md:
documentation for the above.
- docs/v3/twincat-backlog.md: forward-looking backlog seed.
Admin UI + fleet status
- src/.../Admin/Components/Pages/Clusters/DriversTab.razor + Hosts.razor:
UI refresh for fleet-status rendering.
- src/.../Admin/Hubs/FleetStatusHub.cs + FleetStatusPoller.cs +
Admin/Program.cs: SignalR hub + poller plumbing for live fleet data.
- tests/.../Admin.Tests/FleetStatusPollerTests.cs: poller coverage.
Server + redundancy runtime (Phase 6.3 follow-ups)
- src/.../Server/Hosting/RedundancyPublisherHostedService.cs: HostedService
that owns the RedundancyStatePublisher lifecycle + wires peer reachability.
- src/.../Server/Redundancy/ServerRedundancyNodeWriter.cs: OPC UA
variable-node writer binding ServiceLevel + ServerUriArray to the
publisher's events.
- src/.../Server/Program.cs + Server.csproj: hosted-service registration.
- tests/.../Server.Tests/ServerRedundancyNodeWriterTests.cs +
Server.Tests.csproj: coverage for the above.
Configuration
- src/.../Configuration/Validation/DraftValidator.cs +
tests/.../Configuration.Tests/DraftValidatorTests.cs: draft-validation
refinements.
E2E scripts (shared infrastructure)
- scripts/e2e/README.md + _common.ps1 + test-all.ps1: shared helpers + the
all-drivers test-all runner.
- scripts/e2e/test-opcuaclient.ps1: OPC UA Client e2e runner.
Docs
- docs/v2/implementation/phase-6-{1,2,3,4}*.md + exit-gate-phase-{3,7}.md:
phase-gate + implementation doc updates.
- docs/v2/plan.md: top-level plan refresh.
- docs/v2/redundancy-interop-playbook.md: client interop playbook for the
Phase 6.3 redundancy-runtime work.
Two orphan FOCAS docs remain on disk but deliberately unstaged —
docs/v2/focas-deployment.md and docs/v2/implementation/focas-simulator-plan.md
describe the now-retired Tier-C topology and should either be rewritten
or deleted in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.2 KiB
TwinCAT test fixture
Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver.
TL;DR: Integration-test suite lives at
tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/. TwinCATXarFixture
probes TCP 48898 on an operator-supplied runtime; the suite runs 14
[TwinCATFact] methods + one 16-case [TwinCATTheory] = 30 test cases end-to-end
through the real ADS stack when the runtime is reachable, skips cleanly
otherwise. The runtime can be a Hyper-V XAR VM or a TCBSD VM
(TwinCatProject/README.md covers both). Unit tests via FakeTwinCATClient
still carry the exhaustive contract coverage alongside.
TwinCAT is the only driver outside Galaxy that uses native notifications
(no polling) for ISubscribable. The integration suite verifies that path on
the wire; the fake exposes a fire-event harness so notification routing is
also contract-tested rigorously at the unit layer.
What the fixture is
Integration layer: tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/
— TwinCATXarFixture TCP-probes ADS port 48898 on the host supplied by
TWINCAT_TARGET_HOST (defaults to localhost) + requires
TWINCAT_TARGET_NETID (AmsNetId of the runtime). Optionally takes
TWINCAT_TARGET_PORT (default 851 = TC3 PLC runtime 1). No fixture-owned
lifecycle — XAR / TCBSD can't run in Docker because they bypass the host
kernel scheduler, so the runtime stays operator-managed.
TwinCatProject/README.md documents the required project state; the tests
gate on [TwinCATFact] / [TwinCATTheory] and skip cleanly when
TWINCAT_TARGET_NETID is unset or the probe fails.
Unit layer: tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/ remains the
primary contract coverage. FakeTwinCATClient fakes the
AddDeviceNotification flow so tests can trigger callbacks without a running
runtime.
What it actually covers
Integration (live runtime)
Every capability the driver implements is exercised on the wire:
- Read —
Driver_reads_seeded_DINT_through_real_ADS(AMS handshake + symbolic read ofGVL_Fixture.nCounter) - Write + read round-trip —
Driver_write_then_read_round_trip_on_scratch_REALonGVL_Fixture.rSetpoint - Array element round-trip —
Driver_round_trips_array_element_write_and_readonGVL_Arrays.aReal1D[5](exercisesTwinCATSymbolPathsubscript rendering) - Subscribe (native ADS notifications) —
Driver_subscribe_receives_native_ADS_notifications_on_counter_changes; observesOnDataChangefiring within 10 s of subscribe - Symbol browse (direct client path) —
Driver_browses_committed_symbol_hierarchy_via_real_ADSviaITwinCATClient.BrowseSymbolsAsync - Symbol browse (through DiscoverAsync +
IAddressSpaceBuilderpipeline) —DiscoverAsync_renders_declared_tags_and_controller_browse_hits_address_space_builderverifies the realTwinCAT/ → device/ → Discovered/folder tree - Auto-reconnect —
Driver_auto_reconnects_after_underlying_client_is_disposeddisposes theAdsClientmid-flight; next read must re-establish - Primitive type coverage —
Driver_reads_every_primitive_type_with_correct_mappingruns as a[Theory]against the 16 primitives inGVL_Primitives(Bool, SInt, USInt, Int, UInt, DInt, UDInt, LInt, ULInt, Real, LReal, String, Time, TimeOfDay, Date, DateTime) — asserts status + CLR type + seed value where ergonomic - Bit-indexed BOOL —
Driver_reads_bit_indexed_BOOL_from_wordagainstGVL_Primitives.vWord.3+.4(bits of0xBEEF) - Nested UDT navigation —
Driver_reads_deeply_nested_UDT_pathreadsGVL_Plant.Line1.Stations[1].Axes[1].Motor.Temperature(LREAL) +.Running(BOOL) - Multi-device routing + isolation —
Driver_routes_reads_per_device_and_isolates_unreachable_peerspairs the real runtime with a bogus AmsNetId; healthy device reads still succeed - Probe loop +
IHostConnectivityProbe—Probe_loop_raises_host_status_transition_to_Running_on_reachable_targetassertsOnHostStatusChanged → Runningand snapshot parity - Negative error mappings —
Driver_reports_errors_for_unknown_tag_and_nonexistent_symbol_and_readonly_writecoversBadNodeIdUnknown, ghost-symbol communication errors, and theBadNotWritableshort-circuit
All tests gate on TWINCAT_TARGET_NETID (required) via [TwinCATFact] /
[TwinCATTheory]; TWINCAT_TARGET_HOST (default localhost) and
TWINCAT_TARGET_PORT (default 851) are optional overrides.
Unit
TwinCATAmsAddressTests—ads://<netId>:<port>parsing + routingTwinCATCapabilityTests— data-type mapping (primitives + declared UDTs), read-only classificationTwinCATReadWriteTests— read + write through the fake, status mappingTwinCATSymbolPathTests— symbol-path routing for nested struct membersTwinCATSymbolBrowserTests—ITagDiscovery.DiscoverAsyncviaBrowseSymbolsAsync+ system-symbol filteringTwinCATNativeNotificationTests—AddDeviceNotificationregistration, callback-delivery-to-OnDataChangewiring, unregister on unsubscribeTwinCATDriverTests—IDriverlifecycle
Capability surfaces whose contract is verified at the unit layer: IDriver,
IReadable, IWritable, ITagDiscovery, ISubscribable,
IHostConnectivityProbe, IPerCallHostResolver. The integration suite now
verifies ITagDiscovery + IHostConnectivityProbe on the wire as well.
Bugs caught by live runs
The integration suite surfaced three driver defects that FakeTwinCATClient
couldn't, since each lived below the abstraction boundary:
- Notification cycle time unit —
NotificationSettings(cycleTime, maxDelay)takes milliseconds per Beckhoff InfoSys (tcadsnetref/7313319051), but the driver was multiplying by10_000under a "100 ns units" assumption. A requested 250 ms cycle was being set to ~41 minutes — subscribe never fired. Fix inAdsTwinCATClient.AddNotificationAsync. STRING(N)/WSTRING(N)type mapper —MapSymbolTypeNameonly matched bare"STRING"/"WSTRING", so sized strings (the common case) fell offBrowseSymbolsAsyncentirely. Fix: strip the(…)bound before the switch.- Bit-indexed BOOL path — driver was sending
"GVL.vWord.3"to ADS as a BOOL read. TwinCAT's symbol table doesn't expose bit-access paths; the read returnedDeviceSymbolNotFound. Fix: strip the.Nsuffix, read the parent word asuint, extract the bit locally viaExtractBit.
All three paths are now pinned by live-wire tests.
What it does NOT cover
1. AMS / ADS wire framing
No raw AMS packet is inspected. Beckhoff's TwinCAT.Ads NuGet (their own
.NET SDK, not libplctag-style OSS) has no in-process fake at the frame
level; tests run against a real router.
2. Multi-route AMS
ADS supports chained routes (<localNetId> → <routerNetId> → <targetNetId>)
for PLCs behind an EC master / IPC gateway. Parse coverage exists; wire-path
coverage is single-hop only.
3. Notification coalescing under jitter
AddDeviceNotification delivers at the runtime's cycle boundary; under
sustained CPU load or network jitter real notifications can coalesce. The
live test only asserts at-least-one delivery within a generous window —
coalescing behavior under stress isn't verified.
4. TC2 vs TC3 variant handling
TwinCAT 2 (ADS v1) and TwinCAT 3 (ADS v2) have subtly different
GetSymbolInfoByName semantics + symbol-table layouts. Driver + tests target
TC3; TC2 compatibility is not exercised.
5. Alarms / history
Driver doesn't implement IAlarmSource or IHistoryProvider — not in scope
for this driver family. TwinCAT 3's TcEventLogger could theoretically back
an IAlarmSource, but shipping that is a separate feature.
When to trust TwinCAT tests, when to reach for a rig
| Question | Unit tests | Real TwinCAT runtime |
|---|---|---|
| "Does the AMS address parser accept X?" | yes | - |
"Does notification → OnDataChange wire correctly?" |
yes (contract) | yes |
| "Does symbol browsing filter TwinCAT internals?" | yes | yes |
| "Does a real ADS read return correct bytes?" | no | yes (required) |
| "Does auto-reconnect work on router restart?" | no (contract only) | yes (required) |
| "Do notifications coalesce under sustained load?" | no | yes (required) |
| "Does a TC2 PLC work the same as TC3?" | no | yes (required) |
Follow-up candidates
Deferred to v3 — see docs/v3/twincat-backlog.md.
Covers TC2 coverage, notification-coalescing-under-load, multi-hop AMS,
license-rotation automation, and a dedicated lab IPC.
Key fixture / config files
tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs— TCP probe + skip-attributes + env-var parsingtests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs— wire-level test suite (14[TwinCATFact]+ 16-case[TwinCATTheory])tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md— project spec + VM setup + license-rotation notestests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/FakeTwinCATClient.cs— in-process fake with the notification-fire harnesssrc/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs— ctor is(TwinCATDriverOptions, string driverInstanceId, ITwinCATClientFactory? = null)