feat(core): InputQuality.WorstOf — the one worst-of-input rule (archreview R3-02 contract)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user