- Driver.Historian.Wonderware-004: ToHistorianEvent synthesises a fresh
Guid when the upstream EventId is unparseable and logs the substitution
instead of writing the historian with Guid.Empty.
- Driver.Historian.Wonderware-005: GetHealthSnapshot derives the
connection-open booleans from the active-node fields so the snapshot
is self-consistent without depending on the secondary lock.
- Driver.Historian.Wonderware-007: SID-mismatch branch in PipeServer now
sends a HelloAck { Accepted=false, RejectReason } so the client sees a
symmetric rejection.
- Driver.Historian.Wonderware-008: classify StartQuery failures —
connection-class codes drop the connection, query-class codes throw
QueryClassStartQueryException so the IPC layer surfaces Success=false.
- Driver.Historian.Wonderware-010: RequestTimeoutSeconds now enforced
via BuildRequestCts linked to the caller's CancellationToken.
- Driver.Historian.Wonderware-011: refreshed XML docs to describe the
current sidecar / named-pipe architecture (Galaxy.Host / Proxy
references reframed as historical context).
- Driver.Historian.Wonderware-012: pinned the previously-uncovered
HistorianDataSource behaviours with five new test files; also removed
the stale empty tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests
directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.6 KiB
C#
110 lines
4.6 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Driver.Historian.Wonderware-010 regression. <see cref="HistorianConfiguration.RequestTimeoutSeconds"/>
|
|
/// was documented as the "outer safety timeout applied to sync-over-async Historian
|
|
/// operations" but was never read or enforced — a hung <c>StartQuery</c> or a slow
|
|
/// <c>MoveNext</c> could block the single pipe-server connection thread indefinitely.
|
|
/// The fix wires it into the read paths via a linked <see cref="CancellationTokenSource"/>
|
|
/// 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.
|
|
/// </summary>
|
|
[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 });
|
|
}
|
|
}
|