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; /// /// Shared v3 deployment fixture for the 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 _nodeIdByDriverRef is populated before the test publishes anything. /// /// 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. /// /// public static class V3ArtifactSeeder { /// 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 Plant/{DriverName}/{Device}/{Tag}; each UNS /// reference's NodeId is {Area}/{Line}/{Equip}/{Effective ?? backing tag Name}. Enums serialize /// numerically (AccessLevel: ReadWrite = 1). /// The in-memory config-db factory to seed. /// The revision hash the deployment is sealed under. /// The raw tags to materialise, one per (driver, device, tag). /// UNS references projecting a raw tag (by BackingTag) into an equipment. /// The seeded deployment's id. public static DeploymentId SeedV3Deployment( IDbContextFactory 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(), 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; } /// 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. /// The calling TestKit, supplying the ActorSystem + probes. /// The factory whose deployment was seeded by . /// The host's local node id. /// The deployment to dispatch. /// The revision hash the deployment was sealed under. /// The host actor plus the publish and mux probes wired into it. public static (IActorRef Actor, TestProbe Publish, TestProbe Mux) SpawnHostAndApply( TestKitBase kit, IDbContextFactory db, NodeId localNode, DeploymentId deploymentId, RevisionHash rev) { var publish = kit.CreateTestProbe(); var (actor, mux, coordinator) = Spawn(kit, db, localNode, publish.Ref); Dispatch(actor, coordinator, deploymentId, rev); publish.ExpectMsg(RuntimeActorTestBase.PresenceBudget); return (actor, publish, mux); } /// 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. /// The calling TestKit, supplying the ActorSystem + probes. /// The factory whose deployment was seeded by . /// The host's local node id. /// The deployment to dispatch. /// The revision hash the deployment was sealed under. /// The host actor plus the mux probe wired into it. public static (IActorRef Actor, TestProbe Mux) SpawnHostAndApplyWithoutPublishActor( TestKitBase kit, IDbContextFactory 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 db, NodeId localNode, IActorRef? opcUaPublishActor) { 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 { "driver" }, dependencyMux: mux.Ref, opcUaPublishActor: opcUaPublishActor, virtualTagEvaluator: NullVirtualTagEvaluator.Instance, virtualTagHostOverride: vtHost.Ref)); 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(RuntimeActorTestBase.PresenceBudget) .Outcome.ShouldBe(ApplyAckOutcome.Applied); } }