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 3.1 (#313) — per-tag MaxDelay coalescing observed on the wire. Subscribes /// to GVL_Fixture.nCounter (the cycle-incrementing fixture counter) with a /// 500 ms MaxDelayMs; over a 1 s observation window the runtime should batch /// the per-cycle changes into ≤ 3 callbacks rather than one-per-PLC-cycle (~100 with a /// 10 ms task) which is what MaxDelay=0 would deliver. /// /// /// Skipped via when the XAR VM isn't reachable /// / env vars aren't set. Build-only proof in CI; full runtime cover happens on the /// XAR-equipped lab box. /// /// The 1 s window + ≤ 3 events tolerance is generous on purpose — the runtime's /// coalescer can still fire mid-window when the cycle accumulates a value-change burst, /// and the test mostly cares that coalescing happens at all (i.e. it's /// dramatically less than the no-coalescing baseline). /// [Collection("TwinCATXar")] [Trait("Category", "Integration")] [Trait("Simulator", "TwinCAT-XAR")] public sealed class TwinCATMaxDelayTests(TwinCATXarFixture sim) { [TwinCATFact] public async Task Driver_coalesces_notifications_at_max_delay() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); var options = BuildOptions(sim); await using var drv = new TwinCATDriver(options, driverInstanceId: "tc3-maxdelay"); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); var observed = new List(); drv.OnDataChange += (_, _) => { lock (observed) observed.Add(DateTime.UtcNow); }; var handle = await drv.SubscribeAsync( ["Counter"], TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); // Observe for 1 s. With MAIN incrementing nCounter every 10 ms PLC cycle, the // un-coalesced rate would be ~100 events; with MaxDelay=500 ms we expect ≤ 3 // (initial fire + at most two coalesce-window flushes). await Task.Delay(TimeSpan.FromMilliseconds(1000), TestContext.Current.CancellationToken); await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken); int count; lock (observed) count = observed.Count; count.ShouldBeLessThanOrEqualTo( 3, $"MaxDelayMs=500 must coalesce nCounter changes — observed {count} events in 1 s"); } private static TwinCATDriverOptions BuildOptions(TwinCATXarFixture sim) => new() { Devices = [ new TwinCATDeviceOptions( HostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", DeviceName: "XAR-VM"), ], Tags = [ new TwinCATTagDefinition( Name: "Counter", DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", SymbolPath: "GVL_Fixture.nCounter", DataType: TwinCATDataType.DInt, MaxDelayMs: 500), ], UseNativeNotifications = true, Timeout = TimeSpan.FromSeconds(5), // Disable probe so it doesn't race with the 1 s observation window. Probe = new TwinCATProbeOptions { Enabled = false }, }; }