Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64db828d71 | |||
| 1a9367b5de | |||
| 98e997b573 | |||
| 0e8d911fd8 |
@@ -881,6 +881,25 @@ returns a failure code that the gateway surfaces as `FailedPrecondition`.
|
||||
`AcknowledgeByGuid` maps the synthetic GUID back to its reference via an
|
||||
internal dictionary, then calls the same write path.
|
||||
|
||||
`SubtagAlarmConsumer.Subscribe` advises the ack-comment subtag alongside the
|
||||
observed ones (active/acked/priority). This is required: MXAccess rejects a
|
||||
write to an item that has been added but not advised with `E_INVALIDARG`
|
||||
("Value does not fall within the expected range"). Advising it at subscribe
|
||||
time makes it an active item so the later ack write succeeds — its value
|
||||
changes carry no transition (the state machine ignores unmapped addresses).
|
||||
|
||||
### Live validation
|
||||
|
||||
The subtag path was validated against live MXAccess on the dev rig
|
||||
(`DESKTOP-6JL3KKO`, Galaxy `DEV`, `TestMachine_001.TestAlarm001`):
|
||||
|
||||
- `….InAlarm` → `True` (Boolean), `….Acked` → `False` (Boolean),
|
||||
`….Priority` → `500` (Int32), `….AckMsg` → string — confirming the field
|
||||
names **and** the runtime reference shape `<Object>.<AlarmAttr>.<field>`
|
||||
with **no** intermediate alarm-condition segment.
|
||||
- `AcknowledgeByName` (AckMsg write) returned `0` once the ack-comment subtag
|
||||
was advised — confirming the ack-by-comment-write mechanism end to end.
|
||||
|
||||
### Fidelity limitations
|
||||
|
||||
The following fields are not available or have lower quality in subtag mode:
|
||||
|
||||
@@ -40,9 +40,9 @@ public sealed class SubtagAlarmConsumerTests
|
||||
return new SubtagAlarmConsumer(source, new[] { BuildTarget() });
|
||||
}
|
||||
|
||||
/// <summary>Verifies Subscribe advises the active, acked, and priority subtags but not the ack-comment subtag.</summary>
|
||||
/// <summary>Verifies Subscribe advises the active, acked, priority, and ack-comment subtags (the ack-comment is advised so it is an active, writable MXAccess item).</summary>
|
||||
[Fact]
|
||||
public void Subscribe_AdvisesActiveAndAckedSubtags()
|
||||
public void Subscribe_AdvisesAllSubtagsIncludingAckComment()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
@@ -52,7 +52,7 @@ public sealed class SubtagAlarmConsumerTests
|
||||
Assert.Contains(ActiveSubtag, source.Advised);
|
||||
Assert.Contains(AckedSubtag, source.Advised);
|
||||
Assert.Contains(PrioritySubtag, source.Advised);
|
||||
Assert.DoesNotContain(AckCommentSubtag, source.Advised);
|
||||
Assert.Contains(AckCommentSubtag, source.Advised);
|
||||
}
|
||||
|
||||
/// <summary>Verifies an active=true value change raises a degraded, GUID-stamped UNACK_ALM transition.</summary>
|
||||
|
||||
@@ -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,147 @@ public sealed class AlarmSubtagLiveSmokeTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// (<c><Object>.<AlarmAttr>.<field></c>) resolves with
|
||||
/// no intermediate alarm-condition segment — the last open item from the
|
||||
/// design. Runs only when <c>MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1</c>.
|
||||
/// Reports "inconclusive" (without failing) when no values are delivered,
|
||||
/// e.g. the Galaxy engine for the test object is not running.
|
||||
/// </summary>
|
||||
[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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Also persist to a file so the evidence survives shell-logger quirks
|
||||
// when run over SSH. Path overridable via PROBE_LOG.
|
||||
string logPath = Environment.GetEnvironmentVariable("PROBE_LOG")
|
||||
?? System.IO.Path.Combine(System.IO.Path.GetTempPath(), "alarm-subtag-probe.log");
|
||||
try { System.IO.File.WriteAllLines(logPath, lines); }
|
||||
catch (Exception writeEx) { output.WriteLine("probe-log write failed: " + writeEx.Message); }
|
||||
|
||||
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<AlarmSubtagTarget> watchList = new List<AlarmSubtagTarget> { 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<string, string> lastValues =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
source.ValueChanged += (_, c) =>
|
||||
{
|
||||
string v = c.Value == null
|
||||
? "<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));
|
||||
|
||||
@@ -78,10 +78,15 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advises every observable alarm subtag (active / acked / priority —
|
||||
/// the ack-comment subtag is a write-only target and is not advised)
|
||||
/// and begins listening for value changes. The <paramref name="subscription"/>
|
||||
/// expression is ignored: the subtag set is fixed by the watch list.
|
||||
/// Advises every alarm subtag (active / acked / priority, plus the
|
||||
/// ack-comment subtag) and begins listening for value changes. The
|
||||
/// ack-comment subtag is advised so it is an active MXAccess item by the
|
||||
/// time <see cref="AcknowledgeByName"/> writes it — MXAccess rejects a
|
||||
/// write to an added-but-not-advised item with E_INVALIDARG (confirmed
|
||||
/// against live MXAccess). Its value changes carry no transition (the
|
||||
/// state machine ignores addresses it does not map to active/acked).
|
||||
/// The <paramref name="subscription"/> expression is ignored: the subtag
|
||||
/// set is fixed by the watch list.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription expression (unused in subtag mode).</param>
|
||||
public void Subscribe(string subscription)
|
||||
@@ -97,6 +102,7 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
|
||||
AddIfNotEmpty(addresses, target.ActiveSubtag);
|
||||
AddIfNotEmpty(addresses, target.AckedSubtag);
|
||||
AddIfNotEmpty(addresses, target.PrioritySubtag);
|
||||
AddIfNotEmpty(addresses, target.AckCommentSubtag);
|
||||
}
|
||||
|
||||
source.Advise(addresses);
|
||||
|
||||
Reference in New Issue
Block a user