From bba6d81c16e09aab6c44c2ae9b8d6845108a8b7e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 23:19:31 -0500 Subject: [PATCH] test(heidenhain): end-to-end through the real DriverInstanceActor (#803) Real Heidenhain driver (factory-built from a merged config) against the in-process LSV2 simulator inside DriverInstanceActor: subscribe publishes Good, an OPC write is refused by the runtime (no IWritable) with nothing sent to the control, an outage stamps BadNoCommunication, recovery re-publishes the identical value Good, and only allowlisted telegrams ever reach the simulated control. --- .../Drivers/HeidenhainDriverEndToEndTests.cs | 84 +++++++++++++++++++ .../ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj | 4 + 2 files changed, 88 insertions(+) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/HeidenhainDriverEndToEndTests.cs diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/HeidenhainDriverEndToEndTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/HeidenhainDriverEndToEndTests.cs new file mode 100644 index 000000000..4afb9da24 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/HeidenhainDriverEndToEndTests.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using Akka.Actor; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; +using ZB.MOM.WW.OtOpcUa.Driver.Heidenhain; +using ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.Tests.Simulator; +using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; +using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; + +namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; + +/// +/// lmxopcua#803 end-to-end through the REAL : the real Heidenhain driver +/// (built by its factory from a merged config) against the in-process LSV2 simulator — subscribe, OPC write +/// refusal, controller-side outbound restriction, outage invalidation and recovery. +/// +[Trait("Category", "Unit")] +public sealed class HeidenhainDriverEndToEndTests : RuntimeActorTestBase +{ + private const string Exec = "Plant/H/dev1/Status/ExecutionState"; + private const string Model = "Plant/H/dev1/Controller/Model"; + + private static string Config(int port) => JsonSerializer.Serialize(new + { + Devices = new[] { new { Host = "127.0.0.1", Port = port, DeviceName = "dev1" } }, + RawTags = new[] + { + new { RawPath = Exec, TagConfig = """{"address":"Status/ExecutionState"}""", WriteIdempotent = false, DeviceName = "dev1" }, + new { RawPath = Model, TagConfig = """{"address":"Controller/Model"}""", WriteIdempotent = false, DeviceName = "dev1" }, + }, + TimeoutMs = 500, + Polling = new { StatusMs = 100, CustomMs = 100, SlowMs = 200 }, + Reconnect = new { InitialDelayMs = 100, MaxDelayMs = 200 }, + }); + + private DriverInstanceActor.AttributeValueItem? AwaitItem(Akka.TestKit.TestProbe parent, string reference, Func match) + { + DriverInstanceActor.AttributeValueItem? found = null; + parent.FishForMessage(b => + { + foreach (var i in b.Items) + if (i.FullReference == reference && match(i)) { found = i; return true; } + return false; + }, TimeSpan.FromSeconds(10), cancellationToken: TestContext.Current.CancellationToken); + return found; + } + + [Fact] + public async Task Subscribe_write_refusal_outage_and_recovery_through_the_real_actor() + { + var control = new SimulatedControl(); + control.Runtime[23] = () => SimulatedControl.U16(4); + await using var sim = Lsv2Simulator.WithControl(control); + var driver = HeidenhainDriverFactoryExtensions.CreateInstance("hh-e2e", Config(sim.Port)); + var parent = CreateTestProbe(); + parent.IgnoreMessages(m => m is not DriverInstanceActor.AttributeValueBatchPublished); + var actor = parent.ChildActorOf( + DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(100), + healthPollInterval: TimeSpan.FromMilliseconds(100), commsLossTimeout: TimeSpan.FromSeconds(1)), + TestContext.Current.CancellationToken); + + actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions([Exec, Model], TimeSpan.FromMilliseconds(100))); + actor.Tell(new DriverInstanceActor.InitializeRequested(Config(sim.Port))); + + AwaitItem(parent, Exec, i => i.StatusCode == OpcUaStatus.Good)!.Value.Value.ShouldBe(4); + + // OPC write: refused by the runtime (the driver implements no IWritable) — nothing reaches the control. + var write = await actor.Ask( + new DriverInstanceActor.WriteAttribute(Exec, 9), TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken); + write.Success.ShouldBeFalse(); + + // Outage: every remote value goes BadNoCommunication. + sim.GoOffline(); + AwaitItem(parent, Exec, i => i.StatusCode == HeidenhainStatus.BadNoCommunication).ShouldNotBeNull(); + + // Recovery: the control comes back holding the SAME value, and it re-publishes Good. + sim.GoOnline(); + AwaitItem(parent, Exec, i => i.StatusCode == OpcUaStatus.Good)!.Value.Value.ShouldBe(4); + + control.Violations.ShouldBeEmpty(); + sim.Requests.Select(r => r.Command).Distinct().ShouldBeSubsetOf(new[] { "A_LG", "R_VR", "R_RI" }); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj index 276983f61..93ea8b0a9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj @@ -40,11 +40,15 @@ + + +