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; /// /// Driver.Historian.Wonderware-005 regression tests for . /// 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. /// [Trait("Category", "Unit")] public sealed class HistorianDataSourceHealthSnapshotTests { /// /// Drives the "half-published" state directly via reflection: set _connection /// to a non-null sentinel but leave _activeProcessNode null. The snapshot must /// report ProcessConnectionOpen = false and ActiveProcessNode = null /// consistently — never a mismatch. /// [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"); } /// /// Symmetric case for the event connection. /// [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"); } /// /// The other direction: connection cleared but node still populated (the failure path /// between the two field clears). The snapshot must still pair them consistently. /// [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"); } /// /// Steady-state happy path: both fields populated — snapshot reports both consistently. /// [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"); } /// /// Steady-state default (no connect attempted): both null. /// [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); } }