using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
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.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
{
private static readonly NodeId TestNode = NodeId.Parse("driver-test");
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
/// Verifies that applying a deployment with driver instances spawns one child per enabled row.
[Fact]
public void Apply_with_driver_instances_in_artifact_spawns_one_child_per_enabled_row()
{
var db = NewInMemoryDbFactory();
var factory = new CountingDriverFactory("Modbus");
var deploymentId = SeedDeploymentWithDrivers(db, RevA,
("DI-1", "Modbus", "{}", true),
("DI-2", "Modbus", "{}", true),
("DI-3", "Modbus", "{}", false)); // disabled — not spawned
var coordinator = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
driverFactory: factory,
localRoles: new HashSet { "driver" }));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
AwaitAssert(() => factory.CreateCount.ShouldBe(2), duration: TimeSpan.FromSeconds(3));
}
/// Verifies that applying a deployment with unsupported driver type falls back to stub.
[Fact]
public void Apply_with_unsupported_driver_type_falls_back_to_stub()
{
var db = NewInMemoryDbFactory();
// Factory only supports "Modbus" — the Galaxy row should boot stubbed.
var factory = new CountingDriverFactory("Modbus");
var deploymentId = SeedDeploymentWithDrivers(db, RevA,
("DI-galaxy", "Galaxy", "{}", true));
var coordinator = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
driverFactory: factory,
localRoles: new HashSet { "driver" }));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
// No real driver was constructed — stubbing took over.
factory.CreateCount.ShouldBe(0);
// GetDiagnostics should still report the (stubbed) child.
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
var snap = coordinator.ExpectMsg(TimeSpan.FromSeconds(2));
snap.Drivers.Count.ShouldBe(1);
snap.Drivers[0].State.ShouldBe("Stubbed");
}
/// Verifies that Galaxy driver on non-Windows is stubbed by ShouldStub check.
[Fact]
public void Galaxy_on_non_windows_is_stubbed_by_ShouldStub_check()
{
// Even if the factory could create it, ShouldStub('Galaxy', ...) returns true on macOS/Linux —
// the factory should never be called.
var db = NewInMemoryDbFactory();
var factory = new CountingDriverFactory("Galaxy");
var deploymentId = SeedDeploymentWithDrivers(db, RevA,
("DI-galaxy", "Galaxy", "{}", true));
var coordinator = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
driverFactory: factory,
localRoles: new HashSet { "driver" }));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg(TimeSpan.FromSeconds(5));
if (OperatingSystem.IsWindows())
{
factory.CreateCount.ShouldBe(1);
}
else
{
factory.CreateCount.ShouldBe(0); // ShouldStub forced the stub path
}
}
/// Verifies that a second apply with removed driver stops the child.
[Fact]
public void Second_apply_with_removed_driver_stops_the_child()
{
var db = NewInMemoryDbFactory();
var factory = new CountingDriverFactory("Modbus");
var d1 = SeedDeploymentWithDrivers(db, RevA, ("DI-1", "Modbus", "{}", true), ("DI-2", "Modbus", "{}", true));
var d2 = SeedDeploymentWithDrivers(db, RevB, ("DI-1", "Modbus", "{}", true));
var coordinator = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
driverFactory: factory,
localRoles: new HashSet { "driver" }));
actor.Tell(new DispatchDeployment(d1, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg(TimeSpan.FromSeconds(5));
AwaitAssert(() => factory.CreateCount.ShouldBe(2), duration: TimeSpan.FromSeconds(3));
actor.Tell(new DispatchDeployment(d2, RevB, CorrelationId.NewId()));
coordinator.ExpectMsg(TimeSpan.FromSeconds(5));
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
var snap = coordinator.ExpectMsg(TimeSpan.FromSeconds(2));
snap.Drivers.Count.ShouldBe(1);
}
private static DeploymentId SeedDeploymentWithDrivers(
IDbContextFactory db,
RevisionHash rev,
params (string Id, string Type, string Config, bool Enabled)[] drivers)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{
DriverInstances = drivers.Select(d => new
{
DriverInstanceRowId = Guid.NewGuid(),
DriverInstanceId = d.Id,
Name = d.Id,
DriverType = d.Type,
Enabled = d.Enabled,
DriverConfig = d.Config,
}).ToArray(),
});
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;
}
/// Test double for IDriverFactory that counts driver creation attempts.
private sealed class CountingDriverFactory : IDriverFactory
{
private readonly string _supportedType;
/// Gets the number of times TryCreate was called and returned a driver.
public int CreateCount;
/// Initializes a new instance with the specified supported driver type.
/// The driver type this factory supports.
public CountingDriverFactory(string supportedType) { _supportedType = supportedType; }
/// Attempts to create a driver if the type is supported.
/// The driver type to create.
/// The unique identifier for the driver instance.
/// The driver configuration in JSON format.
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
{
if (!string.Equals(driverType, _supportedType, StringComparison.Ordinal)) return null;
Interlocked.Increment(ref CreateCount);
return new TestDriver(driverInstanceId, driverType);
}
///
public IReadOnlyCollection SupportedTypes => new[] { _supportedType };
}
/// Test double for IDriver with minimal implementation.
private sealed class TestDriver : IDriver
{
///
public string DriverInstanceId { get; }
///
public string DriverType { get; }
/// Initializes a new test driver with the specified ID and type.
/// The driver instance ID.
/// The driver type.
public TestDriver(string id, string type) { DriverInstanceId = id; DriverType = type; }
///
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
///
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
///
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
///
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, LastError: null);
///
public long GetMemoryFootprint() => 0;
///
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}