98 lines
3.9 KiB
C#
98 lines
3.9 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
|
|
|
/// <summary>
|
|
/// Truth table for <see cref="InputQuality"/> — the one worst-of-input rule every derived-value
|
|
/// seam uses (scripted alarms, virtual tags, the Calculation driver). The quality contract
|
|
/// (archreview R3-02) is "a value consumer never reports a quality better than its source's",
|
|
/// and this type is the single place that decides what "worse" means, so a divergence here is a
|
|
/// divergence in every seam at once.
|
|
/// </summary>
|
|
public sealed class InputQualityTests
|
|
{
|
|
private const uint Good = 0x00000000u;
|
|
private const uint Uncertain = 0x40000000u;
|
|
private const uint UncertainLastUsableValue = 0x40900000u;
|
|
private const uint BadNoCommunication = 0x80310000u;
|
|
private const uint BadInternalError = 0x80020000u;
|
|
private const uint BadWaitingForInitialData = 0x80320000u;
|
|
|
|
private static DataValueSnapshot Snap(object? value, uint status) =>
|
|
new(value, status, SourceTimestampUtc: DateTime.UtcNow, ServerTimestampUtc: DateTime.UtcNow);
|
|
|
|
/// <summary>An empty input set reports Good — there is nothing to be worse than.</summary>
|
|
[Fact]
|
|
public void WorstOf_no_inputs_is_Good()
|
|
{
|
|
InputQuality.WorstOf([]).ShouldBe(Good);
|
|
}
|
|
|
|
/// <summary>All-Good inputs report Good.</summary>
|
|
[Fact]
|
|
public void WorstOf_all_good_is_Good()
|
|
{
|
|
InputQuality.WorstOf([Snap(1, Good), Snap(2, Good)]).ShouldBe(Good);
|
|
}
|
|
|
|
/// <summary>A single Uncertain input carries its own sub-code out, not the generic Uncertain.</summary>
|
|
[Fact]
|
|
public void WorstOf_propagates_the_uncertain_sub_code()
|
|
{
|
|
InputQuality.WorstOf([Snap(1, Good), Snap(2, UncertainLastUsableValue)])
|
|
.ShouldBe(UncertainLastUsableValue);
|
|
}
|
|
|
|
/// <summary>Bad beats Uncertain regardless of the order the inputs are seen in.</summary>
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void WorstOf_bad_beats_uncertain(bool badFirst)
|
|
{
|
|
DataValueSnapshot?[] inputs = badFirst
|
|
? [Snap(null, BadNoCommunication), Snap(2, Uncertain)]
|
|
: [Snap(2, Uncertain), Snap(null, BadNoCommunication)];
|
|
|
|
InputQuality.WorstOf(inputs).ShouldBe(BadNoCommunication);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A null entry means "this dependency has never been pushed", which is
|
|
/// <c>BadWaitingForInitialData</c> — the same code the materialiser stamps on a fresh node.
|
|
/// Reporting Good for it would be the exact laundering this helper exists to stop.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WorstOf_a_null_entry_is_BadWaitingForInitialData()
|
|
{
|
|
InputQuality.WorstOf([Snap(1, Good), null]).ShouldBe(BadWaitingForInitialData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Among equal severity the FIRST seen wins, so a re-evaluation over the same inputs reports
|
|
/// the same sub-code rather than oscillating between two equally-bad codes.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WorstOf_equal_severity_keeps_the_first_seen()
|
|
{
|
|
InputQuality.WorstOf([Snap(null, BadNoCommunication), Snap(null, BadInternalError)])
|
|
.ShouldBe(BadNoCommunication);
|
|
InputQuality.WorstOf([Snap(null, BadInternalError), Snap(null, BadNoCommunication)])
|
|
.ShouldBe(BadInternalError);
|
|
}
|
|
|
|
/// <summary>Good and Uncertain are usable (a script may still inspect the value); Bad is not.</summary>
|
|
[Theory]
|
|
[InlineData(Good, true)]
|
|
[InlineData(Uncertain, true)]
|
|
[InlineData(UncertainLastUsableValue, true)]
|
|
[InlineData(BadNoCommunication, false)]
|
|
[InlineData(BadWaitingForInitialData, false)]
|
|
[InlineData(0xC0000000u, false)]
|
|
public void IsUsable_follows_Part4_severity(uint statusCode, bool expected)
|
|
{
|
|
InputQuality.IsUsable(statusCode).ShouldBe(expected);
|
|
}
|
|
}
|