Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Harness/V3ArtifactSeeder.cs
T

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