From 1681c933a07258aeed187a38085dcdc2bcd183fc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 11:46:35 -0400 Subject: [PATCH] test(alarms): drivable-alarm discovery scan across candidate alarm tags --- .../Probes/AlarmSubtagLiveSmokeTests.cs | 91 +++++++++++++++++++ 1 file changed, 91 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 19346a4..74ab20f 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs @@ -335,6 +335,97 @@ public sealed class AlarmSubtagLiveSmokeTests } } + /// + /// Discovery run: scans candidate alarm attributes for one whose value + /// actually responds to an external write (so it can drive a full + /// Raise/Clear lifecycle). Logs each candidate's before/after value and + /// InAlarm; does not assert. Runs only with + /// MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1. Side effect: flips any + /// writable candidate. + /// + [LiveMxAccessFact] + public void LiveProbe_FindDrivableAlarm() + { + Exception? threadException = null; + ManualResetEventSlim done = new ManualResetEventSlim(false); + Thread thread = new Thread(() => + { + try { RunFindDrivable(); } + 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(); + 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-find-drivable.log"); + try { System.IO.File.WriteAllLines(logPath, lines); } catch { /* best effort */ } + if (threadException != null) { throw threadException; } + } + + private void RunFindDrivable() + { + string[] candidates = new string[] + { + "TestMachine_001.TestAlarm002", + "TestMachine_001.TestAlarm003", + "TestMachine_002.TestAlarm001", + "TestMachine_003.TestAlarm001", + }; + + Log("=== Find a drivable alarm ==="); + MxAccessComObjectFactory factory = new MxAccessComObjectFactory(); + using LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null); + + Dictionary latest = + new Dictionary(StringComparer.OrdinalIgnoreCase); + source.ValueChanged += (_, c) => + { + latest[c.ItemAddress] = c.Value; + Log(string.Format("raw: {0} = {1}", c.ItemAddress, c.Value ?? "")); + }; + + List toAdvise = new List(); + foreach (string cand in candidates) + { + toAdvise.Add(cand); + toAdvise.Add(cand + ".InAlarm"); + } + source.Advise(toAdvise); + Log("Advised candidates. Pumping ..."); + PumpFor(TimeSpan.FromSeconds(7)); + + foreach (string cand in candidates) + { + object? before; + latest.TryGetValue(cand, out before); + bool beforeBool = before is bool bb && bb; + Log(string.Format("[{0}] before: value={1} InAlarm={2}", + cand, before ?? "", latest.ContainsKey(cand + ".InAlarm") ? latest[cand + ".InAlarm"] : "")); + + Log(string.Format("[{0}] writing {1} ...", cand, !beforeBool)); + try { source.Write(cand, !beforeBool); } + catch (Exception ex) { Log(string.Format("[{0}] write threw: {1}", cand, ex.Message)); continue; } + + PumpFor(TimeSpan.FromSeconds(5)); + object? after; + latest.TryGetValue(cand, out after); + bool changed = !Equals(before, after); + Log(string.Format("[{0}] after: value={1} InAlarm={2} -> {3}", + cand, after ?? "", + latest.ContainsKey(cand + ".InAlarm") ? latest[cand + ".InAlarm"] : "", + changed ? "DRIVABLE (value changed)" : "not drivable (value held)")); + } + + Log("=== Find-drivable scan complete ==="); + } + private void RunSelfDriven() { // The discrete alarm's source attribute. Writing its boolean value