179 lines
9.4 KiB
C#
179 lines
9.4 KiB
C#
using System.Text.Json;
|
|
using Akka.Actor;
|
|
using Akka.TestKit;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
|
|
/// <summary>
|
|
/// Shared v3 deployment fixture for the <see cref="DriverHostActor"/> value-routing suites: seeds a
|
|
/// Sealed artifact carrying the raw-tag chain (+ optional UNS references) and spawns a host that has
|
|
/// actually applied it, so <c>_nodeIdByDriverRef</c> is populated before the test publishes anything.
|
|
/// <para>
|
|
/// Kept here rather than in either test class because the per-value and batched fan-out suites
|
|
/// assert different deliveries over the SAME arrangement — duplicating the seeder let the two
|
|
/// copies drift. Deliberately narrow: the assembly's other artifact fixtures seed their own
|
|
/// shapes and are not folded in.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class V3ArtifactSeeder
|
|
{
|
|
/// <summary>Seeds a Sealed deployment whose artifact carries the v3 raw-tag chain (RawFolder "Plant" →
|
|
/// DriverInstance(RawFolderId) → Device → Tag) plus optional UnsTagReferences projecting a raw tag into an
|
|
/// Area/Line/Equipment. Each raw tag's RawPath is <c>Plant/{DriverName}/{Device}/{Tag}</c>; each UNS
|
|
/// reference's NodeId is <c>{Area}/{Line}/{Equip}/{Effective ?? backing tag Name}</c>. Enums serialize
|
|
/// numerically (AccessLevel: ReadWrite = 1).</summary>
|
|
/// <param name="db">The in-memory config-db factory to seed.</param>
|
|
/// <param name="rev">The revision hash the deployment is sealed under.</param>
|
|
/// <param name="rawTags">The raw tags to materialise, one per (driver, device, tag).</param>
|
|
/// <param name="unsRefs">UNS references projecting a raw tag (by <c>BackingTag</c>) into an equipment.</param>
|
|
/// <returns>The seeded deployment's id.</returns>
|
|
public static DeploymentId SeedV3Deployment(
|
|
IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev,
|
|
(string Driver, string DriverName, string Device, string Tag, string DataType, bool Writable)[] rawTags,
|
|
(string Area, string Line, string Equip, string BackingTag, string? Effective)[] unsRefs)
|
|
{
|
|
var drivers = rawTags
|
|
.Select(t => (t.Driver, t.DriverName))
|
|
.Distinct()
|
|
.Select(d => new { DriverInstanceId = d.Driver, RawFolderId = "rf-plant", Name = d.DriverName, DriverType = "Modbus", DriverConfig = "{}", ClusterId = "c1", Enabled = false })
|
|
.ToArray();
|
|
var devices = rawTags
|
|
.Select(t => (t.Driver, t.Device))
|
|
.Distinct()
|
|
.Select(d => new { DeviceId = $"{d.Driver}:{d.Device}", DriverInstanceId = d.Driver, Name = d.Device, DeviceConfig = "{}" })
|
|
.ToArray();
|
|
var tags = rawTags.Select(t => new
|
|
{
|
|
TagId = t.Tag, // tag id == name for test simplicity (unique per driver here)
|
|
DeviceId = $"{t.Driver}:{t.Device}",
|
|
TagGroupId = (string?)null,
|
|
Name = t.Tag,
|
|
DataType = t.DataType,
|
|
AccessLevel = t.Writable ? 1 : 0, // TagAccessLevel.ReadWrite = 1
|
|
TagConfig = "{}",
|
|
}).ToArray();
|
|
|
|
// UNS topology: one area/line per distinct (Area, Line); one equipment per (Area, Line, Equip).
|
|
var areas = unsRefs.Select(r => r.Area).Distinct(StringComparer.Ordinal)
|
|
.Select(a => new { UnsAreaId = $"a-{a}", Name = a, ClusterId = "c1" }).ToArray();
|
|
var lines = unsRefs.Select(r => (r.Area, r.Line)).Distinct()
|
|
.Select(l => new { UnsLineId = $"l-{l.Area}-{l.Line}", UnsAreaId = $"a-{l.Area}", Name = l.Line }).ToArray();
|
|
var equipment = unsRefs.Select(r => (r.Area, r.Line, r.Equip)).Distinct()
|
|
.Select(e => new { EquipmentId = $"EQ-{e.Area}-{e.Line}-{e.Equip}", UnsLineId = $"l-{e.Area}-{e.Line}", Name = e.Equip, MachineCode = e.Equip }).ToArray();
|
|
var unsTagReferences = unsRefs.Select((r, i) => new
|
|
{
|
|
UnsTagReferenceId = $"ref-{i}",
|
|
EquipmentId = $"EQ-{r.Area}-{r.Line}-{r.Equip}",
|
|
TagId = r.BackingTag,
|
|
DisplayNameOverride = r.Effective,
|
|
}).ToArray();
|
|
|
|
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
RawFolders = new[] { new { RawFolderId = "rf-plant", ParentRawFolderId = (string?)null, Name = "Plant", ClusterId = "c1" } },
|
|
DriverInstances = drivers,
|
|
Devices = devices,
|
|
TagGroups = Array.Empty<object>(),
|
|
Tags = tags,
|
|
UnsAreas = areas,
|
|
UnsLines = lines,
|
|
Equipment = equipment,
|
|
UnsTagReferences = unsTagReferences,
|
|
});
|
|
|
|
var id = DeploymentId.NewId();
|
|
using var ctx = db.CreateDbContext();
|
|
ctx.Deployments.Add(new Deployment
|
|
{
|
|
DeploymentId = id.Value,
|
|
RevisionHash = rev.Value,
|
|
Status = DeploymentStatus.Sealed,
|
|
CreatedBy = "test",
|
|
SealedAtUtc = DateTime.UtcNow,
|
|
ArtifactBlob = artifact,
|
|
});
|
|
ctx.SaveChanges();
|
|
return id;
|
|
}
|
|
|
|
/// <summary>Spawns the host with publish + mux probes, dispatches the deployment, and waits for the
|
|
/// Applied ACK so the map build in PushDesiredSubscriptions has completed before the test publishes a
|
|
/// value. A VirtualTag-host probe is injected so the real host isn't spawned.</summary>
|
|
/// <param name="kit">The calling TestKit, supplying the ActorSystem + probes.</param>
|
|
/// <param name="db">The factory whose deployment was seeded by <see cref="SeedV3Deployment"/>.</param>
|
|
/// <param name="localNode">The host's local node id.</param>
|
|
/// <param name="deploymentId">The deployment to dispatch.</param>
|
|
/// <param name="rev">The revision hash the deployment was sealed under.</param>
|
|
/// <param name="commsRecoveryAuditDelay">Optional 03/S15 audit delay — how long after a reconnect the
|
|
/// host checks for refs still latched <c>BadNoCommunication</c>. Null keeps the production default,
|
|
/// which is far longer than any test's lifetime.</param>
|
|
/// <returns>The host actor plus the publish and mux probes wired into it.</returns>
|
|
public static (IActorRef Actor, TestProbe Publish, TestProbe Mux) SpawnHostAndApply(
|
|
TestKitBase kit, IDbContextFactory<OtOpcUaConfigDbContext> db,
|
|
NodeId localNode, DeploymentId deploymentId, RevisionHash rev,
|
|
TimeSpan? commsRecoveryAuditDelay = null)
|
|
{
|
|
var publish = kit.CreateTestProbe();
|
|
var (actor, mux, coordinator) = Spawn(kit, db, localNode, publish.Ref, commsRecoveryAuditDelay);
|
|
|
|
Dispatch(actor, coordinator, deploymentId, rev);
|
|
publish.ExpectMsg<OpcUaPublishActor.RebuildAddressSpace>(RuntimeActorTestBase.PresenceBudget);
|
|
|
|
return (actor, publish, mux);
|
|
}
|
|
|
|
/// <summary>The same arrangement with NO OPC UA publish actor wired (the ctor default) — the dev/Mac
|
|
/// shape, where the mux hop must still happen. Returns only the mux probe, since there is no sink.</summary>
|
|
/// <param name="kit">The calling TestKit, supplying the ActorSystem + probes.</param>
|
|
/// <param name="db">The factory whose deployment was seeded by <see cref="SeedV3Deployment"/>.</param>
|
|
/// <param name="localNode">The host's local node id.</param>
|
|
/// <param name="deploymentId">The deployment to dispatch.</param>
|
|
/// <param name="rev">The revision hash the deployment was sealed under.</param>
|
|
/// <returns>The host actor plus the mux probe wired into it.</returns>
|
|
public static (IActorRef Actor, TestProbe Mux) SpawnHostAndApplyWithoutPublishActor(
|
|
TestKitBase kit, IDbContextFactory<OtOpcUaConfigDbContext> db,
|
|
NodeId localNode, DeploymentId deploymentId, RevisionHash rev)
|
|
{
|
|
var (actor, mux, coordinator) = Spawn(kit, db, localNode, opcUaPublishActor: null);
|
|
Dispatch(actor, coordinator, deploymentId, rev);
|
|
return (actor, mux);
|
|
}
|
|
|
|
private static (IActorRef Actor, TestProbe Mux, TestProbe Coordinator) Spawn(
|
|
TestKitBase kit, IDbContextFactory<OtOpcUaConfigDbContext> db, NodeId localNode,
|
|
IActorRef? opcUaPublishActor, TimeSpan? commsRecoveryAuditDelay = null)
|
|
{
|
|
var coordinator = kit.CreateTestProbe();
|
|
var mux = kit.CreateTestProbe();
|
|
var vtHost = kit.CreateTestProbe();
|
|
|
|
var actor = kit.Sys.ActorOf(DriverHostActor.Props(
|
|
db, localNode, coordinator.Ref,
|
|
localRoles: new HashSet<string> { "driver" },
|
|
dependencyMux: mux.Ref,
|
|
opcUaPublishActor: opcUaPublishActor,
|
|
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
|
|
virtualTagHostOverride: vtHost.Ref,
|
|
commsRecoveryAuditDelay: commsRecoveryAuditDelay));
|
|
|
|
return (actor, mux, coordinator);
|
|
}
|
|
|
|
private static void Dispatch(IActorRef actor, TestProbe coordinator, DeploymentId deploymentId, RevisionHash rev)
|
|
{
|
|
actor.Tell(new DispatchDeployment(deploymentId, rev, CorrelationId.NewId()));
|
|
coordinator.ExpectMsg<ApplyAck>(RuntimeActorTestBase.PresenceBudget)
|
|
.Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
|
}
|
|
}
|