Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATSymbolVersionTests.cs
2026-04-25 22:16:05 -04:00

127 lines
5.9 KiB
C#

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;
}
}
}