diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx index 670153c..6207782 100644 --- a/ZB.MOM.WW.OtOpcUa.slnx +++ b/ZB.MOM.WW.OtOpcUa.slnx @@ -38,6 +38,7 @@ + diff --git a/docs/drivers/README.md b/docs/drivers/README.md index 899a479..4b48c19 100644 --- a/docs/drivers/README.md +++ b/docs/drivers/README.md @@ -45,7 +45,7 @@ Each driver has a dedicated fixture doc that lays out what the integration / uni - [Modbus](Modbus-Test-Fixture.md) — Dockerized `pymodbus` + per-family JSON profiles (4 compose profiles); best-covered driver, gaps are error-path-shaped - [Siemens S7](S7-Test-Fixture.md) — Dockerized `python-snap7` server; DB/MB read + write round-trip verified end-to-end on `:1102` - [AB Legacy](AbLegacy-Test-Fixture.md) — no integration fixture, unit-only via `FakeAbLegacyTag` (libplctag PCCC) -- [TwinCAT](TwinCAT-Test-Fixture.md) — no integration fixture, unit-only via `FakeTwinCATClient` with native-notification harness +- [TwinCAT](TwinCAT-Test-Fixture.md) — XAR-VM integration scaffolding (task #221); three smoke tests skip when VM unreachable. Unit via `FakeTwinCATClient` with native-notification harness - [FOCAS](FOCAS-Test-Fixture.md) — no integration fixture, unit-only via `FakeFocasClient`; Tier C out-of-process isolation scoped but not shipped - [OPC UA Client](OpcUaClient-Test-Fixture.md) — no integration fixture, unit-only via mocked `Session`; loopback against this repo's own server is the obvious next step - [Galaxy](Galaxy-Test-Fixture.md) — richest harness: E2E Host subprocess + ZB SQL live-smoke + MXAccess opt-in diff --git a/docs/drivers/TwinCAT-Test-Fixture.md b/docs/drivers/TwinCAT-Test-Fixture.md index ed92364..fdc2476 100644 --- a/docs/drivers/TwinCAT-Test-Fixture.md +++ b/docs/drivers/TwinCAT-Test-Fixture.md @@ -2,24 +2,62 @@ Coverage map + gap inventory for the Beckhoff TwinCAT ADS driver. -**TL;DR: there is no integration fixture.** Every test uses a -`FakeTwinCATClient` injected via `ITwinCATClientFactory`. Beckhoff's ADS -library has no open-source simulator; ADS traffic against real TwinCAT -runtimes is trusted from field deployments. +**TL;DR:** Integration-test scaffolding lives at +`tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/` (task #221). +`TwinCATXarFixture` probes TCP 48898 on an operator-supplied VM; three +smoke tests (read / write / native notification) run end-to-end through +the real ADS stack when the VM is reachable, skip cleanly otherwise. +**Remaining operational work**: stand up a TwinCAT 3 XAR runtime in a +Hyper-V VM, author the `.tsproj` project documented at +`TwinCatProject/README.md`, rotate the 7-day trial license (or buy a +paid runtime). Unit tests via `FakeTwinCATClient` still carry the +exhaustive contract coverage. -The silver lining: TwinCAT is the only driver outside Galaxy that uses -**native notifications** (no polling) for `ISubscribable`, and the fake -exposes a fire-event harness so notification routing is contract-tested -rigorously — just not on the wire. +TwinCAT is the only driver outside Galaxy that uses **native +notifications** (no polling) for `ISubscribable`, and the fake exposes a +fire-event harness so notification routing is contract-tested rigorously +at the unit layer. ## What the fixture is -Nothing at the integration layer. -`tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` is unit-only. -`FakeTwinCATClient` also fakes the `AddDeviceNotification` flow so tests can -trigger callbacks without a running runtime. +**Integration layer** (task #221, scaffolded): +`tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/` — +`TwinCATXarFixture` TCP-probes ADS port 48898 on the host specified by +`TWINCAT_TARGET_HOST` + requires `TWINCAT_TARGET_NETID` (AmsNetId of the +VM). No fixture-owned lifecycle — XAR can't run in Docker because it +bypasses the Windows kernel scheduler, so the VM stays +operator-managed. `TwinCatProject/README.md` documents the required +`.tsproj` project state; the file itself lands once the XAR VM is up + +the project is authored. Three smoke tests: +`Driver_reads_seeded_DINT_through_real_ADS`, +`Driver_write_then_read_round_trip_on_scratch_REAL`, and +`Driver_subscribe_receives_native_ADS_notifications_on_counter_changes` +— all skip cleanly via `[TwinCATFact]` when the runtime isn't +reachable. -## What it actually covers (unit only) +**Unit layer**: `tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/` is +still the primary coverage. `FakeTwinCATClient` also fakes the +`AddDeviceNotification` flow so tests can trigger callbacks without a +running runtime. + +## What it actually covers + +### Integration (XAR VM, task #221 — code scaffolded, needs VM + project) + +- `TwinCAT3SmokeTests.Driver_reads_seeded_DINT_through_real_ADS` — real AMS + handshake + ADS read of `GVL_Fixture.nCounter` (seeded at 1234, MAIN + increments each cycle) +- `TwinCAT3SmokeTests.Driver_write_then_read_round_trip_on_scratch_REAL` — + real ADS write + read on `GVL_Fixture.rSetpoint` +- `TwinCAT3SmokeTests.Driver_subscribe_receives_native_ADS_notifications_on_counter_changes` + — real `AddDeviceNotification` against the cycle-incrementing counter; + observes `OnDataChange` firing within 3 s of subscribe + +All three gated on `TWINCAT_TARGET_HOST` + `TWINCAT_TARGET_NETID` env +vars; skip cleanly via `[TwinCATFact]` when the VM isn't reachable or +vars are unset. + +### Unit - `TwinCATAmsAddressTests` — `ads://:` parsing + routing - `TwinCATCapabilityTests` — data-type mapping (primitives + declared UDTs), @@ -89,21 +127,30 @@ back an `IAlarmSource`, but shipping that is a separate feature. ## Follow-up candidates -1. **TwinCAT 3 runtime on CI** — Beckhoff ships a free developer runtime - (7-day trial, restartable). Could run on a Windows CI runner with a - helper that auto-restarts the runtime every 6 days. Works but operational - overhead. -2. **AdsSimulator** — Beckhoff has a closed-source "ADS simulator" library - used internally; not publicly available. -3. **Lab rig** — cheapest IPC (CX7000 / CX9020) on a dedicated network; the - only route that covers TC2 + real notification behavior + EtherCAT I/O - effects. - -Without a rig, TwinCAT correctness is trusted from the fake matching -reality, which has held across field deployments so far. +1. **XAR VM live-population** — scaffolding is in place (this PR); the + remaining work is operational: stand up the Hyper-V VM, install XAR, + author the `.tsproj` per `TwinCatProject/README.md`, configure the + bilateral ADS route, set `TWINCAT_TARGET_HOST` + `TWINCAT_TARGET_NETID` + on the dev box. Then the three smoke tests transition skip → pass. + Tracked as #221. +2. **License-rotation automation** — XAR's 7-day trial expires on + schedule. Either automate `TcActivate.exe /reactivate` via a + scheduled task on the VM (not officially supported; reportedly works + for some TC3 builds), or buy a paid runtime license (~$1k one-time + per runtime per CPU) to kill the rotation. The doc at + `TwinCatProject/README.md` §License rotation walks through both. +3. **Lab rig** — cheapest IPC (CX7000 / CX9020) on a dedicated network; + the only route that covers TC2 + real EtherCAT I/O timing + cycle + jitter under CPU load. ## 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` + — three wire-level smoke tests +- `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 used by `TwinCATNativeNotificationTests` diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs new file mode 100644 index 0000000..65ebdc7 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCAT3SmokeTests.cs @@ -0,0 +1,135 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; + +namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests; + +/// +/// End-to-end smoke tests against a live TwinCAT 3 XAR runtime. Skipped via +/// when the VM isn't reachable / the AmsNetId +/// isn't set. Proves the driver's AMS route setup, ADS read/write, symbol browse, +/// and native AddDeviceNotification subscription all work on the wire — +/// coverage the FakeTwinCATClient-backed unit suite can only contract-test. +/// +/// +/// Required VM project state (see TwinCatProject/README.md): +/// +/// GVL GVL_Fixture with nCounter : DINT (seed 1234), +/// rSetpoint : REAL (scratch; smoke writes + reads), bFlag : BOOL +/// (seed TRUE). +/// PLC program MAIN that increments GVL_Fixture.nCounter +/// every cycle (so the native-notification test can observe monotonic changes +/// without writing). +/// +/// +[Collection("TwinCATXar")] +[Trait("Category", "Integration")] +[Trait("Simulator", "TwinCAT-XAR")] +public sealed class TwinCAT3SmokeTests(TwinCATXarFixture sim) +{ + [TwinCATFact] + public async Task Driver_reads_seeded_DINT_through_real_ADS() + { + if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); + + var options = BuildOptions(sim); + await using var drv = new TwinCATDriver(options, driverInstanceId: "tc3-smoke-read"); + await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); + + var snapshots = await drv.ReadAsync( + ["Counter"], TestContext.Current.CancellationToken); + + snapshots.Count.ShouldBe(1); + snapshots[0].StatusCode.ShouldBe(0u, + "ADS read against GVL_Fixture.nCounter must succeed end-to-end"); + // MAIN increments the counter every cycle, so the seed value (1234) is only the + // minimum we can assert — value grows monotonically. + Convert.ToInt32(snapshots[0].Value).ShouldBeGreaterThanOrEqualTo(1234); + } + + [TwinCATFact] + public async Task Driver_write_then_read_round_trip_on_scratch_REAL() + { + if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); + + var options = BuildOptions(sim); + await using var drv = new TwinCATDriver(options, driverInstanceId: "tc3-smoke-write"); + await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); + + const float probe = 42.5f; + var writeResults = await drv.WriteAsync( + [new WriteRequest("Setpoint", probe)], + TestContext.Current.CancellationToken); + writeResults.Count.ShouldBe(1); + writeResults[0].StatusCode.ShouldBe(0u); + + var readResults = await drv.ReadAsync( + ["Setpoint"], TestContext.Current.CancellationToken); + readResults.Count.ShouldBe(1); + readResults[0].StatusCode.ShouldBe(0u); + Convert.ToSingle(readResults[0].Value).ShouldBe(probe, tolerance: 0.001f); + } + + [TwinCATFact] + public async Task Driver_subscribe_receives_native_ADS_notifications_on_counter_changes() + { + if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); + + var options = BuildOptions(sim); + await using var drv = new TwinCATDriver(options, driverInstanceId: "tc3-smoke-sub"); + await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); + + var observed = new List(); + var gate = new SemaphoreSlim(0); + drv.OnDataChange += (_, e) => + { + lock (observed) observed.Add(e); + gate.Release(); + }; + + var handle = await drv.SubscribeAsync( + ["Counter"], TimeSpan.FromMilliseconds(250), + TestContext.Current.CancellationToken); + + // MAIN increments the counter every PLC cycle (default 10 ms task tick). + // Native ADS notifications fire on cycle boundaries so 3 s is generous for + // at least one OnDataChange to land. + var got = await gate.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken); + got.ShouldBeTrue("native ADS notification on GVL_Fixture.nCounter must fire within 3 s of subscribe"); + + int observedCount; + lock (observed) observedCount = observed.Count; + observedCount.ShouldBeGreaterThan(0); + + await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken); + } + + private static TwinCATDriverOptions BuildOptions(TwinCATXarFixture sim) => new() + { + Devices = [ + new TwinCATDeviceOptions( + HostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", + DeviceName: "XAR-VM"), + ], + Tags = [ + new TwinCATTagDefinition( + Name: "Counter", + DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", + SymbolPath: "GVL_Fixture.nCounter", + DataType: TwinCATDataType.DInt), + new TwinCATTagDefinition( + Name: "Setpoint", + DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", + SymbolPath: "GVL_Fixture.rSetpoint", + DataType: TwinCATDataType.Real, + Writable: true), + ], + UseNativeNotifications = true, + Timeout = TimeSpan.FromSeconds(5), + // Disable the probe loop — the smoke tests run their own reads; a background + // probe against GVL_Fixture.nCounter would race with them for the ADS client + // gate + inject flakiness unrelated to the code under test. + Probe = new TwinCATProbeOptions { Enabled = false }, + }; +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs new file mode 100644 index 0000000..5df1323 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATXarFixture.cs @@ -0,0 +1,136 @@ +using System.Net.Sockets; +using Xunit; +using Xunit.Sdk; + +namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests; + +/// +/// Reachability probe for a TwinCAT 3 XAR runtime on a Hyper-V VM or dedicated +/// Windows box. TCP-probes ADS port 48898 on the operator-supplied host. Tests +/// skip via / +/// when the runtime isn't reachable, so dotnet test on a fresh clone without +/// a TwinCAT VM stays green. Matches the +/// / +/// / +/// OpcPlcFixture / AbServerFixture patterns. +/// +/// +/// Why a VM, not a container: TwinCAT XAR bypasses the Windows +/// kernel scheduler to hit real-time PLC cycles. It can't run inside Docker, and +/// on bare metal it conflicts with Hyper-V / WSL 2 — that's why this repo's dev +/// environment puts XAR in a dedicated Hyper-V VM per +/// docs/v2/dev-environment.md §Integration host. The fixture treats the VM +/// as a black-box ADS endpoint reachable over TCP. +/// +/// License rotation: the free XAR trial license expires every 7 days. +/// When it lapses the runtime goes silent + the fixture's TCP probe fails; tests +/// skip with the reason message until the operator renews via +/// TcActivate.exe /reactivate (or buys a paid runtime). Intentionally surfaces +/// as a skip rather than a hang because "trial expired" is operator action, not a +/// test failure. +/// +/// Env var overrides: +/// +/// TWINCAT_TARGET_HOST — IP or hostname of the XAR VM (default +/// localhost, assumed to be unset on the average dev box + result in a +/// clean skip). +/// TWINCAT_TARGET_NETID — AMS NetId the tests address (e.g. +/// 5.23.91.23.1.1). Seeded on the target VM via TwinCAT System +/// Manager → Routes; the dev box's AmsNetId also needs a bilateral route +/// entry on the VM side. No sensible default — tests skip if unset. +/// TWINCAT_TARGET_PORT — ADS target port (default 851 = +/// TC3 PLC runtime 1). Set to 852 for runtime 2, etc. +/// +/// +public sealed class TwinCATXarFixture : IAsyncLifetime +{ + private const string HostEnvVar = "TWINCAT_TARGET_HOST"; + private const string NetIdEnvVar = "TWINCAT_TARGET_NETID"; + private const string PortEnvVar = "TWINCAT_TARGET_PORT"; + + /// ADS-over-TCP port on the XAR host. Not the PLC runtime port (that's + /// ). + public const int AdsTcpPort = 48898; + + /// TC3 PLC runtime 1. Override via . + public const int DefaultAmsPort = 851; + + public string TargetHost { get; } + public string? TargetNetId { get; } + public int AmsPort { get; } + public string? SkipReason { get; } + + public TwinCATXarFixture() + { + TargetHost = Environment.GetEnvironmentVariable(HostEnvVar) ?? "localhost"; + TargetNetId = Environment.GetEnvironmentVariable(NetIdEnvVar); + AmsPort = int.TryParse(Environment.GetEnvironmentVariable(PortEnvVar), out var p) + ? p : DefaultAmsPort; + + if (string.IsNullOrWhiteSpace(TargetNetId)) + { + SkipReason = $"TwinCAT XAR unreachable: {NetIdEnvVar} is not set. " + + $"Start the XAR VM + set {HostEnvVar}= and {NetIdEnvVar}=."; + return; + } + + if (!TcpProbe(TargetHost, AdsTcpPort, TimeSpan.FromSeconds(2))) + { + SkipReason = $"TwinCAT XAR at {TargetHost}:{AdsTcpPort} not reachable within 2 s. " + + $"Verify the XAR VM is running, its trial license hasn't expired " + + $"(run TcActivate.exe /reactivate on the VM), and {HostEnvVar}/{NetIdEnvVar} point at it."; + } + } + + public ValueTask InitializeAsync() => ValueTask.CompletedTask; + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + /// true when the XAR runtime is reachable + the AmsNetId is set. + /// Used by the skip attributes to avoid spinning up the fixture for every test + /// class. + public static bool IsRuntimeAvailable() + { + if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(NetIdEnvVar))) return false; + var host = Environment.GetEnvironmentVariable(HostEnvVar) ?? "localhost"; + return TcpProbe(host, AdsTcpPort, TimeSpan.FromMilliseconds(500)); + } + + private static bool TcpProbe(string host, int port, TimeSpan timeout) + { + try + { + using var client = new TcpClient(); + var task = client.ConnectAsync(host, port); + return task.Wait(timeout) && client.Connected; + } + catch { return false; } + } +} + +[Xunit.CollectionDefinition(Name)] +public sealed class TwinCATXarCollection : Xunit.ICollectionFixture +{ + public const string Name = "TwinCATXar"; +} + +/// [Fact]-equivalent gated on . +public sealed class TwinCATFactAttribute : FactAttribute +{ + public TwinCATFactAttribute() + { + if (!TwinCATXarFixture.IsRuntimeAvailable()) + Skip = "TwinCAT XAR not reachable. See docs/drivers/TwinCAT-Test-Fixture.md " + + "for setup; typical cause is the trial license expired or TWINCAT_TARGET_NETID is unset."; + } +} + +/// [Theory]-equivalent with the same gate as . +public sealed class TwinCATTheoryAttribute : TheoryAttribute +{ + public TwinCATTheoryAttribute() + { + if (!TwinCATXarFixture.IsRuntimeAvailable()) + Skip = "TwinCAT XAR not reachable. See docs/drivers/TwinCAT-Test-Fixture.md " + + "for setup; typical cause is the trial license expired or TWINCAT_TARGET_NETID is unset."; + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md new file mode 100644 index 0000000..1b019d4 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCatProject/README.md @@ -0,0 +1,145 @@ +# TwinCAT XAR fixture project + +This folder holds the TwinCAT 3 XAE project that the XAR VM runs for the +integration-tests suite (`tests/.../TwinCAT.IntegrationTests/*.cs`). + +**Status today**: stub. The `.tsproj` isn't committed yet; once the XAR +VM is up + a project with the required state exists, export via +File → Export + drop it here as `OtOpcUaTwinCatFixture.tsproj` + its +PLC `.library` / `.plcproj` companions. + +## Why `.tsproj`, not the binary bootproject + +TwinCAT ships two project forms: the XAE `.tsproj` (XML, source of +truth) and the compiled bootproject that the XAR runtime actually +loads. Ship the `.tsproj` because: + +- Text format — reviewable in PR diffs, diffable in git +- Rebuildable across TC3 engineering versions (the XAE tool rebuilds + the bootproject from `.tsproj` on "Activate Configuration") +- Doesn't carry per-install state (target AmsNetId, source licensing) + +Reconstruction workflow on the VM: + +1. Open TC3 XAE (Visual Studio shell) +2. File → Open → `OtOpcUaTwinCatFixture.tsproj` +3. Target system → the VM's AmsNetId (set in System Manager → Routes) +4. Build → Build Solution (produces the bootproject) +5. Activate Configuration → Run Mode (deploys to XAR + starts the + runtime) + +## Required project state + +The smoke tests in `TwinCAT3SmokeTests.cs` depend on this exact GVL + +PLC setup. Missing or renamed symbols surface as ADS `DeviceSymbolNotFound` +or wrong-type read failures, not silent skips. + +### Global Variable List: `GVL_Fixture` + +```st +VAR_GLOBAL + // Monotonically-increasing counter; MAIN increments each cycle. + // Seed value 1234 picked so the smoke test can assert ">= 1234" without + // synchronising with the initial cycle. + nCounter : DINT := 1234; + + // Scratch REAL for write-then-read round-trip test. Smoke test writes + // 42.5 + reads back. + rSetpoint : REAL := 0.0; + + // Readable boolean with seed value TRUE. Reserved for future + // expansion (e.g. discovery / symbol-browse tests). + bFlag : BOOL := TRUE; +END_VAR +``` + +### PLC program: `MAIN` + +```st +PROGRAM MAIN +VAR +END_VAR + +// One-line program: increment the fixture counter every cycle. +// The native-notification smoke test subscribes to GVL_Fixture.nCounter +// + observes the monotonic changes without a write path. +GVL_Fixture.nCounter := GVL_Fixture.nCounter + 1; +``` + +### Task + +- `PlcTask` — cyclic, 10 ms interval, priority 20 +- Assigned to `MAIN` + +### Runtime ID + +- TC3 PLC runtime 1 (AMS port `851`) — the smoke-test fixture defaults + to this. Use runtime 2 / port `852` only if the single runtime is + already taken by another project on the same VM. + +## XAR VM setup (one-time) + +Full bootstrap lives in `docs/v2/dev-environment.md`. The TwinCAT-specific +steps: + +1. **Create the Hyper-V VM** — Gen 2, Windows 10/11 64-bit, 4 GB RAM, + 2 CPUs. External virtual switch so the dev box can reach + `:48898`. +2. **Install TwinCAT 3 XAE + XAR** — free download from Beckhoff + (`www.beckhoff.com/en-en/products/automation/twincat/`). Activate the + 7-day trial on first boot. +3. **Note the VM's AmsNetId** — shown in the TwinCAT system tray icon → + Properties → AMS NetId (format like `5.23.91.23.1.1`). +4. **Configure bilateral ADS route**: + - On the VM: System Manager → Routes → Add Route → dev box's + AmsNetId + IP + - On the dev box: edit `%TC_INSTALLPATH%\Target\StaticRoutes.xml` (or + use the dev box's own TwinCAT System Manager if installed) to add + the VM's AmsNetId + IP +5. **Import this project** per the reconstruction workflow above. +6. **Hit Activate Configuration + Run Mode**. The runtime starts; the + system tray icon goes green; port `48898` is live. + +## License rotation + +The XAR trial expires every 7 days. When it lapses: + +1. The runtime goes silent (red tray icon, ADS port `48898` stops + responding to new connections). +2. Integration tests skip with the reason message pointing at this + folder's README. +3. Operator runs `C:\TwinCAT\3.1\Target\StartUp\TcActivate.exe /reactivate` + on the VM console (not RDP — the trial activation wants the + interactive-login desktop). + +Options to eliminate the manual step: + +- **Scheduled task** that runs the reactivate every 6 days at 02:00 — + documented in the Beckhoff forums as working for some TC3 builds, + not officially supported. +- **Paid runtime license** (~$1k one-time per runtime, per CPU) — kills + the rotation permanently, worth it if the integration host is + long-lived. + +## How to run the TwinCAT-tier tests + +On the dev box: + +```powershell +$env:TWINCAT_TARGET_HOST = '10.0.0.42' # replace with the VM IP +$env:TWINCAT_TARGET_NETID = '5.23.91.23.1.1' # replace with the VM AmsNetId +# $env:TWINCAT_TARGET_PORT = '852' # only if not using PLC runtime 1 +dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests +``` + +With any of those env vars unset, all three smoke tests skip cleanly via +`[TwinCATFact]`; unit suite (`TwinCAT.Tests`) runs unchanged. + +## See also + +- [`docs/drivers/TwinCAT-Test-Fixture.md`](../../../docs/drivers/TwinCAT-Test-Fixture.md) + — coverage map +- [`docs/v2/dev-environment.md`](../../../docs/v2/dev-environment.md) + §Integration host — VM + route + license-rotation notes +- Beckhoff Information System → TwinCAT 3 → Product overview + ADS + + PLC reference (licensed; internal link only) diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj new file mode 100644 index 0000000..cfb2824 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests.csproj @@ -0,0 +1,35 @@ + + + + net10.0 + enable + enable + false + true + ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + +