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; /// /// PR 2.2 integration test — exercises the live /// handle cache against a real XAR runtime. Reads 50 distinct symbols twice and /// asserts the second pass issues zero new CreateVariableHandleAsync calls. /// /// /// Hooks into AdsTwinCATClient.HandleCreateCount + HandleCacheCount via /// the InternalsVisibleTo bridge added in PR 2.1. The fixture's skip-reason is /// surfaced through so the test stays green on a /// dev box without the XAR VM (and its expiring trial license). /// [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); } /// /// Routes through the production /// and snapshots the produced client so the /// test can read its internal handle-cache counters. /// 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; } } }