- 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>
115 lines
4.6 KiB
C#
115 lines
4.6 KiB
C#
using System;
|
|
using System.Reflection;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Driver.Historian.Wonderware-005 regression tests for <see cref="HistorianDataSource.GetHealthSnapshot"/>.
|
|
/// The active-node strings and the connection-open booleans were published under different
|
|
/// locks, so a snapshot could observe an internally inconsistent pairing (open with no node,
|
|
/// or closed with a non-null node). The fix derives the open booleans from the same field
|
|
/// that is published under the same lock so the snapshot is self-consistent by construction.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class HistorianDataSourceHealthSnapshotTests
|
|
{
|
|
/// <summary>
|
|
/// Drives the "half-published" state directly via reflection: set <c>_connection</c>
|
|
/// to a non-null sentinel but leave <c>_activeProcessNode</c> null. The snapshot must
|
|
/// report <c>ProcessConnectionOpen = false</c> and <c>ActiveProcessNode = null</c>
|
|
/// consistently — never a mismatch.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Snapshot_with_connection_set_but_active_node_null_is_consistent()
|
|
{
|
|
var ds = new HistorianDataSource(
|
|
new HistorianConfiguration { Enabled = true, ServerName = "h1" });
|
|
|
|
SetField(ds, "_connection", new HistorianAccess());
|
|
SetField(ds, "_activeProcessNode", (string?)null);
|
|
|
|
var snap = ds.GetHealthSnapshot();
|
|
(snap.ProcessConnectionOpen == (snap.ActiveProcessNode != null)).ShouldBeTrue(
|
|
"snapshot must not advertise open with no node — picks one source of truth");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Symmetric case for the event connection.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Snapshot_with_event_connection_set_but_active_node_null_is_consistent()
|
|
{
|
|
var ds = new HistorianDataSource(
|
|
new HistorianConfiguration { Enabled = true, ServerName = "h1" });
|
|
|
|
SetField(ds, "_eventConnection", new HistorianAccess());
|
|
SetField(ds, "_activeEventNode", (string?)null);
|
|
|
|
var snap = ds.GetHealthSnapshot();
|
|
(snap.EventConnectionOpen == (snap.ActiveEventNode != null)).ShouldBeTrue(
|
|
"snapshot must not advertise event open with no node");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The other direction: connection cleared but node still populated (the failure path
|
|
/// between the two field clears). The snapshot must still pair them consistently.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Snapshot_with_connection_cleared_but_active_node_populated_is_consistent()
|
|
{
|
|
var ds = new HistorianDataSource(
|
|
new HistorianConfiguration { Enabled = true, ServerName = "h1" });
|
|
|
|
SetField(ds, "_connection", (HistorianAccess?)null);
|
|
SetField(ds, "_activeProcessNode", "node-stale");
|
|
|
|
var snap = ds.GetHealthSnapshot();
|
|
(snap.ProcessConnectionOpen == (snap.ActiveProcessNode != null)).ShouldBeTrue(
|
|
"snapshot must not advertise closed with a node still set");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Steady-state happy path: both fields populated — snapshot reports both consistently.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Snapshot_with_both_fields_populated_reports_open_and_active_node()
|
|
{
|
|
var ds = new HistorianDataSource(
|
|
new HistorianConfiguration { Enabled = true, ServerName = "h1" });
|
|
|
|
SetField(ds, "_connection", new HistorianAccess());
|
|
SetField(ds, "_activeProcessNode", "h1");
|
|
|
|
var snap = ds.GetHealthSnapshot();
|
|
snap.ProcessConnectionOpen.ShouldBeTrue();
|
|
snap.ActiveProcessNode.ShouldBe("h1");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Steady-state default (no connect attempted): both null.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Snapshot_with_default_fields_reports_closed_with_no_active_node()
|
|
{
|
|
var ds = new HistorianDataSource(
|
|
new HistorianConfiguration { Enabled = true, ServerName = "h1" });
|
|
|
|
var snap = ds.GetHealthSnapshot();
|
|
snap.ProcessConnectionOpen.ShouldBeFalse();
|
|
snap.ActiveProcessNode.ShouldBeNull();
|
|
snap.EventConnectionOpen.ShouldBeFalse();
|
|
snap.ActiveEventNode.ShouldBeNull();
|
|
}
|
|
|
|
private static void SetField(object target, string name, object? value)
|
|
{
|
|
var f = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
f.ShouldNotBeNull($"private field '{name}' must exist on {target.GetType().Name}");
|
|
f!.SetValue(target, value);
|
|
}
|
|
}
|