@@ -0,0 +1,126 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// PR 2.3 integration test — exercises the proactive Symbol-Version invalidation
|
||||
/// listener against a real XAR runtime. Reads 5 symbols to populate the handle
|
||||
/// cache, polls until the operator triggers an online change in TwinCAT XAE
|
||||
/// (which bumps the PLC symbol-version counter), then asserts the cache wiped
|
||||
/// and a follow-up read recreates handles.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Manual gating</b>: this test requires an operator to trigger the
|
||||
/// online change from XAE while the test is polling — there's no programmatic ADS
|
||||
/// surface for "edit + activate". Gated on env <c>TWINCAT_MANUAL_ONLINE_CHANGE=1</c>
|
||||
/// so the default integration pass skips it; operators flip it on when running
|
||||
/// the scenario manually per <c>TwinCatProject/README.md §Online-change test scenario</c>.</para>
|
||||
///
|
||||
/// <para><b>How to run</b>: set <c>TWINCAT_TARGET_HOST</c> + <c>TWINCAT_TARGET_NETID</c>
|
||||
/// + <c>TWINCAT_MANUAL_ONLINE_CHANGE=1</c>, kick off the test, then within ~60 s
|
||||
/// open the project in XAE → add a dummy variable to <c>GVL_Perf</c> → Login +
|
||||
/// Activate Configuration. The PLC re-initialises, the symbol-version counter
|
||||
/// bumps, the listener fires, and the test passes.</para>
|
||||
/// </remarks>
|
||||
[Collection("TwinCATXar")]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Simulator", "TwinCAT-XAR")]
|
||||
public sealed class TwinCATSymbolVersionTests(TwinCATXarFixture sim)
|
||||
{
|
||||
private const string ManualOnlineChangeEnv = "TWINCAT_MANUAL_ONLINE_CHANGE";
|
||||
|
||||
[TwinCATFact]
|
||||
public async Task Driver_invalidates_handle_cache_on_symbol_version_bump()
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
if (Environment.GetEnvironmentVariable(ManualOnlineChangeEnv) != "1")
|
||||
Assert.Skip(
|
||||
$"Manual online-change scenario disabled. Set {ManualOnlineChangeEnv}=1 + " +
|
||||
"follow TwinCatProject/README.md §Online-change test scenario to run.");
|
||||
|
||||
var deviceAddress = $"ads://{sim.TargetNetId}:{sim.AmsPort}";
|
||||
|
||||
// Use 5 symbols out of GVL_Perf.aTags — same fixture state as the perf tests.
|
||||
// ArrayDimensions = [1] forces the per-tag (handle-cached) path so the test
|
||||
// actually exercises the cache the listener is meant to invalidate.
|
||||
var tags = new TwinCATTagDefinition[5];
|
||||
var refs = new string[5];
|
||||
for (var i = 0; i < tags.Length; i++)
|
||||
{
|
||||
var name = $"Perf{i + 1}";
|
||||
refs[i] = name;
|
||||
tags[i] = new TwinCATTagDefinition(
|
||||
Name: name,
|
||||
DeviceHostAddress: deviceAddress,
|
||||
SymbolPath: $"GVL_Perf.aTags[{i + 1}]",
|
||||
DataType: TwinCATDataType.DInt,
|
||||
ArrayDimensions: [1]);
|
||||
}
|
||||
|
||||
var options = new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(deviceAddress, "XAR-VM")],
|
||||
Tags = tags,
|
||||
UseNativeNotifications = false,
|
||||
Timeout = TimeSpan.FromSeconds(15),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
};
|
||||
|
||||
var capture = new CapturingFactory();
|
||||
await using var drv = new TwinCATDriver(options, "tc3-symbol-version", capture);
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
// First pass populates the cache.
|
||||
var firstResults = await drv.ReadAsync(refs, TestContext.Current.CancellationToken);
|
||||
firstResults.ShouldAllBe(s => s.StatusCode == TwinCATStatusMapper.Good);
|
||||
capture.Client.ShouldNotBeNull();
|
||||
var initialCreates = capture.Client!.HandleCreateCount;
|
||||
initialCreates.ShouldBe(tags.Length);
|
||||
capture.Client!.HandleCacheCount.ShouldBe(tags.Length);
|
||||
var initialBumps = capture.Client!.SymbolVersionBumps;
|
||||
|
||||
// Wait for an operator-triggered online change. Poll the bump counter at 500 ms
|
||||
// intervals up to 60 s — long enough to cover the manual XAE workflow (open
|
||||
// project → add var → Login → Activate). When the counter ticks, the listener
|
||||
// has fired + wiped the cache.
|
||||
var deadline = DateTime.UtcNow.AddSeconds(60);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
if (capture.Client!.SymbolVersionBumps > initialBumps) break;
|
||||
await Task.Delay(500, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
capture.Client!.SymbolVersionBumps.ShouldBeGreaterThan(initialBumps,
|
||||
"expected operator to trigger an online change within 60 s; " +
|
||||
"see TwinCatProject/README.md §Online-change test scenario");
|
||||
capture.Client!.HandleCacheCount.ShouldBe(0,
|
||||
"Symbol-Version listener should have wiped the handle cache on bump");
|
||||
|
||||
// Subsequent reads recreate handles — total CreateVariableHandle count grows.
|
||||
var secondResults = await drv.ReadAsync(refs, TestContext.Current.CancellationToken);
|
||||
secondResults.ShouldAllBe(s => s.StatusCode == TwinCATStatusMapper.Good);
|
||||
capture.Client!.HandleCreateCount.ShouldBe(initialCreates + tags.Length,
|
||||
"post-bump reads must recreate every handle from cold");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes <see cref="ITwinCATClientFactory.Create"/> through the production
|
||||
/// <see cref="AdsTwinCATClientFactory"/> and snapshots the produced client so the
|
||||
/// test can read its internal handle-cache + symbol-version counters. Mirror of
|
||||
/// the <c>CapturingFactory</c> in <see cref="TwinCATHandleCachePerfTests"/>.
|
||||
/// </summary>
|
||||
private sealed class CapturingFactory : ITwinCATClientFactory
|
||||
{
|
||||
public AdsTwinCATClient? Client { get; private set; }
|
||||
|
||||
public ITwinCATClient Create()
|
||||
{
|
||||
var c = new AdsTwinCATClient();
|
||||
Client ??= c;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,47 @@ Options to eliminate the manual step:
|
||||
the rotation permanently, worth it if the integration host is
|
||||
long-lived.
|
||||
|
||||
## Online-change test scenario
|
||||
|
||||
PR 2.3 (proactive Symbol-Version invalidation listener) ships an
|
||||
operator-gated integration test
|
||||
(`TwinCATSymbolVersionTests.Driver_invalidates_handle_cache_on_symbol_version_bump`)
|
||||
that verifies `AdsTwinCATClient`'s `AdsSymbolVersionChanged` listener
|
||||
wipes the handle cache when the PLC re-initialises after an online
|
||||
change. The test polls for up to 60 s waiting for the operator to
|
||||
trigger the change from XAE.
|
||||
|
||||
The fixture state (`GVL_Perf` + `aTags[1..1000]`) is the same one used by
|
||||
the Sum-read perf test — no new project state required.
|
||||
|
||||
### Manual workflow
|
||||
|
||||
With the XAR runtime live + the test process polling:
|
||||
|
||||
1. **Open the project in XAE** on the dev box (or wherever XAE runs).
|
||||
2. **Add a dummy variable to `GVL_Perf`** — any new declaration triggers
|
||||
a symbol-table rebuild. Example: append
|
||||
`bSymVerProbe : BOOL := FALSE;` to the GVL.
|
||||
3. **Login** (`Ctrl+F8`) — XAE prompts to load the change.
|
||||
4. **Activate Configuration** (Yellow-arrow button, or `TwinCAT → Activate Configuration`).
|
||||
The runtime re-initialises; the symbol-version counter increments;
|
||||
`AdsTwinCATClient.OnAdsSymbolVersionChanged` fires; the handle cache
|
||||
wipes; the test polls observe `SymbolVersionBumps > 0` + asserts the
|
||||
post-bump read recreates handles.
|
||||
|
||||
The test skips by default — opt in by setting
|
||||
`TWINCAT_MANUAL_ONLINE_CHANGE=1` alongside the standard
|
||||
`TWINCAT_TARGET_HOST` / `TWINCAT_TARGET_NETID` env vars before kicking
|
||||
off the test run.
|
||||
|
||||
```powershell
|
||||
$env:TWINCAT_TARGET_HOST = '10.0.0.42'
|
||||
$env:TWINCAT_TARGET_NETID = '5.23.91.23.1.1'
|
||||
$env:TWINCAT_MANUAL_ONLINE_CHANGE = '1'
|
||||
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests `
|
||||
--filter "FullyQualifiedName~TwinCATSymbolVersionTests"
|
||||
```
|
||||
|
||||
## How to run the TwinCAT-tier tests
|
||||
|
||||
On the dev box:
|
||||
|
||||
Reference in New Issue
Block a user