using System.Collections.Concurrent; using System.Text.Json; using Akka.Actor; 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.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.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.VirtualTags; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; /// /// H5d (DI seam): pins that accepts an /// and threads it through to the REAL spawned /// (the virtualTagHostOverride is deliberately NOT used, /// so the production spawn path that wires historyWriter is exercised). /// /// /// This is a construction/threading smoke test, not a record-behaviour test. The record path — /// "a Historize=true result is recorded with the IHistoryWriter under the folder-scoped NodeId" — /// is already covered end-to-end against the host directly by /// VirtualTagHostActorTests.Historized_vtag_result_is_recorded_with_the_history_writer (H5c). /// Driving a real spawned child to emit a synthetic /// from inside would require reaching the auto-named grandchild and a /// live evaluator/mux feed — heavy and brittle for no extra coverage. So here we assert the writer /// threads through cleanly: a real apply carrying a Historize=true Equipment VirtualTag is Applied /// with the real host spawned and the capturing writer wired in, with no construction error. /// public sealed class DriverHostActorHistoryWriterTests : RuntimeActorTestBase { private static readonly NodeId TestNode = NodeId.Parse("driver-hw-test"); private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64)); /// Spawning a real DriverHostActor (no host override, publish actor wired so the real /// VirtualTagHostActor spawns) with a capturing IHistoryWriter applies a Historize=true VirtualTag /// deployment cleanly — proving Props/ctor accept the writer and thread it through the real spawn. [Fact] public void Props_accepts_history_writer_and_applies_historized_vtag_deployment() { var db = NewInMemoryDbFactory(); var deploymentId = SeedDeploymentWithHistorizedVirtualTag(db, RevA); var coordinator = CreateTestProbe(); // A publish actor MUST be wired for SpawnVirtualTagHost to spawn the real host (its ctor // requires a non-null sink). No virtualTagHostOverride ⇒ the real VirtualTagHostActor is // spawned with _historyWriter threaded in. var publish = CreateTestProbe(); var writer = new CapturingHistoryWriter(); var actor = Sys.ActorOf(DriverHostActor.Props( db, TestNode, coordinator.Ref, localRoles: new HashSet { "driver" }, opcUaPublishActor: publish.Ref, virtualTagEvaluator: NullVirtualTagEvaluator.Instance, historyWriter: writer)); actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId())); // The deployment Applies — the real VirtualTagHostActor spawned and accepted ApplyVirtualTags // with the writer threaded through (a construction/threading failure would have faulted PreStart // or the apply turn). Record behaviour itself is covered by VirtualTagHostActorTests (H5c). coordinator.ExpectMsg(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied); } private static DeploymentId SeedDeploymentWithHistorizedVirtualTag( IDbContextFactory db, RevisionHash rev) { var artifact = JsonSerializer.SerializeToUtf8Bytes(new { Scripts = new[] { new { ScriptId = "scr-1", SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;", }, }, VirtualTags = new[] { new { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Doubled", DataType = "Float", ScriptId = "scr-1", Historize = true, }, }, }); 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; } /// Capturing : records every Record call so the threading /// smoke test can be tightened later if a record assertion becomes feasible here. private sealed class CapturingHistoryWriter : IHistoryWriter { public readonly ConcurrentQueue<(string Path, DataValueSnapshot Value)> Calls = new(); /// Captures the path + snapshot of a Record call. /// The virtual tag path. /// The data value snapshot. public void Record(string path, DataValueSnapshot value) => Calls.Enqueue((path, value)); } }