Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/MxAccessSeverityMapperTests.cs
T
Joseph Doherty b61f3e0560
v2-ci / unit-tests (push) Has been cancelled
fix(galaxy): AVEVA priority 1 is the most urgent: Galaxy alarm Severity follows AVEVA's priority bands, live and history alike (#887)
GalaxyAlarmPriority, the one table live Galaxy alarms (MxAccessSeverityMapper)
and AVEVA A&E history (EventMapper, #886) map the AVEVA priority through, ranked
a higher number as more severe. AVEVA ranks priority 1 as the most urgent, so a
priority-1 alarm read 250 and a priority-999 system event 1000.

It now uses AVEVA's default priority ranges, most urgent first:

  AVEVA priority  AVEVA severity  bucket    OPC UA Severity
  1-250           1 Critical      Critical  1000
  251-500         2 High          High       750
  501-750         3 Medium        Medium     500
  751-999         4 Low           Low        250
  (history only)  0 not an alarm  -            1

Out-of-range priorities clamp (<= 0 Critical, > 999 Low). Evidence, vd03
Runtime.dbo.Events read-only: priority 1/100 alarms carry AVEVA severity 1,
400/500 severity 2, 600/700 severity 3, and every priority-999 System.* /
User.* row severity 0 with IsAlarm 0.

- Live and history now use the same number. GalaxyDriver passes the table's
  exact value as AlarmEventArgs.OpcUaSeverity. Before, it passed only the bucket,
  which NativeAlarmProjector re-mapped through its generic 200/500/700/900
  ladder, so live and history disagreed even before the direction fix.
- History: a row whose AVEVA severity is 0 (System.Start/Stop/Deploy,
  User.Write...) is not an alarm and reads Severity 1, below every alarm band.
  For alarms AVEVA's 1-4 severity is still never the value: the live feed
  carries only the priority, so it cannot use it, and history must agree.
- Docs: docs/Historian.md gains "Galaxy alarm priority -> Severity
  (lmxopcua#887)"; docs/AlarmTracking.md updated.
- Live re-run of the #801/#886 harness against vd03 HistorianGateway
  ea9f21c (read-only, network-only): 224 of 233 cases identical to the #886
  run, the 9 others differ only in Severity (priority-700 alarm rows 750 ->
  500, 19 system/user events 1000 -> 1). Record: aveva-history-801.md
  "#887 re-run", report aveva-gate-sdk2-887.json.gz.

Client-visible: every OPC UA client that ranks or filters Galaxy alarms by
Severity sees the order reversed (live and HistoryReadEvents). ScadaBridge has
no code that depends on the old values; its own mixed severity directions are
ScadaBridge#129.

Tests updated because they pinned the inverted table:
- Driver.Galaxy.Tests MxAccessSeverityMapperTests (rewritten: edges 1/250/251/
  500/501/750/751/999, 0/-100/int.MinValue/1000/int.MaxValue clamps,
  monotonic-decreasing sweep, equals GalaxyAlarmPriority for -2..1001)
- Driver.Galaxy.Tests GatewayGalaxyAlarmFeedTests (750 and 600 -> Medium)
- Driver.Historian.Gateway.Tests EventMapperTests (band table, priority 999
  -> 250, 1 -> 1000; system events -> 1)
- OpcUaServer.IntegrationTests GalaxyEquipmentEventHistoryWireTests (700 ->
  500, 100 -> 1000, System.Stop -> 1; the fake now stores AVEVA's real 0-4
  severity)

New tests:
- Runtime.Tests NativeAlarmRestatementEndToEndTests
  .Galaxy_live_condition_severity_is_the_AVEVA_priority_band (8 rows: the real
  feed -> real GalaxyDriver -> host -> projector; the same rows EventMapperTests
  pins on the history path)
- EventMapperTests.Aveva_severity_zero_marks_an_event_that_is_not_an_alarm
  (5 rows, incl. the priority-999 alarm positive control)
- GalaxyDriverAlarmSourceTests: OpcUaSeverity passes through

Mutation check, 8 mutants, all killed: the pre-#887 inverted table; the
driver dropping OpcUaSeverity; no not-an-alarm rule; each of the three band
edges off by one; history ignoring priority; a missing AVEVA severity read
as 0.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 12:14:49 -05:00

67 lines
3.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
/// <summary>
/// Pins the AVEVA priority → (AlarmSeverity, OPC UA numeric) table the live Galaxy alarm feed uses (lmxopcua#887).
/// mxaccessgw sends the AVEVA priority (1 = most urgent … 999 = least) as the alarm's raw severity; the table is
/// AVEVA's default four ranges, most urgent first.
/// </summary>
public sealed class MxAccessSeverityMapperTests
{
/// <summary>Verifies the band table at every edge, plus the out-of-range clamps.</summary>
/// <param name="rawPriority">The raw value the gateway sends: the AVEVA priority.</param>
/// <param name="expectedBucket">The expected AlarmSeverity bucket.</param>
/// <param name="expectedOpcUaSeverity">The expected OPC UA numeric severity value.</param>
[Theory]
[InlineData(1, AlarmSeverity.Critical, 1000)] // AVEVA Critical 1–250
[InlineData(250, AlarmSeverity.Critical, 1000)]
[InlineData(251, AlarmSeverity.High, 750)] // AVEVA High 251–500
[InlineData(500, AlarmSeverity.High, 750)]
[InlineData(501, AlarmSeverity.Medium, 500)] // AVEVA Medium 501–750
[InlineData(750, AlarmSeverity.Medium, 500)]
[InlineData(751, AlarmSeverity.Low, 250)] // AVEVA Low 751–999
[InlineData(999, AlarmSeverity.Low, 250)]
[InlineData(0, AlarmSeverity.Critical, 1000)] // below range: clamped to priority 1
[InlineData(-100, AlarmSeverity.Critical, 1000)]
[InlineData(int.MinValue, AlarmSeverity.Critical, 1000)]
[InlineData(1000, AlarmSeverity.Low, 250)] // above range: clamped to priority 999
[InlineData(int.MaxValue, AlarmSeverity.Low, 250)]
public void Map_assigns_the_AVEVA_priority_band(int rawPriority, AlarmSeverity expectedBucket, int expectedOpcUaSeverity)
{
var (bucket, opcUa) = MxAccessSeverityMapper.Map(rawPriority);
bucket.ShouldBe(expectedBucket);
opcUa.ShouldBe(expectedOpcUaSeverity);
}
/// <summary>
/// Verifies the mapping is monotonic decreasing over the whole AVEVA range: a more urgent (lower) priority
/// never yields a lower OPC UA severity, and the two ends differ (so a constant mapping fails too).
/// </summary>
[Fact]
public void Map_never_ranks_a_less_urgent_priority_above_a_more_urgent_one()
{
var previous = MxAccessSeverityMapper.Map(1).OpcUaSeverity;
for (var priority = 2; priority <= 999; priority++)
{
var current = MxAccessSeverityMapper.Map(priority).OpcUaSeverity;
current.ShouldBeLessThanOrEqualTo(previous, $"priority {priority} must not be more severe than {priority - 1}");
previous = current;
}
MxAccessSeverityMapper.Map(1).OpcUaSeverity.ShouldBeGreaterThan(MxAccessSeverityMapper.Map(999).OpcUaSeverity);
}
/// <summary>Verifies the live mapper is the shared table history uses (Gitea #886), value for value.</summary>
[Fact]
public void Map_is_the_shared_GalaxyAlarmPriority_table()
{
for (var priority = -2; priority <= 1001; priority++)
MxAccessSeverityMapper.Map(priority).ShouldBe(GalaxyAlarmPriority.Map(priority));
}
}