From 6226f2d864bffc66dd985a36ec16f0245eae610c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 11:30:46 -0400 Subject: [PATCH] test(alarms): self-driving live lifecycle test (drives alarm source true/false; raise/ack/clear) --- .../Probes/AlarmSubtagLiveSmokeTests.cs | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs index 41ff65a..a8b1ad5 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs @@ -288,6 +288,150 @@ public sealed class AlarmSubtagLiveSmokeTests Log("CONFIRMED: runtime subtag path resolves and the AckMsg write succeeded."); } + /// + /// Self-driving full lifecycle test. Drives the alarm by writing its + /// source attribute (TestMachine_001.TestAlarm001) false then + /// true then false, and asserts that + /// synthesizes a Raise (after true) and a Clear (after the final false), + /// with an acknowledge (AckMsg write) in between. No external alarm-flip + /// script required. Runs only when + /// MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1; has a side effect + /// (toggles the live test alarm). + /// + [LiveMxAccessFact] + public void LiveSelfDriven_RaiseAckClear_SynthesizesTransitions() + { + Exception? threadException = null; + ManualResetEventSlim done = new ManualResetEventSlim(false); + Thread thread = new Thread(() => + { + try { RunSelfDriven(); } + catch (Exception ex) { threadException = ex; } + finally { done.Set(); } + }); + thread.IsBackground = false; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + done.Wait(); + thread.Join(); + + List lines = new List(); + output.WriteLine(string.Format("Captured {0} log line(s):", log.Count)); + string? line; + while (log.TryDequeue(out line)) + { + output.WriteLine(line); + lines.Add(line); + } + + string logPath = Environment.GetEnvironmentVariable("PROBE_LOG") + ?? System.IO.Path.Combine(System.IO.Path.GetTempPath(), "alarm-subtag-selfdriven.log"); + try { System.IO.File.WriteAllLines(logPath, lines); } + catch (Exception writeEx) { output.WriteLine("self-driven-log write failed: " + writeEx.Message); } + + if (threadException != null) + { + throw threadException; + } + } + + private void RunSelfDriven() + { + // The discrete alarm's source attribute. Writing its boolean value + // drives the alarm: true -> in alarm, false -> normal. + const string AlarmSourceTag = "TestMachine_001.TestAlarm001"; + + Log("=== Self-driven Raise/Ack/Clear lifecycle ==="); + Log("AlarmFullReference: " + AlarmFullReference); + Log("Alarm source tag (driven): " + AlarmSourceTag); + + AlarmSubtagTarget target = new AlarmSubtagTarget + { + AlarmFullReference = AlarmFullReference, + SourceObjectReference = AlarmTagName, + ActiveSubtag = PlaceholderActiveSubtag, + AckedSubtag = PlaceholderAckedSubtag, + AckCommentSubtag = PlaceholderAckCommentSubtag, + PrioritySubtag = PlaceholderPrioritySubtag, + }; + List watchList = new List { target }; + + MxAccessComObjectFactory factory = new MxAccessComObjectFactory(); + LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null); + using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, watchList); + + ConcurrentQueue transitions = + new ConcurrentQueue(); + consumer.AlarmTransitionEmitted += (_, e) => + { + Log("Transition emitted: " + DescribeTransition(e)); + transitions.Enqueue(e); + }; + + // Advise the alarm subtags (InAlarm/Acked/Priority/AckMsg). + consumer.Subscribe(string.Format(@"\\{0}\Galaxy!{1}", Environment.MachineName, Group)); + // Also advise the source attribute so it is an active, writable item + // (MXAccess rejects writes to added-but-not-advised items). + source.Advise(new List { AlarmSourceTag }); + Log("Subscribed + advised source. Pumping to activate items ..."); + PumpFor(TimeSpan.FromSeconds(7)); + + // Baseline OFF: drive the source false and drain whatever transitions + // result (including any initial Raise if the alarm started active). + Log("Driving source = false (baseline OFF) ..."); + source.Write(AlarmSourceTag, false); + PumpFor(TimeSpan.FromSeconds(5)); + int drained = 0; + MxAlarmTransitionEvent? ignored; + while (transitions.TryDequeue(out ignored)) { drained++; } + Log(string.Format("Drained {0} baseline transition(s).", drained)); + + // RAISE: drive the source true. + Log("Driving source = true (RAISE) ..."); + source.Write(AlarmSourceTag, true); + MxAlarmTransitionEvent? raiseEvent = WaitForTransitionKind( + transitions, AlarmTransitionKind.Raise, RaiseWaitTimeout, "Raise"); + Assert.NotNull(raiseEvent); + Assert.True(raiseEvent!.Record.Degraded, "Subtag-synthesized records must have Degraded=true."); + Assert.NotEqual(Guid.Empty, raiseEvent.Record.AlarmGuid); + Assert.Equal(MxAlarmStateKind.UnackAlm, raiseEvent.Record.State); + Log(string.Format("RAISE confirmed: Guid={0} State={1}", raiseEvent.Record.AlarmGuid, raiseEvent.Record.State)); + + // ACK: write the ack-comment subtag. + Log("AcknowledgeByName (writes AckMsg) ..."); + int rc = consumer.AcknowledgeByName( + alarmName: AlarmTagName, + providerName: Provider, + groupName: Group, + ackComment: "self-driven-smoke ack", + ackOperatorName: Environment.UserName, + ackOperatorNode: Environment.MachineName, + ackOperatorDomain: Environment.UserDomainName ?? string.Empty, + ackOperatorFullName: Environment.UserName); + Log(string.Format("AcknowledgeByName -> rc={0}", rc)); + Assert.Equal(0, rc); + + // CLEAR: drive the source false. + Log("Driving source = false (CLEAR) ..."); + source.Write(AlarmSourceTag, false); + MxAlarmTransitionEvent? clearEvent = WaitForTransitionKind( + transitions, AlarmTransitionKind.Clear, ClearWaitTimeout, "Clear"); + Assert.NotNull(clearEvent); + Log(string.Format("CLEAR confirmed: State={0}", clearEvent!.Record.State)); + + Log("Self-driven lifecycle complete: Raise -> Ack -> Clear all synthesized."); + } + + private void PumpFor(TimeSpan duration) + { + DateTime deadline = DateTime.UtcNow + duration; + while (DateTime.UtcNow < deadline) + { + PumpMessages(); + Thread.Sleep(100); + } + } + private void RunSmoke() { Log(string.Format("AlarmFullReference: {0}", AlarmFullReference));