Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Backend/HistorianDataSourceHealthSnapshotTests.cs
Joseph Doherty 1f29b215c8 fix(driver-historian-wonderware): resolve Low code-review findings (Driver.Historian.Wonderware-004,005,007,008,010,011,012)
- 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>
2026-05-23 08:18:10 -04:00

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);
}
}