Files
lmxopcua/docs/drivers/TwinCAT-Test-Fixture.md
Joseph Doherty 69e0d02c72 task-galaxy-e2e branch — non-FOCAS work-in-progress snapshot
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>
2026-04-24 14:12:19 -04:00

9.2 KiB
Raw Blame History

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:

  • ReadDriver_reads_seeded_DINT_through_real_ADS (AMS handshake + symbolic read of GVL_Fixture.nCounter)
  • Write + read round-tripDriver_write_then_read_round_trip_on_scratch_REAL on GVL_Fixture.rSetpoint
  • Array element round-tripDriver_round_trips_array_element_write_and_read on GVL_Arrays.aReal1D[5] (exercises TwinCATSymbolPath subscript rendering)
  • Subscribe (native ADS notifications)Driver_subscribe_receives_native_ADS_notifications_on_counter_changes; observes OnDataChange firing within 10 s of subscribe
  • Symbol browse (direct client path)Driver_browses_committed_symbol_hierarchy_via_real_ADS via ITwinCATClient.BrowseSymbolsAsync
  • Symbol browse (through DiscoverAsync + IAddressSpaceBuilder pipeline)DiscoverAsync_renders_declared_tags_and_controller_browse_hits_address_space_builder verifies the real TwinCAT/ → device/ → Discovered/ folder tree
  • Auto-reconnectDriver_auto_reconnects_after_underlying_client_is_disposed disposes the AdsClient mid-flight; next read must re-establish
  • Primitive type coverageDriver_reads_every_primitive_type_with_correct_mapping runs as a [Theory] against the 16 primitives in GVL_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 BOOLDriver_reads_bit_indexed_BOOL_from_word against GVL_Primitives.vWord.3 + .4 (bits of 0xBEEF)
  • Nested UDT navigationDriver_reads_deeply_nested_UDT_path reads GVL_Plant.Line1.Stations[1].Axes[1].Motor.Temperature (LREAL) + .Running (BOOL)
  • Multi-device routing + isolationDriver_routes_reads_per_device_and_isolates_unreachable_peers pairs the real runtime with a bogus AmsNetId; healthy device reads still succeed
  • Probe loop + IHostConnectivityProbeProbe_loop_raises_host_status_transition_to_Running_on_reachable_target asserts OnHostStatusChanged → Running and snapshot parity
  • Negative error mappingsDriver_reports_errors_for_unknown_tag_and_nonexistent_symbol_and_readonly_write covers BadNodeIdUnknown, ghost-symbol communication errors, and the BadNotWritable short-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

  • TwinCATAmsAddressTestsads://<netId>:<port> parsing + routing
  • TwinCATCapabilityTests — data-type mapping (primitives + declared UDTs), read-only classification
  • TwinCATReadWriteTests — read + write through the fake, status mapping
  • TwinCATSymbolPathTests — symbol-path routing for nested struct members
  • TwinCATSymbolBrowserTestsITagDiscovery.DiscoverAsync via BrowseSymbolsAsync + system-symbol filtering
  • TwinCATNativeNotificationTestsAddDeviceNotification registration, callback-delivery-to-OnDataChange wiring, unregister on unsubscribe
  • TwinCATDriverTestsIDriver lifecycle

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:

  1. Notification cycle time unitNotificationSettings(cycleTime, maxDelay) takes milliseconds per Beckhoff InfoSys (tcadsnetref/7313319051), but the driver was multiplying by 10_000 under a "100 ns units" assumption. A requested 250 ms cycle was being set to ~41 minutes — subscribe never fired. Fix in AdsTwinCATClient.AddNotificationAsync.
  2. STRING(N) / WSTRING(N) type mapperMapSymbolTypeName only matched bare "STRING" / "WSTRING", so sized strings (the common case) fell off BrowseSymbolsAsync entirely. Fix: strip the (…) bound before the switch.
  3. 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 returned DeviceSymbolNotFound. Fix: strip the .N suffix, read the parent word as uint, extract the bit locally via ExtractBit.

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 parsing
  • tests/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 notes
  • tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/FakeTwinCATClient.cs — in-process fake with the notification-fire harness
  • src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs — ctor is (TwinCATDriverOptions, string driverInstanceId, ITwinCATClientFactory? = null)