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-008 regression. The previous implementation unconditionally /// called HandleConnectionError() whenever StartQuery returned false, /// which tore down the (relatively expensive) shared SDK connection on a query-class error /// such as a bad tag name. A burst of bad-tag queries could therefore push an otherwise /// healthy cluster node into cooldown via the picker's MarkFailed. The fix /// classifies the SDK error code: connection-class codes drop the connection; query-class /// codes leave it intact. /// [Trait("Category", "Unit")] public sealed class HistorianDataSourceStartQueryClassificationTests { // ── Connection-class codes — the connection should be reset ─────────── /// Verifies that connection-class error codes are classified as connection errors. /// The historian error code to test. [Theory] [InlineData(HistorianAccessError.ErrorValue.FailedToConnect)] [InlineData(HistorianAccessError.ErrorValue.FailedToCreateSession)] [InlineData(HistorianAccessError.ErrorValue.NoReply)] [InlineData(HistorianAccessError.ErrorValue.NotReady)] [InlineData(HistorianAccessError.ErrorValue.NotInitialized)] [InlineData(HistorianAccessError.ErrorValue.Stopping)] [InlineData(HistorianAccessError.ErrorValue.Win32Exception)] [InlineData(HistorianAccessError.ErrorValue.InvalidResponse)] public void Connection_class_codes_are_classified_as_connection_errors(HistorianAccessError.ErrorValue code) { HistorianDataSource.IsConnectionClassError(code).ShouldBeTrue( $"{code} is a connection/server failure — the SDK connection should be reset"); } // ── Query-class codes — the connection should NOT be reset ──────────── /// Verifies that query-class error codes are NOT classified as connection errors. /// The historian error code to test. [Theory] [InlineData(HistorianAccessError.ErrorValue.InvalidArgument)] // bad tag name, etc. [InlineData(HistorianAccessError.ErrorValue.ValidationFailed)] // bad query args [InlineData(HistorianAccessError.ErrorValue.NotApplicable)] // wrong tag kind for query [InlineData(HistorianAccessError.ErrorValue.NotImplemented)] // unsupported aggregate [InlineData(HistorianAccessError.ErrorValue.NoData)] // empty range public void Query_class_codes_are_NOT_classified_as_connection_errors(HistorianAccessError.ErrorValue code) { HistorianDataSource.IsConnectionClassError(code).ShouldBeFalse( $"{code} is a query payload problem — must NOT tear down the SDK connection"); } // ── Driver.Historian.Wonderware-014: the at-time loop must classify a per-timestamp // StartQuery failure the same way the raw / aggregate / event paths do. The SDK // HistoryQuery type is sealed-by-non-virtual + has no interface, so the loop itself // can't be driven offline; the per-failure decision is therefore extracted into a // pure helper that the at-time loop calls and these tests pin directly. ────────── /// /// A connection-class StartQuery error in the at-time loop must signal "reset the /// connection and abort the read" (true) — not silently record a Bad sample and keep /// hammering the dead connection for every remaining timestamp. /// /// The connection-class error code. [Theory] [InlineData(HistorianAccessError.ErrorValue.FailedToConnect)] [InlineData(HistorianAccessError.ErrorValue.NoReply)] [InlineData(HistorianAccessError.ErrorValue.NotReady)] public void AtTime_StartQuery_failure_with_connection_class_code_requests_connection_reset( HistorianAccessError.ErrorValue code) { var error = new HistorianAccessError { ErrorCode = code }; HistorianDataSource.ShouldResetConnectionForStartQueryFailure(error).ShouldBeTrue( $"{code} is a connection failure — the at-time loop must reset the connection, not record Bad"); } /// /// A query-class StartQuery error (or a missing error) in the at-time loop must NOT /// reset the connection (false): a single bad/empty timestamp records a per-timestamp /// Bad sample and continues to the next without tearing down the shared connection. /// /// The query-class error code. [Theory] [InlineData(HistorianAccessError.ErrorValue.InvalidArgument)] [InlineData(HistorianAccessError.ErrorValue.NoData)] [InlineData(HistorianAccessError.ErrorValue.NotApplicable)] public void AtTime_StartQuery_failure_with_query_class_code_does_not_request_reset( HistorianAccessError.ErrorValue code) { var error = new HistorianAccessError { ErrorCode = code }; HistorianDataSource.ShouldResetConnectionForStartQueryFailure(error).ShouldBeFalse( $"{code} is a query/no-data problem — the at-time loop keeps the connection and records Bad"); } /// A null error defaults to query-class (no reset) — the caller still records a Bad sample. [Fact] public void AtTime_StartQuery_failure_with_null_error_defaults_to_no_reset() { HistorianDataSource.ShouldResetConnectionForStartQueryFailure(null).ShouldBeFalse( "a null error must not be promoted to a connection reset"); } }