using System.Runtime.Serialization; using ArchestrA; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Backend; /// /// Driver.Historian.Wonderware-012 coverage — pins the two static helpers on /// that previously had no direct tests: /// (the string-vs-numeric heuristic /// for the raw + at-time read paths) and /// (the aggregate-column dispatch). The SDK HistoryQueryResult initialises internal /// state lazily on first property access, which makes it impractical to fake via /// ; the heuristic was therefore /// refactored into an SDK-independent overload that the tests drive directly. /// [Trait("Category", "Unit")] public sealed class HistorianDataSourceValueAndAggregateTests { // ── SelectValueFromPair ─────────────────────────────────────────────── [Fact] public void SelectValueFromPair_returns_numeric_value_when_StringValue_is_empty() { HistorianDataSource.SelectValueFromPair(42.5, string.Empty).ShouldBe(42.5); } [Fact] public void SelectValueFromPair_returns_numeric_value_when_Value_is_non_zero_even_with_StringValue_populated() { // Tag is numeric and sampled non-zero; the SDK may still populate a formatted // StringValue but the value path wins. HistorianDataSource.SelectValueFromPair(3.14, "3.14").ShouldBe(3.14); } [Fact] public void SelectValueFromPair_returns_StringValue_when_Value_is_zero_and_StringValue_non_empty() { // String tags in the SDK always project Value=0 — that's the documented heuristic. HistorianDataSource.SelectValueFromPair(0.0, "Ready").ShouldBe("Ready"); } [Fact] public void SelectValueFromPair_returns_numeric_zero_when_Value_is_zero_and_StringValue_empty() { // Numeric tag legitimately samples zero, no formatted text — must remain numeric. HistorianDataSource.SelectValueFromPair(0.0, string.Empty).ShouldBe(0.0); } [Fact] public void SelectValueFromPair_null_StringValue_falls_back_to_numeric() { HistorianDataSource.SelectValueFromPair(7.7, null).ShouldBe(7.7); } [Fact] public void SelectValueFromPair_documented_edge_case_numeric_zero_with_formatted_string_returns_string() { // The doc comment on SelectValue calls this out as a known SDK-binding edge case: // "A numeric tag at exactly zero with a non-empty formatted StringValue (e.g. '0.00') // would be mis-reported as a string". This test pins that documented behaviour so // a future SDK upgrade that surfaces a real data-type field can replace the // heuristic deliberately rather than by accident. HistorianDataSource.SelectValueFromPair(0.0, "0.00").ShouldBe("0.00"); } // ── ExtractAggregateValue ───────────────────────────────────────────── [Theory] [InlineData("Average", 10.0)] [InlineData("Minimum", 1.0)] [InlineData("Maximum", 20.0)] [InlineData("First", 2.0)] [InlineData("Last", 8.0)] [InlineData("StdDev", 1.5)] public void ExtractAggregateValue_dispatches_known_columns(string column, double expected) { var result = NewAggregateResult(); result.Average = 10.0; result.Minimum = 1.0; result.Maximum = 20.0; result.ValueCount = 5; result.First = 2.0; result.Last = 8.0; result.StdDev = 1.5; HistorianDataSource.ExtractAggregateValue(result, column).ShouldBe(expected); } [Fact] public void ExtractAggregateValue_ValueCount_dispatches_to_uint_field() { var result = NewAggregateResult(); result.ValueCount = 42; HistorianDataSource.ExtractAggregateValue(result, "ValueCount").ShouldBe(42.0); } [Fact] public void ExtractAggregateValue_unknown_column_returns_null() { // Unknown column → null → IPC sample carries no value → client maps to BadNoData. HistorianDataSource.ExtractAggregateValue(NewAggregateResult(), "NotAColumn").ShouldBeNull(); } [Fact] public void ExtractAggregateValue_case_sensitive_dispatch() { // The switch is case-sensitive — "average" (lowercase) does NOT dispatch. Pinned so // the canonical column-name casing is preserved across refactors. var result = NewAggregateResult(); result.Average = 99.0; HistorianDataSource.ExtractAggregateValue(result, "average").ShouldBeNull(); HistorianDataSource.ExtractAggregateValue(result, "Average").ShouldBe(99.0); } private static AnalogSummaryQueryResult NewAggregateResult() { return (AnalogSummaryQueryResult)FormatterServices.GetUninitializedObject(typeof(AnalogSummaryQueryResult)); } }