131 lines
6.8 KiB
C#
131 lines
6.8 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
|
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>
|
|
/// The comms-loss fan (<c>DriverHostActor.FanCommsLossToDependencyMux</c>) end-to-end through a REAL
|
|
/// <see cref="DependencyMuxActor"/>, rather than the mux probe the other host suites use.
|
|
/// <para>
|
|
/// Two properties are pinned here that a probe cannot show. First, the fan is ONE
|
|
/// <see cref="DriverInstanceActor.AttributeValueBatchPublished"/> for the whole driver rather than one
|
|
/// Tell per ref — a driver owning 5k tags used to cost 5k messages on a single connectivity edge — and
|
|
/// the mux's batch path still delivers every item to an interested subscriber, so batching did not
|
|
/// quietly drop the delivery it exists to make.
|
|
/// </para>
|
|
/// <para>
|
|
/// Second, the driver scoping: the fan resolves its refs through a per-driver index built alongside
|
|
/// <c>_nodeIdByDriverRef</c> instead of scanning every entry in it, and an index is exactly the thing
|
|
/// that can key a lookup wrong. The two-driver case is therefore the falsification control — the
|
|
/// untouched driver's ref must be absent from the batch, which a scan-and-filter implementation and a
|
|
/// correctly-keyed index both satisfy but a mis-keyed one does not.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class DriverHostActorCommsLossTests : RuntimeActorTestBase
|
|
{
|
|
private static readonly NodeId TestNode = NodeId.Parse("driver-commsloss-test");
|
|
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('c', 64));
|
|
|
|
/// <summary>Spawns a host whose dependency mux is a REAL <see cref="DependencyMuxActor"/>, applies the
|
|
/// seeded deployment, and registers <paramref name="subscriber"/>'s interest in <paramref name="refs"/>.
|
|
/// </summary>
|
|
/// <param name="db">The factory whose deployment was seeded by <see cref="SeedV3Deployment"/>.</param>
|
|
/// <param name="deploymentId">The deployment to dispatch.</param>
|
|
/// <param name="subscriber">The probe standing in for a VirtualTagActor / scripted-alarm subscriber.</param>
|
|
/// <param name="refs">The RawPaths that subscriber registers interest in.</param>
|
|
/// <returns>The applied host actor.</returns>
|
|
private IActorRef SpawnHostWithRealMux(
|
|
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId, TestProbe subscriber, params string[] refs)
|
|
{
|
|
var coordinator = CreateTestProbe();
|
|
var mux = Sys.ActorOf(DependencyMuxActor.Props());
|
|
mux.Tell(new DependencyMuxActor.RegisterInterest(refs, subscriber.Ref));
|
|
|
|
var actor = Sys.ActorOf(DriverHostActor.Props(
|
|
db, TestNode, coordinator.Ref,
|
|
localRoles: new HashSet<string> { "driver" },
|
|
dependencyMux: mux,
|
|
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
|
|
virtualTagHostOverride: CreateTestProbe().Ref));
|
|
|
|
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
|
coordinator.ExpectMsg<ApplyAck>(PresenceBudget).Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
|
|
|
return actor;
|
|
}
|
|
|
|
/// <summary>A driver's comms loss reaches the mux as ONE batch carrying a Bad item per ref it owns, and
|
|
/// the mux delivers that whole slice to an interested subscriber in ONE
|
|
/// <see cref="VirtualTagActor.DependencyValueBatch"/>.</summary>
|
|
[Fact]
|
|
public void Comms_loss_reaches_the_mux_as_one_batch_of_bad_items()
|
|
{
|
|
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),
|
|
},
|
|
unsRefs: Array.Empty<(string, string, string, string, string?)>());
|
|
|
|
var subscriber = CreateTestProbe();
|
|
var actor = SpawnHostWithRealMux(db, deploymentId, subscriber,
|
|
"Plant/Modbus/dev1/speed", "Plant/Modbus/dev1/temp");
|
|
|
|
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: false));
|
|
|
|
var batch = subscriber.ExpectMsg<VirtualTagActor.DependencyValueBatch>(PresenceBudget);
|
|
batch.Items.Select(i => i.TagId).ShouldBe(
|
|
new[] { "Plant/Modbus/dev1/speed", "Plant/Modbus/dev1/temp" }, ignoreOrder: true);
|
|
foreach (var item in batch.Items)
|
|
{
|
|
item.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
|
item.Value.ShouldBeNull();
|
|
}
|
|
|
|
// ONE batch, not one per ref: the whole point of the change. A per-ref fan would land a second
|
|
// single-item DependencyValueBatch here.
|
|
subscriber.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
|
}
|
|
|
|
/// <summary>Falsification control for the per-driver index: with TWO drivers configured, one driver's
|
|
/// comms loss carries ONLY its own refs — the other driver's tag is untouched.</summary>
|
|
[Fact]
|
|
public void Comms_loss_batch_carries_only_the_dead_drivers_refs()
|
|
{
|
|
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: "Modbus2", Device: "dev2", Tag: "flow", DataType: "Double", Writable: false),
|
|
},
|
|
unsRefs: Array.Empty<(string, string, string, string, string?)>());
|
|
|
|
var subscriber = CreateTestProbe();
|
|
var actor = SpawnHostWithRealMux(db, deploymentId, subscriber,
|
|
"Plant/Modbus/dev1/speed", "Plant/Modbus/dev1/temp", "Plant/Modbus2/dev2/flow");
|
|
|
|
actor.Tell(new DriverInstanceActor.ConnectivityChanged("drv-1", Connected: false));
|
|
|
|
var batch = subscriber.ExpectMsg<VirtualTagActor.DependencyValueBatch>(PresenceBudget);
|
|
batch.Items.Select(i => i.TagId).ShouldBe(
|
|
new[] { "Plant/Modbus/dev1/speed", "Plant/Modbus/dev1/temp" }, ignoreOrder: true);
|
|
subscriber.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
|
}
|
|
}
|