DriverHostActor.ApplyAndAck now reads the deployment artifact and reconciles its set of DriverInstanceActor children — spawn the missing, ApplyDelta to those with changed config, stop the removed/disabled. The diff lives in pure DriverSpawnPlanner so it can be unit-tested without an ActorSystem. Adds IDriverFactory in Core.Abstractions (consumed by Runtime) + DriverFactoryRegistryAdapter in Core.Hosting that wraps the existing v1 DriverFactoryRegistry — Runtime stays decoupled from Polly/Serilog, the Host wires the adapter once driver assemblies have registered. ShouldStub(type, roles) is now actually called on every spawn — Galaxy + Wonderware-Historian boot stubbed on macOS/Linux or whenever the host carries the dev role. Missing factory ⇒ stub fallback, never a crash. Tests: 24 → 34 in Runtime (+10): - DriverSpawnPlannerTests x7 (diff cases, type change ⇒ stop+respawn) - DeploymentArtifactTests x5 (empty/malformed/missing fields tolerant) - DriverHostActorReconcileTests x4 (spawn count, stub fallback, ShouldStub gate, second-apply stops the removed) All 6 v2 test suites green: 120 tests passing. Closes F20 (ShouldStub wired). F7 marked partial — subscription publishing + write path still stubbed in DriverInstanceActor itself.
94 lines
2.8 KiB
C#
94 lines
2.8 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
public sealed class DeploymentArtifactTests
|
|
{
|
|
[Fact]
|
|
public void Empty_blob_returns_empty_list()
|
|
{
|
|
DeploymentArtifact.ParseDriverInstances(ReadOnlySpan<byte>.Empty).ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Malformed_json_returns_empty_list()
|
|
{
|
|
DeploymentArtifact.ParseDriverInstances(Encoding.UTF8.GetBytes("not json")).ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Snapshot_without_DriverInstances_returns_empty()
|
|
{
|
|
var blob = Encoding.UTF8.GetBytes("{\"Clusters\":[]}");
|
|
DeploymentArtifact.ParseDriverInstances(blob).ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Parses_driver_instances_from_composer_shaped_blob()
|
|
{
|
|
// Mirrors the shape ConfigComposer.SnapshotAndFlattenAsync emits — Pascal-case fields
|
|
// serialised directly off the EF entity.
|
|
var rowId = Guid.NewGuid();
|
|
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
DriverInstances = new[]
|
|
{
|
|
new
|
|
{
|
|
DriverInstanceRowId = rowId,
|
|
DriverInstanceId = "DI-modbus-1",
|
|
Name = "Modbus Line A",
|
|
DriverType = "Modbus",
|
|
Enabled = true,
|
|
DriverConfig = "{\"host\":\"127.0.0.1\"}",
|
|
},
|
|
new
|
|
{
|
|
DriverInstanceRowId = Guid.NewGuid(),
|
|
DriverInstanceId = "DI-disabled",
|
|
Name = "Decommissioned",
|
|
DriverType = "AbCip",
|
|
Enabled = false,
|
|
DriverConfig = "{}",
|
|
},
|
|
},
|
|
});
|
|
|
|
var specs = DeploymentArtifact.ParseDriverInstances(blob);
|
|
|
|
specs.Count.ShouldBe(2);
|
|
specs[0].DriverInstanceRowId.ShouldBe(rowId);
|
|
specs[0].DriverInstanceId.ShouldBe("DI-modbus-1");
|
|
specs[0].DriverType.ShouldBe("Modbus");
|
|
specs[0].Enabled.ShouldBeTrue();
|
|
specs[0].DriverConfig.ShouldContain("127.0.0.1");
|
|
specs[1].Enabled.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Spec_missing_required_fields_is_dropped()
|
|
{
|
|
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
DriverInstances = new object[]
|
|
{
|
|
new { Name = "no-id" },
|
|
new
|
|
{
|
|
DriverInstanceId = "DI-ok",
|
|
DriverType = "Modbus",
|
|
DriverConfig = "{}",
|
|
},
|
|
},
|
|
});
|
|
|
|
var specs = DeploymentArtifact.ParseDriverInstances(blob);
|
|
|
|
specs.Single().DriverInstanceId.ShouldBe("DI-ok");
|
|
}
|
|
}
|