108 lines
4.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|