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.2 / issue #314 — wire-level coverage for the cycle-time / jitter / online-change /// diagnostics surfaced through the probe loop. Skipped via /// when the XAR VM isn't reachable. The four well-known system symbols /// (TwinCAT_SystemInfoVarList._AppInfo.OnlineChangeCnt, _AppInfo.AppName, /// _TaskInfo[1].CycleTime, _TaskInfo[1].LastExecTime) are always exported by /// a live TC3 PLC runtime — no extra fixture state required beyond the existing /// GVL_Fixture / MAIN setup documented in TwinCatProject/README.md. /// [Collection("TwinCATXar")] [Trait("Category", "Integration")] [Trait("Simulator", "TwinCAT-XAR")] public sealed class TwinCATDiagnosticsIntegrationTests(TwinCATXarFixture sim) { [TwinCATFact] public async Task Probe_loop_surfaces_cycle_time_and_online_change_count() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); // Probe interval kept tight (250 ms) so the test doesn't have to wait the default 5 s // between probe ticks. The four system symbols are scalar UDINTs / a STRING(80), so // the diagnostics sample completes well within the 250 ms budget on any healthy // runtime. var options = new TwinCATDriverOptions { Devices = [ new TwinCATDeviceOptions( HostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", DeviceName: "XAR-VM"), ], // No Tags — diagnostics doesn't need any user-declared symbols. Tags = [], UseNativeNotifications = false, Timeout = TimeSpan.FromSeconds(5), Probe = new TwinCATProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(250), }, }; await using var drv = new TwinCATDriver(options, driverInstanceId: "tc3-diag-probe"); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); // Wait up to 5 s for the probe loop to fire at least once + populate the snapshot. // 250 ms cycle × 20 attempts ≈ 5 s budget — generous enough for AMS handshake on a // freshly-restarted runtime. var hostAddress = $"ads://{sim.TargetNetId}:{sim.AmsPort}"; TwinCATDeviceDiagnostics? snap = null; for (var i = 0; i < 20 && snap is null; i++) { await Task.Delay(250, TestContext.Current.CancellationToken); snap = drv.GetDeviceDiagnostics(hostAddress); } snap.ShouldNotBeNull( "probe loop must populate TwinCATDeviceDiagnostics within ~5 s on a reachable XAR runtime"); snap.CycleTimeMs.ShouldBeGreaterThan(0, "TwinCAT_SystemInfoVarList._TaskInfo[1].CycleTime is non-zero on every running PLC task"); snap.OnlineChangeCnt.ShouldBeGreaterThanOrEqualTo(0u, "OnlineChangeCnt is a UDINT counter — non-negative by type, present on every TC3 runtime"); // AppName is best-effort but on a project-loaded runtime it should be non-empty. snap.AppName.ShouldNotBeNull("running PLC project always reports an AppName"); // The cross-driver DriverHealth.Diagnostics dictionary should also reflect the same // values so the driver-diagnostics RPC has a consistent surface. var dict = drv.GetHealth().DiagnosticsOrEmpty; dict["TwinCAT.CycleTimeMs"].ShouldBe(snap.CycleTimeMs); dict["TwinCAT.OnlineChangeCnt"].ShouldBe(snap.OnlineChangeCnt); } }