513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
338 lines
13 KiB
C#
338 lines
13 KiB
C#
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Client;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
|
using CommonsTransitionKind = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmTransitionKind;
|
|
using ProtoConditionState = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState;
|
|
using ProtoTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
|
|
|
/// <summary>Pure MxGateway alarm-feed proto → NativeAlarmTransition mapping.</summary>
|
|
public class MxGatewayAlarmMapperTests
|
|
{
|
|
/// <summary>Verifies that an acknowledge transition is mapped as acknowledged with the operator.</summary>
|
|
[Fact]
|
|
public void MapTransition_AckTransition_IsAcknowledgedWithOperator()
|
|
{
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
|
TransitionKind = ProtoTransitionKind.Acknowledge,
|
|
Severity = 600,
|
|
OperatorUser = "operator1",
|
|
OperatorComment = "ack",
|
|
Category = "Process",
|
|
Description = "hi"
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapTransition(ev);
|
|
|
|
Assert.Equal(CommonsTransitionKind.Acknowledge, t.Kind);
|
|
Assert.True(t.Condition.Active);
|
|
Assert.True(t.Condition.Acknowledged);
|
|
Assert.Equal(600, t.Condition.Severity);
|
|
Assert.Equal("operator1", t.OperatorUser);
|
|
Assert.Equal("Tank01", t.SourceObjectReference);
|
|
}
|
|
|
|
/// <summary>Verifies that an active, acknowledged condition state maps to active true and acknowledged true.</summary>
|
|
[Fact]
|
|
public void MapConditionState_ActiveAcked_To_ActiveTrue_AckTrue()
|
|
{
|
|
var c = MxGatewayAlarmMapper.MapConditionState(ProtoConditionState.ActiveAcked, severity: 600);
|
|
Assert.True(c.Active);
|
|
Assert.True(c.Acknowledged);
|
|
Assert.Equal(600, c.Severity);
|
|
}
|
|
|
|
/// <summary>Verifies that an active, unacknowledged snapshot maps to the snapshot kind.</summary>
|
|
[Fact]
|
|
public void MapSnapshot_ActiveUnacked_IsSnapshotKind()
|
|
{
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Tank01.Level.Hi",
|
|
SourceObjectReference = "Tank01",
|
|
AlarmTypeName = "AnalogLimitAlarm.Hi",
|
|
CurrentState = ProtoConditionState.Active,
|
|
Severity = 1500 // out of range — must clamp
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
|
|
|
|
Assert.Equal(CommonsTransitionKind.Snapshot, t.Kind);
|
|
Assert.True(t.Condition.Active);
|
|
Assert.False(t.Condition.Acknowledged);
|
|
Assert.Equal(1000, t.Condition.Severity);
|
|
}
|
|
|
|
/// <summary>Verifies that the source reference is object-relative rather than the full provider reference.</summary>
|
|
[Fact]
|
|
public void SourceReference_IsObjectRelative_NotFullProviderReference()
|
|
{
|
|
// The condition identity surfaced upward is the object-relative reference
|
|
// (e.g. "Z28061.HeartbeatTimeoutAlarm"), not the gateway's full provider
|
|
// reference ("Galaxy!<area>.<object>.<alarm>"). Area lives in Category.
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
|
|
SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
|
|
AlarmTypeName = "Syst",
|
|
Category = "CVDAisle_1",
|
|
CurrentState = ProtoConditionState.Active,
|
|
Severity = 400
|
|
};
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Galaxy!CVDAisle_1.Z28061.HeartbeatTimeoutAlarm",
|
|
SourceObjectReference = "Z28061.HeartbeatTimeoutAlarm",
|
|
AlarmTypeName = "Syst",
|
|
TransitionKind = ProtoTransitionKind.Raise,
|
|
Severity = 400
|
|
};
|
|
|
|
var snapT = MxGatewayAlarmMapper.MapSnapshot(snap);
|
|
var liveT = MxGatewayAlarmMapper.MapTransition(ev);
|
|
|
|
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", snapT.SourceReference);
|
|
Assert.Equal("Z28061.HeartbeatTimeoutAlarm", liveT.SourceReference);
|
|
Assert.Equal("CVDAisle_1", snapT.Category);
|
|
}
|
|
|
|
/// <summary>Verifies that the source reference falls back to the full reference when the object reference is empty.</summary>
|
|
[Fact]
|
|
public void SourceReference_FallsBackToFullReference_WhenObjectReferenceEmpty()
|
|
{
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Galaxy!Area.Obj.Alarm",
|
|
SourceObjectReference = "",
|
|
CurrentState = ProtoConditionState.Active,
|
|
Severity = 100
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
|
|
|
|
Assert.Equal("Galaxy!Area.Obj.Alarm", t.SourceReference);
|
|
}
|
|
|
|
// ── CurrentValue / LimitValue ──────────────────────────────
|
|
|
|
/// <summary>Verifies that the current and limit values of a transition are populated from the proto.</summary>
|
|
[Fact]
|
|
public void MapTransition_CurrentAndLimitValue_PopulatedFromProto()
|
|
{
|
|
// The gateway proto OnAlarmTransitionEvent carries current_value and
|
|
// limit_value as MxValue union fields. Verify both are mapped through
|
|
// MxValueToString into the neutral NativeAlarmTransition strings.
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
|
TransitionKind = ProtoTransitionKind.Raise,
|
|
Severity = 800,
|
|
CurrentValue = 95.3.ToMxValue(),
|
|
LimitValue = 90.0.ToMxValue()
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapTransition(ev);
|
|
|
|
Assert.Equal("95.3", t.CurrentValue);
|
|
Assert.Equal("90", t.LimitValue);
|
|
}
|
|
|
|
/// <summary>Verifies that a transition with no current or limit value yields empty values.</summary>
|
|
[Fact]
|
|
public void MapTransition_AbsentCurrentAndLimitValue_YieldsEmpty()
|
|
{
|
|
// When the gateway sends events without current/limit value fields (optional),
|
|
// the resulting transition must have empty strings — never null.
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.Hi",
|
|
SourceObjectReference = "Tank01",
|
|
AlarmTypeName = "AnalogLimitAlarm.Hi",
|
|
TransitionKind = ProtoTransitionKind.Raise,
|
|
Severity = 600
|
|
// CurrentValue and LimitValue not set → proto default (null reference)
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapTransition(ev);
|
|
|
|
Assert.Equal("", t.CurrentValue);
|
|
Assert.Equal("", t.LimitValue);
|
|
}
|
|
|
|
/// <summary>Verifies that the current and limit values of a snapshot are populated from the proto.</summary>
|
|
[Fact]
|
|
public void MapSnapshot_CurrentAndLimitValue_PopulatedFromProto()
|
|
{
|
|
// ActiveAlarmSnapshot also carries current_value and limit_value.
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Pump01.Vibration.HiHi",
|
|
SourceObjectReference = "Pump01",
|
|
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
|
CurrentState = ProtoConditionState.Active,
|
|
Severity = 900,
|
|
CurrentValue = 12.7.ToMxValue(),
|
|
LimitValue = 10.0.ToMxValue()
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
|
|
|
|
Assert.Equal("12.7", t.CurrentValue);
|
|
Assert.Equal("10", t.LimitValue);
|
|
}
|
|
|
|
/// <summary>Verifies that a string MX value produces a string current value.</summary>
|
|
[Fact]
|
|
public void MapSnapshot_StringMxValue_ProducesStringCurrentValue()
|
|
{
|
|
// MxValue can carry string values (e.g. for discrete/string-type tags).
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Mode.Alarm",
|
|
SourceObjectReference = "Mode",
|
|
AlarmTypeName = "DiscreteAlarm",
|
|
CurrentState = ProtoConditionState.Active,
|
|
Severity = 500,
|
|
CurrentValue = "FAULT".ToMxValue()
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapSnapshot(snap);
|
|
|
|
Assert.Equal("FAULT", t.CurrentValue);
|
|
Assert.Equal("", t.LimitValue); // not set
|
|
}
|
|
|
|
// ── MES alarm-status API §6.4: AckTime stamping ──
|
|
|
|
private static readonly DateTimeOffset GatewayTime =
|
|
new(2026, 8, 1, 9, 30, 0, TimeSpan.Zero);
|
|
|
|
/// <summary>Verifies that an acknowledge transition stamps the observed transition time as the ack time.</summary>
|
|
[Fact]
|
|
public void MapTransition_Acknowledge_StampsTheObservedTransitionTimeAsAckTime()
|
|
{
|
|
// The gateway feed carries NO dedicated ack timestamp, so the ack transition's own
|
|
// time is what gets stamped — accurate to when the system saw the ack.
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
|
TransitionKind = ProtoTransitionKind.Acknowledge,
|
|
Severity = 600,
|
|
TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapTransition(ev);
|
|
|
|
Assert.Equal(GatewayTime, t.AckTime);
|
|
Assert.Equal(t.TransitionTime, t.AckTime);
|
|
}
|
|
|
|
/// <summary>Verifies that a raise transition has no ack time.</summary>
|
|
[Fact]
|
|
public void MapTransition_Raise_HasNoAckTime()
|
|
{
|
|
// Null while unacknowledged.
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
TransitionKind = ProtoTransitionKind.Raise,
|
|
Severity = 600,
|
|
TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
|
|
};
|
|
|
|
Assert.Null(MxGatewayAlarmMapper.MapTransition(ev).AckTime);
|
|
}
|
|
|
|
/// <summary>Verifies that a retrigger transition clears the ack time.</summary>
|
|
[Fact]
|
|
public void MapTransition_Retrigger_ClearsTheAckTime()
|
|
{
|
|
// "Cleared on re-raise": a Retrigger arrives unacknowledged, so no ack time is
|
|
// reported even though the condition was acked a moment earlier.
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
TransitionKind = ProtoTransitionKind.Retrigger,
|
|
Severity = 600,
|
|
TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
|
|
};
|
|
|
|
Assert.Null(MxGatewayAlarmMapper.MapTransition(ev).AckTime);
|
|
}
|
|
|
|
/// <summary>Verifies that a clear transition has no ack time despite the inactive state mapping to acknowledged.</summary>
|
|
[Fact]
|
|
public void MapTransition_Clear_HasNoAckTime_DespiteInactiveMappingToAcked()
|
|
{
|
|
// The gateway maps INACTIVE to acked = true. Without the active check every
|
|
// return-to-normal would claim an ack the system never observed.
|
|
var ev = new OnAlarmTransitionEvent
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
TransitionKind = ProtoTransitionKind.Clear,
|
|
Severity = 600,
|
|
TransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
|
|
};
|
|
|
|
var t = MxGatewayAlarmMapper.MapTransition(ev);
|
|
|
|
Assert.True(t.Condition.Acknowledged); // the gateway's own mapping
|
|
Assert.Null(t.AckTime); // …but no ack was observed
|
|
}
|
|
|
|
/// <summary>Verifies that an active, acknowledged snapshot restores an ack time from the last transition.</summary>
|
|
[Fact]
|
|
public void MapSnapshot_ActiveAcked_RestoresAnAckTimeFromTheLastTransition()
|
|
{
|
|
// A (re)subscribe snapshot must not silently drop the ack instant of an already
|
|
// acknowledged alarm — the last transition time is the best the feed supplies.
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
CurrentState = ProtoConditionState.ActiveAcked,
|
|
Severity = 600,
|
|
LastTransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
|
|
};
|
|
|
|
Assert.Equal(GatewayTime, MxGatewayAlarmMapper.MapSnapshot(snap).AckTime);
|
|
}
|
|
|
|
/// <summary>Verifies that an active, unacknowledged snapshot has no ack time.</summary>
|
|
[Fact]
|
|
public void MapSnapshot_ActiveUnacked_HasNoAckTime()
|
|
{
|
|
var snap = new ActiveAlarmSnapshot
|
|
{
|
|
AlarmFullReference = "Tank01.Level.HiHi",
|
|
SourceObjectReference = "Tank01",
|
|
CurrentState = ProtoConditionState.Active,
|
|
Severity = 600,
|
|
LastTransitionTimestamp = Timestamp.FromDateTimeOffset(GatewayTime)
|
|
};
|
|
|
|
Assert.Null(MxGatewayAlarmMapper.MapSnapshot(snap).AckTime);
|
|
}
|
|
|
|
/// <summary>Verifies that the snapshot-complete sentinel carries no ack time.</summary>
|
|
[Fact]
|
|
public void SnapshotComplete_Sentinel_CarriesNoAckTime()
|
|
{
|
|
// The end-of-snapshot sentinel has no condition payload at all.
|
|
Assert.Null(MxGatewayAlarmMapper.SnapshotComplete().AckTime);
|
|
}
|
|
}
|