bba6d81c16
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.
85 lines
4.2 KiB
C#
85 lines
4.2 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#803 end-to-end through the REAL <see cref="DriverInstanceActor"/>: 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.
|
|
/// </summary>
|
|
[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<DriverInstanceActor.AttributeValueItem, bool> match)
|
|
{
|
|
DriverInstanceActor.AttributeValueItem? found = null;
|
|
parent.FishForMessage<DriverInstanceActor.AttributeValueBatchPublished>(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<DriverInstanceActor.WriteAttributeResult>(
|
|
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" });
|
|
}
|
|
}
|