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.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <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" });
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,15 @@
|
||||
<!-- Lane S7 review I-1: the real S7 driver (over a scripted IS7Plc), so the comms-loss watchdog test proves an
|
||||
access-denied (PUT/GET disabled) CPU is NOT reconnected every 90 s, against the real actor. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.S7\ZB.MOM.WW.OtOpcUa.Driver.S7.csproj"/>
|
||||
<!-- lmxopcua#803: the real Heidenhain LSV2 driver over the in-process LSV2 simulator (linked below), so the
|
||||
end-to-end test proves subscribe / write refusal / comms-loss watchdog recovery through the real actor. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Heidenhain\ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Shared bind-race retry helper for tests that boot a real SDK server on a probed port. -->
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests\OpcUaHostStart.cs" Link="OpcUaHostStart.cs"/>
|
||||
<Compile Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.Tests\Simulator\*.cs" LinkBase="HeidenhainSimulator"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user