fix(runtime): comms loss marks raw + UNS value nodes BadNoCommunication, value retained (archreview 03/S15)
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
||||
using Akka.Actor;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
using static ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness.V3ArtifactSeeder;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Archreview <b>03/S15</b> — the value-node half of the comms-loss fan-out. Until this, a driver
|
||||
/// declared comms-lost annotated its Part 9 conditions and pushed a synthetic Bad into the dependency
|
||||
/// mux, but its <b>raw and UNS value variables</b> — the product's external surface — got nothing: an
|
||||
/// OPC UA client subscribed to a value node saw the last value at <b>Good</b> quality for the whole
|
||||
/// outage, for every driver whose failure shape is "reader stops producing" rather than "per-tag Bad
|
||||
/// snapshot".
|
||||
///
|
||||
/// <para><b>Status-only, value retained.</b> The write goes through
|
||||
/// <see cref="OpcUaPublishActor.AttributeStatusBatchUpdate"/> so the node keeps its last known value
|
||||
/// under <c>BadNoCommunication</c> (OPC UA Part 8's convention). The host holds no value cache — v3's
|
||||
/// single-source design — so only the node itself can retain it.</para>
|
||||
///
|
||||
/// <para><b>Nothing is published on reconnect</b>, deliberately: publishing Good would assert a
|
||||
/// quality this actor has not observed. Recovery is the driver's own re-publish (the poll engine's
|
||||
/// <c>forceRaise</c> on a fresh subscription, or the push-model initial push). That contract is
|
||||
/// end-to-end tested in <see cref="DriverHostCommsLossRecoveryTests"/>.</para>
|
||||
/// </summary>
|
||||
public sealed class DriverHostActorCommsLossValueNodesTests : RuntimeActorTestBase
|
||||
{
|
||||
private static readonly NodeId TestNode = NodeId.Parse("driver-commsloss-values");
|
||||
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('c', 64));
|
||||
|
||||
/// <summary>
|
||||
/// A disconnect produces ONE status batch naming exactly the disconnected driver's raw nodes plus
|
||||
/// every referencing UNS mirror, all <c>BadNoCommunication</c>. The second driver's ref is the
|
||||
/// control: a fan-out that walked every value ref on the node instead of the per-driver index would
|
||||
/// mark a perfectly healthy driver's nodes Bad, and no assertion about the first driver's nodes
|
||||
/// would notice.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Disconnect_marks_the_drivers_raw_and_uns_value_nodes_BadNoCommunication()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[]
|
||||
{
|
||||
(Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false),
|
||||
(Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "temp", DataType: "Double", Writable: false),
|
||||
(Driver: "drv-2", DriverName: "S7", Device: "dev2", Tag: "other", DataType: "Double", Writable: false),
|
||||
},
|
||||
unsRefs: new[] { (Area: "filling", Line: "line1", Equip: "station1", BackingTag: "speed", Effective: (string?)null) });
|
||||
|
||||
var (actor, publish, _) = SpawnHostAndApply(this, db, TestNode, deploymentId, RevA);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: false));
|
||||
|
||||
var batch = publish.ExpectMsg<OpcUaPublishActor.AttributeStatusBatchUpdate>(PresenceBudget);
|
||||
batch.Items.Select(i => i.NodeId).ShouldBe(new[]
|
||||
{
|
||||
"Plant/Modbus/dev1/speed",
|
||||
"filling/line1/station1/speed",
|
||||
"Plant/Modbus/dev1/temp",
|
||||
}, ignoreOrder: true);
|
||||
|
||||
batch.Items.ShouldAllBe(i => i.StatusCode == OpcUaStatus.BadNoCommunication);
|
||||
batch.Items.Single(i => i.NodeId == "Plant/Modbus/dev1/speed").Realm.ShouldBe(AddressSpaceRealm.Raw);
|
||||
batch.Items.Single(i => i.NodeId == "filling/line1/station1/speed").Realm.ShouldBe(AddressSpaceRealm.Uns);
|
||||
|
||||
// The control: the OTHER driver's node is untouched by drv-1's outage.
|
||||
batch.Items.ShouldNotContain(i => i.NodeId == "Plant/S7/dev2/other");
|
||||
|
||||
// ONE batch, not one message per node.
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A reconnect publishes <b>nothing</b> to the value nodes. Writing Good here would claim a quality
|
||||
/// the host has not observed — the device may be answering the transport and still returning
|
||||
/// garbage — and it would race the driver's own first real value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Reconnect_publishes_no_value_or_status_message()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[]
|
||||
{
|
||||
(Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false),
|
||||
},
|
||||
unsRefs: Array.Empty<(string, string, string, string, string?)>());
|
||||
|
||||
var (actor, publish, _) = SpawnHostAndApply(this, db, TestNode, deploymentId, RevA);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: false));
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeStatusBatchUpdate>(PresenceBudget);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: true));
|
||||
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A driver that owns no value refs produces no message at all — an empty batch would cost a hop
|
||||
/// and a sink call for nothing on every flap of an alarm-only or freshly-added driver.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_driver_with_no_value_refs_publishes_nothing()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[]
|
||||
{
|
||||
(Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false),
|
||||
},
|
||||
unsRefs: Array.Empty<(string, string, string, string, string?)>());
|
||||
|
||||
var (actor, publish, _) = SpawnHostAndApply(this, db, TestNode, deploymentId, RevA);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-unknown", Connected: false));
|
||||
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user