Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/InputQualityTests.cs
T

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);
}
}