test(alarms): self-driving live lifecycle test (drives alarm source true/false; raise/ack/clear)
This commit is contained in:
@@ -288,6 +288,150 @@ public sealed class AlarmSubtagLiveSmokeTests
|
||||
Log("CONFIRMED: runtime subtag path resolves and the AckMsg write succeeded.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Self-driving full lifecycle test. Drives the alarm by writing its
|
||||
/// source attribute (<c>TestMachine_001.TestAlarm001</c>) false then
|
||||
/// true then false, and asserts that <see cref="SubtagAlarmConsumer"/>
|
||||
/// 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
|
||||
/// <c>MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1</c>; has a side effect
|
||||
/// (toggles the live test alarm).
|
||||
/// </summary>
|
||||
[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<string> lines = new List<string>();
|
||||
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<AlarmSubtagTarget> watchList = new List<AlarmSubtagTarget> { target };
|
||||
|
||||
MxAccessComObjectFactory factory = new MxAccessComObjectFactory();
|
||||
LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null);
|
||||
using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, watchList);
|
||||
|
||||
ConcurrentQueue<MxAlarmTransitionEvent> transitions =
|
||||
new ConcurrentQueue<MxAlarmTransitionEvent>();
|
||||
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<string> { 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));
|
||||
|
||||
Reference in New Issue
Block a user