/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using NUnit.Framework; namespace Opc.Ua.Server.Tests { /// /// Verifies exact values, interpolation, bounds, and error statuses for at-time aggregate calculations. /// [TestFixture] [Category("AggregateCalculator")] [Parallelizable(ParallelScope.All)] public sealed class AggregateCalculatorAtTimeTests { /// /// Verifies that an at-time calculation returns the exact raw value at a matching timestamp. /// [TestCaseSource(nameof(ExactValues))] public void CalculateAtTimeReturnsExactRawValue( StatusCode statusCode) { DateTimeUtc timestamp = TimeAt(5); DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(5, 42, statusCode) ], timestamp, useSimpleBounds: false, stepped: false); Assert.That(result.WrappedValue.TryGetValue(out int value), Is.True); Assert.That(value, Is.EqualTo(42)); Assert.That( result.StatusCode.CodeBits, Is.EqualTo(statusCode.CodeBits)); Assert.That( result.StatusCode.AggregateBits, Is.EqualTo(AggregateBits.Raw)); Assert.That(result.SourceTimestamp, Is.EqualTo(timestamp)); } /// /// Verifies that an at-time calculation uses sloped interpolation between bounds. /// [Test] public void CalculateAtTimeUsesSlopedInterpolation() { DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(0, 0), ValueAt(10, 10) ], TimeAt(5), useSimpleBounds: false, stepped: false); Assert.That(result.WrappedValue.TryGetValue(out int value), Is.True); Assert.That(value, Is.EqualTo(5)); Assert.That(StatusCode.IsGood(result.StatusCode), Is.True); Assert.That( result.StatusCode.AggregateBits, Is.EqualTo(AggregateBits.Interpolated)); } /// /// Verifies that an at-time calculation uses the preceding value for stepped interpolation. /// [Test] public void CalculateAtTimeUsesSteppedInterpolation() { DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(0, 3), ValueAt(10, 10) ], TimeAt(5), useSimpleBounds: false, stepped: true); Assert.That(result.WrappedValue.TryGetValue(out int value), Is.True); Assert.That(value, Is.EqualTo(3)); Assert.That(StatusCode.IsGood(result.StatusCode), Is.True); Assert.That( result.StatusCode.AggregateBits, Is.EqualTo(AggregateBits.Interpolated)); } /// /// Verifies that simple-bound at-time reads still calculate an interpolated value. /// [Test] public void CalculateAtTimeSimpleBoundsStillCalculateValue() { DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(0, 0), ValueAt(10, 10) ], TimeAt(5), useSimpleBounds: true, stepped: false); Assert.That(result.WrappedValue.TryGetValue(out int value), Is.True); Assert.That(value, Is.EqualTo(5)); Assert.That( result.StatusCode.AggregateBits, Is.EqualTo(AggregateBits.Interpolated)); } /// /// Verifies that interpolated at-time bounds skip bad values. /// [Test] public void CalculateAtTimeInterpolatedBoundsSkipBadValues() { DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(0, 0), ValueAt(5, 100, StatusCodes.BadNoData), ValueAt(10, 10) ], TimeAt(7), useSimpleBounds: false, stepped: false); Assert.That(result.WrappedValue.TryGetValue(out int value), Is.True); Assert.That(value, Is.EqualTo(7)); Assert.That(StatusCode.IsUncertain(result.StatusCode), Is.True); Assert.That( result.StatusCode.AggregateBits, Is.EqualTo(AggregateBits.Interpolated)); } /// /// Verifies that a bad late simple bound produces an uncertain stepped value. /// [Test] public void CalculateAtTimeSimpleBadLateBoundUsesUncertainStep() { DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(0, 4), ValueAt(10, 10, StatusCodes.BadNoData) ], TimeAt(5), useSimpleBounds: true, stepped: false); Assert.That(result.WrappedValue.TryGetValue(out int value), Is.True); Assert.That(value, Is.EqualTo(4)); Assert.That(StatusCode.IsUncertain(result.StatusCode), Is.True); Assert.That( result.StatusCode.AggregateBits, Is.EqualTo(AggregateBits.Interpolated)); } /// /// Verifies that an at-time calculation without a starting value returns BadNoData. /// [Test] public void CalculateAtTimeWithoutStartingValueReturnsBadNoData() { DataValue result = AggregateCalculator.CalculateAtTime( [ ValueAt(10, 10) ], TimeAt(5), useSimpleBounds: false, stepped: false); Assert.That( result.StatusCode, Is.EqualTo(StatusCodes.BadNoData)); Assert.That(result.SourceTimestamp, Is.EqualTo(TimeAt(5))); } /// /// Verifies that sloped interpolation of an unsupported type returns BadTypeMismatch. /// [Test] public void CalculateAtTimeUnsupportedSlopedTypeReturnsBadTypeMismatch() { DataValue result = AggregateCalculator.CalculateAtTime( [ TextValueAt(0, "before"), TextValueAt(10, "after") ], TimeAt(5), useSimpleBounds: false, stepped: false); Assert.That( result.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch)); } private static readonly StatusCode[] ExactValues = [ StatusCodes.Good, StatusCodes.Uncertain, StatusCodes.Bad ]; private static DataValue ValueAt( int second, int value, StatusCode statusCode = default) { if (statusCode == default) { statusCode = StatusCodes.Good; } return new DataValue( Variant.From(value), statusCode, TimeAt(second), DateTimeUtc.MinValue); } private static DataValue TextValueAt(int second, string value) { return new DataValue( Variant.From(value), StatusCodes.Good, TimeAt(second), DateTimeUtc.MinValue); } private static DateTimeUtc TimeAt(int second) { return new DateTimeUtc(2026, 1, 1, 0, 0, second); } } }