Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests/TwinCATHandleCachePerfTests.cs
2026-04-25 22:03:20 -04:00

108 lines
4.8 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.2 integration test — exercises the live <see cref="AdsTwinCATClient"/>
/// handle cache against a real XAR runtime. Reads 50 distinct symbols twice and
/// asserts the second pass issues zero new <c>CreateVariableHandleAsync</c> calls.
/// </summary>
/// <remarks>
/// Hooks into <c>AdsTwinCATClient.HandleCreateCount</c> + <c>HandleCacheCount</c> via
/// the <c>InternalsVisibleTo</c> bridge added in PR 2.1. The fixture's skip-reason is
/// surfaced through <see cref="TwinCATFactAttribute"/> so the test stays green on a
/// dev box without the XAR VM (and its expiring trial license).
/// </remarks>
[Collection("TwinCATXar")]
[Trait("Category", "Integration")]
[Trait("Simulator", "TwinCAT-XAR")]
public sealed class TwinCATHandleCachePerfTests(TwinCATXarFixture sim)
{
private const int TagCount = 50;
[TwinCATFact]
public async Task Driver_handle_cache_avoids_repeat_symbol_resolution()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var deviceAddress = $"ads://{sim.TargetNetId}:{sim.AmsPort}";
var tags = new TwinCATTagDefinition[TagCount];
var refs = new string[TagCount];
for (var i = 0; i < TagCount; i++)
{
// GVL_Perf.aTags is 1-based per IEC 61131-3 ARRAY declaration. The 1000-element
// perf array is shared with TwinCATSumCommandPerfTests; this test only touches
// the first 50 indices so it stays cheap on every CI run.
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] forces the per-tag (handle-cached) path rather
// than the bulk Sum-read path, which still flows through symbolic paths
// by the PR 2.2 deviation note.
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 },
};
// Factory wrapper to capture the live AdsTwinCATClient and expose its internal
// counters back up to the test. Driver-side code only sees ITwinCATClient so this
// doesn't leak the implementation type out of the test.
var capture = new CapturingFactory();
await using var drv = new TwinCATDriver(options, "tc3-handle-cache", capture);
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
// First pass: every symbol is a cache miss. After this pass HandleCreateCount
// should equal TagCount.
var firstResults = await drv.ReadAsync(refs, TestContext.Current.CancellationToken);
firstResults.Count.ShouldBe(TagCount);
firstResults.ShouldAllBe(s => s.StatusCode == TwinCATStatusMapper.Good);
capture.Client.ShouldNotBeNull("CapturingFactory should have produced exactly one AdsTwinCATClient");
var firstPassCreates = capture.Client!.HandleCreateCount;
capture.Client!.HandleCacheCount.ShouldBe(TagCount);
firstPassCreates.ShouldBe(TagCount);
// Second pass: every symbol is a cache hit. HandleCreateCount must not have moved.
var secondResults = await drv.ReadAsync(refs, TestContext.Current.CancellationToken);
secondResults.Count.ShouldBe(TagCount);
secondResults.ShouldAllBe(s => s.StatusCode == TwinCATStatusMapper.Good);
capture.Client!.HandleCreateCount.ShouldBe(
firstPassCreates,
$"Second pass over {TagCount} symbols should have created zero new handles, " +
$"but HandleCreateCount went {firstPassCreates} -> {capture.Client!.HandleCreateCount}.");
capture.Client!.HandleCacheCount.ShouldBe(TagCount);
}
/// <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 counters.
/// </summary>
private sealed class CapturingFactory : ITwinCATClientFactory
{
public AdsTwinCATClient? Client { get; private set; }
public ITwinCATClient Create()
{
var c = new AdsTwinCATClient();
Client ??= c; // first one wins — single-device test path.
return c;
}
}
}