Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactRawPathTests.cs
T
Joseph Doherty e81ac352ed v3 B1-WP4/WP5: pipeline rewire to greenfield schema + dark address space
- ConfigComposer: snapshot v3 tables (RawFolders/TagGroups/UnsTagReferences),
  drop the retired Namespaces snapshot; RevisionHash follows the new blob.
- DeploymentArtifact: compute each raw tag's RawPath (RawFolder->Driver->Device->
  TagGroup ancestry via shared RawPathResolver); per-driver RawTags + merged
  DriverConfig+DeviceConfig config injected into DriverInstanceSpec via
  DriverDeviceConfigMerger; ParseComposition emits an empty EquipmentTags set
  (DARK until Batch 4); EquipmentNode driver/device hooks always null; cluster
  scoping attributes equipment by UNS line only.
- AddressSpaceComposer: rewritten to the v3 shape (no Namespace/NamespaceKind,
  no dropped Tag/Equipment fields); emits empty EquipmentTags (parity mirror).
- DriverHostActor: fan-out/write map tuple member FullName -> RawPath.
- Commons: new RawPathResolver (shared identity authority) + DriverDeviceConfigMerger
  (single/multi-device endpoint+RawTags merge).
- DraftValidator: v3 rules — raw-name charset (RawPaths.ValidateSegment),
  historized effective-tagname <=255, UNS effective-leaf uniqueness across
  UnsTagReference/VirtualTag/ScriptedAlarm; DraftSnapshot(+Factory) carry the new tables.
- AdminOperationsActor: tag-config inspection resolves driver via Device (no
  EquipmentId/DriverInstanceId on Tag).
- Tests: RawPathResolver/merger unit tests (Commons.Tests, green) +
  DeploymentArtifact RawPath/dark test (Runtime.Tests, awaits WP6 corpus fixes).
2026-07-15 20:45:24 -04:00

90 lines
4.5 KiB
C#

using System.Text.Json;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// v3 (B1-WP4) — the deploy artifact must compute each raw tag's RawPath by walking
/// RawFolder → Driver → Device → TagGroup ancestry, inject the per-driver RawTags into the driver's
/// merged config, and fold the sole device's DeviceConfig endpoint up to the top level. A strong signal
/// for the docker-dev live gate (a driver that gets the wrong RawPath or a missing endpoint won't bind).
/// </summary>
public sealed class DeploymentArtifactRawPathTests
{
private static byte[] BlobOf(object snapshot) => JsonSerializer.SerializeToUtf8Bytes(snapshot);
private static object NestedSingleDeviceSnapshot() => new
{
Clusters = new[] { new { ClusterId = "MAIN" } },
Nodes = Array.Empty<object>(),
RawFolders = new[]
{
new { RawFolderId = "f1", ParentRawFolderId = (string?)null, Name = "Plant", ClusterId = "MAIN" },
new { RawFolderId = "f2", ParentRawFolderId = (string?)"f1", Name = "Line1", ClusterId = "MAIN" },
},
DriverInstances = new[]
{
new { DriverInstanceRowId = Guid.NewGuid(), DriverInstanceId = "drv1", Name = "Modbus1", DriverType = "Modbus", Enabled = true, DriverConfig = "{\"UnitId\":3}", ClusterId = "MAIN", RawFolderId = "f2" },
},
Devices = new[]
{
new { DeviceId = "dev1", DriverInstanceId = "drv1", Name = "PLC-A", DeviceConfig = "{\"Host\":\"10.0.0.5\",\"Port\":502}" },
},
TagGroups = new[]
{
new { TagGroupId = "g1", DeviceId = "dev1", ParentTagGroupId = (string?)null, Name = "Motors" },
new { TagGroupId = "g2", DeviceId = "dev1", ParentTagGroupId = (string?)"g1", Name = "M1" },
},
Tags = new[]
{
new { TagId = "t1", DeviceId = "dev1", TagGroupId = (string?)"g2", Name = "Speed", DataType = "Float", TagConfig = "{\"address\":\"40001\"}", WriteIdempotent = true },
new { TagId = "t2", DeviceId = "dev1", TagGroupId = (string?)null, Name = "Status", DataType = "Boolean", TagConfig = "{\"address\":\"1\"}", WriteIdempotent = false },
},
};
/// <summary>The nested folder + nested group tag resolves to
/// <c>Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed</c>; a device-root tag drops the group segments.</summary>
[Fact]
public void ParseDriverInstances_computes_nested_RawPath_and_merges_device_endpoint()
{
var specs = DeploymentArtifact.ParseDriverInstances(BlobOf(NestedSingleDeviceSnapshot()));
var spec = specs.ShouldHaveSingleItem();
spec.DriverInstanceId.ShouldBe("drv1");
using var doc = JsonDocument.Parse(spec.DriverConfig);
var root = doc.RootElement;
// Endpoint from the sole device's DeviceConfig merged up to the top level (single-endpoint driver).
root.GetProperty("Host").GetString().ShouldBe("10.0.0.5");
root.GetProperty("Port").GetInt32().ShouldBe(502);
// Protocol/channel setting from the driver-level DriverConfig survives.
root.GetProperty("UnitId").GetInt32().ShouldBe(3);
// RawTags injected, ordinal-ordered by RawPath, each carrying its computed RawPath + device name.
var rawTags = root.GetProperty("RawTags").EnumerateArray().ToList();
rawTags.Count.ShouldBe(2);
var paths = rawTags.Select(e => e.GetProperty("RawPath").GetString()).ToList();
paths.ShouldContain("Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed");
paths.ShouldContain("Plant/Line1/Modbus1/PLC-A/Status");
var speed = rawTags.Single(e => e.GetProperty("RawPath").GetString() == "Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed");
speed.GetProperty("WriteIdempotent").GetBoolean().ShouldBeTrue();
speed.GetProperty("DeviceName").GetString().ShouldBe("PLC-A");
// Reconstructed Devices array carries the device name (routing key) + endpoint.
var devices = root.GetProperty("Devices").EnumerateArray().ToList();
devices.ShouldHaveSingleItem();
devices[0].GetProperty("DeviceName").GetString().ShouldBe("PLC-A");
}
/// <summary>The DARK address space this batch: no equipment-tag variable plans materialize.</summary>
[Fact]
public void ParseComposition_emits_dark_address_space_no_equipment_tags()
{
var composition = DeploymentArtifact.ParseComposition(BlobOf(NestedSingleDeviceSnapshot()));
composition.EquipmentTags.ShouldBeEmpty();
}
}