From 0e8d911fd8a490855b8150b2acbf7b9a2621f8b0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 11:14:12 -0400 Subject: [PATCH] test(alarms): live runtime-path resolution probe (LiveMxAccessFact) for alarm subtags --- .../Probes/AlarmSubtagLiveSmokeTests.cs | 133 ++++++++++++++++++ 1 file changed, 133 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 9fc9d88..43a1bae 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs @@ -34,6 +34,7 @@ using System.Runtime.InteropServices; using System.Threading; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Worker.MxAccess; +using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport; using Xunit.Abstractions; namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes; @@ -146,6 +147,138 @@ public sealed class AlarmSubtagLiveSmokeTests } } + /// + /// Runtime-path resolution probe. Unlike the full lifecycle test this + /// does NOT require an active alarm: it advises the four subtags and + /// observes the initial values MXAccess delivers on advise, then writes + /// the AckMsg subtag. It confirms the runtime item-reference path + /// (<Object>.<AlarmAttr>.<field>) resolves with + /// no intermediate alarm-condition segment — the last open item from the + /// design. Runs only when MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1. + /// Reports "inconclusive" (without failing) when no values are delivered, + /// e.g. the Galaxy engine for the test object is not running. + /// + [LiveMxAccessFact] + public void LiveProbe_AlarmSubtagsResolve_AndAckMsgWriteSucceeds() + { + Exception? threadException = null; + ManualResetEventSlim done = new ManualResetEventSlim(false); + Thread thread = new Thread(() => + { + try { RunProbe(); } + catch (Exception ex) { threadException = ex; } + finally { done.Set(); } + }); + thread.IsBackground = false; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + done.Wait(); + thread.Join(); + + output.WriteLine(string.Format("Captured {0} log line(s):", log.Count)); + string? line; + while (log.TryDequeue(out line)) + { + output.WriteLine(line); + } + + if (threadException != null) + { + throw threadException; + } + } + + private void RunProbe() + { + Log("=== Subtag runtime-path resolution probe ==="); + Log("AlarmFullReference: " + AlarmFullReference); + Log("Subtag addresses under test:"); + Log(" active = " + PlaceholderActiveSubtag); + Log(" acked = " + PlaceholderAckedSubtag); + Log(" ackMsg = " + PlaceholderAckCommentSubtag); + Log(" priority= " + PlaceholderPrioritySubtag); + + 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); + + // Hook the RAW source so every advised subtag's value-change is seen, + // including the initial value MXAccess delivers on advise (the state + // machine itself only emits on active/acked transitions). + Dictionary lastValues = + new Dictionary(StringComparer.OrdinalIgnoreCase); + source.ValueChanged += (_, c) => + { + string v = c.Value == null + ? "" + : string.Format("{0} ({1})", c.Value, c.Value.GetType().Name); + lastValues[c.ItemAddress] = v; + Log(string.Format("ValueChanged: {0} = {1}", c.ItemAddress, v)); + }; + + Log("Subscribe (advise InAlarm/Acked/Priority) ..."); + consumer.Subscribe(string.Format(@"\\{0}\Galaxy!{1}", Environment.MachineName, Group)); + Log("Subscribe returned OK."); + + // Pump the STA for ~6s to receive each advised subtag's initial value. + DateTime deadline = DateTime.UtcNow + TimeSpan.FromSeconds(6); + while (DateTime.UtcNow < deadline) + { + PumpMessages(); + Thread.Sleep(100); + } + + Log(string.Format("Distinct subtags that delivered a value: {0}", lastValues.Count)); + bool inAlarmResolved = lastValues.ContainsKey(PlaceholderActiveSubtag); + Log("InAlarm subtag delivered a value: " + inAlarmResolved); + + // Decisive write: AckMsg. Returns 0 only if the address resolves and is writable. + Log("AcknowledgeByName (writes AckMsg subtag) ..."); + int rc = consumer.AcknowledgeByName( + alarmName: AlarmTagName, + providerName: Provider, + groupName: Group, + ackComment: "subtag-resolution-probe", + ackOperatorName: Environment.UserName, + ackOperatorNode: Environment.MachineName, + ackOperatorDomain: Environment.UserDomainName ?? string.Empty, + ackOperatorFullName: Environment.UserName); + Log(string.Format("AcknowledgeByName(AckMsg write) -> rc={0}", rc)); + + DateTime settle = DateTime.UtcNow + TimeSpan.FromSeconds(2); + while (DateTime.UtcNow < settle) + { + PumpMessages(); + Thread.Sleep(100); + } + + if (lastValues.Count == 0) + { + Log("INCONCLUSIVE: no subtag values delivered within the window. The Galaxy " + + "engine hosting TestMachine_001 may be stopped, or the runtime path needs " + + "an intermediate segment. Re-run with the engine running. (Field names " + + "remain confirmed from the ZB AlarmExtension model.)"); + return; + } + + // Live data arrived — assert the in-alarm subtag resolved and the ack write succeeded. + Assert.True(inAlarmResolved, + "InAlarm subtag delivered no value while other subtags did — the runtime path for InAlarm may differ."); + Assert.Equal(0, rc); + Log("CONFIRMED: runtime subtag path resolves and the AckMsg write succeeded."); + } + private void RunSmoke() { Log(string.Format("AlarmFullReference: {0}", AlarmFullReference));