using System.Collections.Concurrent; 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 5.1 / #316 — end-to-end alarm-integration scaffold against a live TwinCAT 3 XAR /// runtime. Skipped via when the VM isn't reachable. /// Proves the driver's bridge surfaces TC3 EventLogger events /// when the PLC's FB_AlarmHarness calls FB_TcLogEvent. /// /// /// Required VM project state (see TwinCatProject/README.md /// §"Alarm scenarios"): /// /// GVL GVL_Alarms with bTriggerEvent : BOOL + bAcked : BOOL. /// A test harness flips bTriggerEvent from FALSE to TRUE via the /// driver's WriteAsync path; FB_AlarmHarness sees the rising edge + /// calls FB_TcLogEvent on the PLC side. /// FB FB_AlarmHarness wired into MAIN that calls FB_TcLogEvent /// with a configured event class GUID + severity + source string when /// GVL_Alarms.bTriggerEvent rises. /// /// Decode caveat — Beckhoff doesn't ship a managed wrapper for /// TcEventLogger in Beckhoff.TwinCAT.Ads v6, so the production /// gate is best-effort and several event fields may surface as "Unknown". /// This test asserts the bridge fires + the event has non-empty content; field-level /// decode tightening lands on a follow-up PR (see /// docs/v3/twincat-eventlogger-spike.md). /// [Collection("TwinCATXar")] [Trait("Category", "Integration")] [Trait("Simulator", "TwinCAT-XAR")] public sealed class TwinCATAlarmIntegrationTests(TwinCATXarFixture sim) { [TwinCATFact] public async Task Driver_raises_alarm_event_when_PLC_logs_event() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); // Fixture-side state is documented in TwinCatProject/README.md §"Alarm scenarios". // The harness is currently a build-only placeholder — once the GVL + FB_AlarmHarness // ship, replace the Skip below with the live-trigger flow: // 1. Init driver with EnableAlarms=true + GVL_Alarms.bTriggerEvent declared as a // writable BOOL tag. // 2. SubscribeAlarmsAsync([], ct). // 3. WriteAsync to flip bTriggerEvent from FALSE to TRUE — the PLC's // FB_AlarmHarness sees the rising edge + calls FB_TcLogEvent. // 4. Assert OnAlarmEvent fires within ~5s with a non-empty Source + Message. Assert.Skip( "PR 5.1 / #316 — alarm-integration build-only scaffold. The GVL_Alarms + " + "FB_AlarmHarness fixture hasn't been authored on the XAR project yet (build-time " + "stubs ship under TwinCatProject/PLC; live trigger lands once the XAR project " + "imports them). Until then this test self-skips even when the runtime is up."); await Task.CompletedTask; } /// /// Build the AMS options with on so /// the driver instantiates the alarm source. Mirrors the smoke-test option builder /// but flips the alarm gate on; once the live fixture lands, the test body above /// calls this helper directly. /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Style", "IDE0051", Justification = "Used by the live test body once the fixture ships.")] private static TwinCATDriverOptions BuildAlarmOptions(TwinCATXarFixture sim) => new() { Devices = [new TwinCATDeviceOptions( HostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", DeviceName: $"xar-{sim.TargetNetId}:{sim.AmsPort}")], Tags = [ // bTriggerEvent rises FALSE→TRUE to fire the PLC-side LogEvent call. new TwinCATTagDefinition( Name: "AlarmTrigger", DeviceHostAddress: $"ads://{sim.TargetNetId}:{sim.AmsPort}", SymbolPath: "GVL_Alarms.bTriggerEvent", DataType: TwinCATDataType.Bool, Writable: true), ], Probe = new TwinCATProbeOptions { Enabled = false }, EnableAlarms = true, }; /// /// Helper kept for parity with the live test body once the fixture ships — collects /// off the driver into a queue caller can /// drain after the trigger flip. /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Style", "IDE0051", Justification = "Used by the live test body once the fixture ships.")] private static ConcurrentQueue WireAlarmCollector(IAlarmSource src) { var q = new ConcurrentQueue(); src.OnAlarmEvent += (_, e) => q.Enqueue(e); return q; } }