55b12dc217
Driver.Historian.Gateway-001 (High): AlarmEventMapper wrote the alarm's
Severity wire property as the .NET enum name (e.g. "High"), but
EventMapper's read side int.TryParse()s that same property and silently
falls back to the minimum severity on any unparseable value - so every
alarm event's severity round-tripped through this module's own mappers
collapsed to 1 (Low), contradicting AlarmHistorianEvent.Severity's own
documented contract ("mapped to Historian's numeric priority"). Now
mapped through a numeric priority mirroring the existing
NativeAlarmProjector.MapSeverity convention (200/500/700/900).
Driver.Historian.Gateway-002 (Low): GatewayTagProvisioner never disposed
its IHistorianGatewayClient, unlike its GatewayHistorianDataSource /
GatewayAlarmHistorianWriter siblings. Now implements IAsyncDisposable so
the DI-owned singleton's gRPC channel closes gracefully at shutdown.
Both TDD: new/corrected tests confirmed RED against the pre-fix code.
81 lines
4.1 KiB
C#
81 lines
4.1 KiB
C#
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; // AlarmHistorianEvent
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions; // AlarmSeverity
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
|
|
|
|
public sealed class AlarmEventMapperTests
|
|
{
|
|
[Fact]
|
|
public void Maps_source_time_type_and_rich_properties()
|
|
{
|
|
var a = new AlarmHistorianEvent("A1", "Area/Line/Pump1", "HiHi", "LimitAlarm",
|
|
AlarmSeverity.High, "Activated", "Temp high", "operator1", "ack note",
|
|
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
var e = AlarmEventMapper.ToHistorianEvent(a);
|
|
Assert.Equal("Area/Line/Pump1", e.SourceName);
|
|
Assert.Equal("LimitAlarm", e.Type);
|
|
Assert.Equal(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), e.EventTime.ToDateTime());
|
|
// The wire Id is deliberately left unset — the gateway's SendEvent rejects a client-supplied
|
|
// event id. The alarm's own id is carried as a property instead.
|
|
Assert.Equal(string.Empty, e.Id);
|
|
Assert.Equal("A1", e.Properties["AlarmId"]);
|
|
Assert.Equal("HiHi", e.Properties["AlarmName"]);
|
|
Assert.Equal("Activated", e.Properties["EventKind"]);
|
|
// Severity is the AlarmHistorianEvent.Severity XML doc contract: "Mapped to Historian's
|
|
// numeric priority on the sink side" — NOT the .NET enum name. EventMapper.ParseSeverity on
|
|
// the read side int.TryParse()s this same property, so a non-numeric string (the old bug:
|
|
// alarm.Severity.ToString() == "High") silently always read back as the minimum severity (1).
|
|
Assert.Equal("700", e.Properties["Severity"]);
|
|
Assert.Equal("operator1", e.Properties["User"]);
|
|
Assert.Equal("ack note", e.Properties["Comment"]);
|
|
Assert.Equal("Temp high", e.Properties["Message"]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Driver.Historian.Gateway-001 regression: the write side (<see cref="AlarmEventMapper"/>) and
|
|
/// the read side (<see cref="EventMapper"/>) must agree on the wire representation of severity —
|
|
/// a numeric OPC UA Part 9 priority string, matching the convention already used elsewhere in the
|
|
/// codebase for the same enum (<c>NativeAlarmProjector.MapSeverity</c>: Low=200/Medium=500/
|
|
/// High=700/Critical=900). Round-tripping through both mappers must reproduce the numeric severity,
|
|
/// not collapse to the minimum every time.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(AlarmSeverity.Low, (ushort)200)]
|
|
[InlineData(AlarmSeverity.Medium, (ushort)500)]
|
|
[InlineData(AlarmSeverity.High, (ushort)700)]
|
|
[InlineData(AlarmSeverity.Critical, (ushort)900)]
|
|
public void Severity_round_trips_through_EventMapper_as_numeric_priority(AlarmSeverity severity, ushort expected)
|
|
{
|
|
var a = new AlarmHistorianEvent("A1", "Area/Line/Pump1", "HiHi", "LimitAlarm",
|
|
severity, "Activated", "m", "operator1", null,
|
|
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
|
|
var wire = AlarmEventMapper.ToHistorianEvent(a);
|
|
var readBack = EventMapper.ToHistoricalEvent(wire);
|
|
|
|
Assert.Equal(expected, readBack.Severity);
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_comment_is_omitted_not_null()
|
|
{
|
|
var a = new AlarmHistorianEvent("A", "S", "N", "DiscreteAlarm", AlarmSeverity.Low, "Cleared", "m", "system", null,
|
|
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
Assert.False(AlarmEventMapper.ToHistorianEvent(a).Properties.ContainsKey("Comment"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Wire_id_is_never_set_and_blank_alarm_id_adds_no_property()
|
|
{
|
|
// A blank AlarmId must NOT fall back to a generated wire Id (the gateway rejects any id) and
|
|
// must not add an empty AlarmId property.
|
|
var a = new AlarmHistorianEvent("", "S", "N", "DiscreteAlarm", AlarmSeverity.Low, "Cleared", "m", "system", null,
|
|
new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
var e = AlarmEventMapper.ToHistorianEvent(a);
|
|
Assert.Equal(string.Empty, e.Id);
|
|
Assert.False(e.Properties.ContainsKey("AlarmId"));
|
|
}
|
|
}
|