using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; /// /// Truth table for — 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. /// 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); /// An empty input set reports Good — there is nothing to be worse than. [Fact] public void WorstOf_no_inputs_is_Good() { InputQuality.WorstOf([]).ShouldBe(Good); } /// All-Good inputs report Good. [Fact] public void WorstOf_all_good_is_Good() { InputQuality.WorstOf([Snap(1, Good), Snap(2, Good)]).ShouldBe(Good); } /// A single Uncertain input carries its own sub-code out, not the generic Uncertain. [Fact] public void WorstOf_propagates_the_uncertain_sub_code() { InputQuality.WorstOf([Snap(1, Good), Snap(2, UncertainLastUsableValue)]) .ShouldBe(UncertainLastUsableValue); } /// Bad beats Uncertain regardless of the order the inputs are seen in. [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); } /// /// A null entry means "this dependency has never been pushed", which is /// BadWaitingForInitialData — the same code the materialiser stamps on a fresh node. /// Reporting Good for it would be the exact laundering this helper exists to stop. /// [Fact] public void WorstOf_a_null_entry_is_BadWaitingForInitialData() { InputQuality.WorstOf([Snap(1, Good), null]).ShouldBe(BadWaitingForInitialData); } /// /// 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. /// [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); } /// Good and Uncertain are usable (a script may still inspect the value); Bad is not. [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); } }