using System; using System.Reflection; using System.Threading; using System.Threading.Tasks; 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-010 regression. /// was documented as the "outer safety timeout applied to sync-over-async Historian /// operations" but was never read or enforced — a hung StartQuery or a slow /// MoveNext could block the single pipe-server connection thread indefinitely. /// The fix wires it into the read paths via a linked /// so the documented safety net actually exists. /// /// The SDK-touching read methods cannot be unit-driven without a live AVEVA Historian. /// This test pins the helper that derives the effective timeout from the config — the /// read methods invoke that helper, so a regression in either the helper or the wiring /// would break the test. /// [Trait("Category", "Unit")] public sealed class HistorianDataSourceRequestTimeoutTests { [Fact] public void Default_request_timeout_is_60_seconds() { new HistorianConfiguration().RequestTimeoutSeconds.ShouldBe(60); } [Fact] public void Positive_request_timeout_is_used_verbatim() { InvokeBuildLinkedTokenSource( new HistorianConfiguration { RequestTimeoutSeconds = 30 }, CancellationToken.None, out var cts); cts.ShouldNotBeNull(); // The helper must wire CancelAfter — easiest cross-check is to observe that the // returned CTS is NOT already cancelled, and that disposing it is safe. cts!.IsCancellationRequested.ShouldBeFalse(); cts.Dispose(); } [Fact] public void Zero_or_negative_request_timeout_is_treated_as_no_timeout() { // A zero/negative value means "no outer timeout" — the helper must still return a // linked CTS so callers can use one code path, but it must not auto-cancel. InvokeBuildLinkedTokenSource( new HistorianConfiguration { RequestTimeoutSeconds = 0 }, CancellationToken.None, out var cts); cts.ShouldNotBeNull(); cts!.IsCancellationRequested.ShouldBeFalse(); // Give the runtime a moment — a misconfigured CancelAfter(0) would fire immediately. Thread.Sleep(50); cts.IsCancellationRequested.ShouldBeFalse("RequestTimeoutSeconds <= 0 must not auto-cancel"); cts.Dispose(); } [Fact] public async Task Small_timeout_cancels_the_linked_token() { // 50 ms timeout — sleep 250 ms then assert the linked CTS has fired. InvokeBuildLinkedTokenSource( new HistorianConfiguration { RequestTimeoutSeconds = 1 }, // smallest non-zero whole-second value CancellationToken.None, out var cts); cts.ShouldNotBeNull(); // The wall-clock cost of waiting a full second per test is acceptable — this // pins the actual CancelAfter wiring rather than just the conditional logic. await Task.Delay(1500); cts!.IsCancellationRequested.ShouldBeTrue("RequestTimeoutSeconds=1 must cancel within 1.5s"); cts.Dispose(); } [Fact] public void Inbound_cancellation_propagates_into_the_linked_token() { using var outer = new CancellationTokenSource(); InvokeBuildLinkedTokenSource( new HistorianConfiguration { RequestTimeoutSeconds = 60 }, outer.Token, out var cts); cts.ShouldNotBeNull(); cts!.IsCancellationRequested.ShouldBeFalse(); outer.Cancel(); cts.IsCancellationRequested.ShouldBeTrue("cancelling the caller's CT must cancel the linked CTS"); cts.Dispose(); } private static void InvokeBuildLinkedTokenSource( HistorianConfiguration cfg, CancellationToken ct, out CancellationTokenSource? cts) { // The helper is internal so the InternalsVisibleTo on the data-source project lets // us bind to it directly. Reflection keeps the test resilient if the method name is // ever shortened. var method = typeof(HistorianDataSource) .GetMethod("BuildRequestCts", BindingFlags.Static | BindingFlags.NonPublic); method.ShouldNotBeNull( "HistorianDataSource.BuildRequestCts must exist — wires RequestTimeoutSeconds into the read paths"); cts = (CancellationTokenSource?)method!.Invoke(null, new object[] { cfg, ct }); } }