feat(runtime): DriverInstanceActor state machine with Connecting/Connected/Reconnecting
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using Akka.Actor;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task ApplyDelta_when_Connected_calls_ReinitializeAsync_and_replies_success()
|
||||
{
|
||||
var driver = new StubDriver();
|
||||
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
||||
|
||||
// Drive: Initialize → Connected.
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
||||
|
||||
// Issue ApplyDelta and capture the reply via Ask.
|
||||
var correlation = CorrelationId.NewId();
|
||||
var reply = await actor.Ask<DriverInstanceActor.ApplyResult>(
|
||||
new DriverInstanceActor.ApplyDelta("{\"changed\":true}", correlation),
|
||||
TimeSpan.FromSeconds(3));
|
||||
|
||||
reply.Success.ShouldBeTrue();
|
||||
reply.Correlation.ShouldBe(correlation);
|
||||
driver.ReinitializeCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_failure_keeps_actor_in_Reconnecting_state()
|
||||
{
|
||||
var driver = new StubDriver { InitializeShouldThrow = true };
|
||||
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
|
||||
// The actor should keep trying — we expect multiple Initialize calls because the
|
||||
// reconnect timer fires every 50ms.
|
||||
AwaitCondition(() => driver.InitializeCount >= 3, TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_against_non_IWritable_driver_returns_failure()
|
||||
{
|
||||
var driver = new StubDriver(); // IDriver only, no IWritable.
|
||||
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
||||
|
||||
var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
|
||||
new DriverInstanceActor.WriteAttribute("tag-1", 42),
|
||||
TimeSpan.FromSeconds(3));
|
||||
|
||||
reply.Success.ShouldBeFalse();
|
||||
reply.Reason!.ShouldContain("IWritable");
|
||||
}
|
||||
|
||||
private sealed class StubDriver : IDriver
|
||||
{
|
||||
public bool InitializeShouldThrow { get; set; }
|
||||
public int InitializeCount;
|
||||
public int ReinitializeCount;
|
||||
|
||||
public string DriverInstanceId => "stub-driver-1";
|
||||
public string DriverType => "Stub";
|
||||
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref InitializeCount);
|
||||
if (InitializeShouldThrow) throw new InvalidOperationException("stub-init-fail");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref ReinitializeCount);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
public long GetMemoryFootprint() => 0;
|
||||
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user