@@ -0,0 +1,107 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user