Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs
Joseph Doherty da141497f8 feat(runtime): F7 spawn lifecycle + F20 ShouldStub gate
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.
2026-05-26 08:57:16 -04:00

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");
}
}