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).
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using System.Text.Json;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// v3 (B1-WP4) identity — the shared <see cref="RawPathResolver"/> must build a raw tag's RawPath by
|
||||
/// walking RawFolder → Driver → Device → TagGroup ancestry, and <see cref="DriverDeviceConfigMerger"/>
|
||||
/// must fold the endpoint + RawTags into the single driver config. This is the byte-parity authority
|
||||
/// both the deploy artifact and the draft validator compute against.
|
||||
/// </summary>
|
||||
public sealed class RawPathResolverTests
|
||||
{
|
||||
private static RawPathResolver Nested() => new(
|
||||
folders: new Dictionary<string, (string?, string)>
|
||||
{
|
||||
["f1"] = (null, "Plant"),
|
||||
["f2"] = ("f1", "Line1"),
|
||||
},
|
||||
drivers: new Dictionary<string, (string?, string)>
|
||||
{
|
||||
["drv1"] = ("f2", "Modbus1"),
|
||||
["drvRoot"] = (null, "S7Root"),
|
||||
},
|
||||
devices: new Dictionary<string, (string, string)>
|
||||
{
|
||||
["dev1"] = ("drv1", "PLC-A"),
|
||||
["devRoot"] = ("drvRoot", "PLC-B"),
|
||||
},
|
||||
groups: new Dictionary<string, (string?, string)>
|
||||
{
|
||||
["g1"] = (null, "Motors"),
|
||||
["g2"] = ("g1", "M1"),
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void TryBuildTagPath_walks_folder_driver_device_group_ancestry()
|
||||
{
|
||||
Nested().TryBuildTagPath("dev1", "g2", "Speed")
|
||||
.ShouldBe("Plant/Line1/Modbus1/PLC-A/Motors/M1/Speed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuildTagPath_device_root_tag_has_no_group_segments()
|
||||
{
|
||||
Nested().TryBuildTagPath("dev1", null, "Status")
|
||||
.ShouldBe("Plant/Line1/Modbus1/PLC-A/Status");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuildTagPath_cluster_root_driver_has_no_folder_segments()
|
||||
{
|
||||
Nested().TryBuildTagPath("devRoot", null, "Run")
|
||||
.ShouldBe("S7Root/PLC-B/Run");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuildTagPath_unknown_device_returns_null()
|
||||
{
|
||||
Nested().TryBuildTagPath("ghost", null, "X").ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBuildDriverPrefix_is_folders_plus_driver_name()
|
||||
{
|
||||
Nested().TryBuildDriverPrefix("drv1").ShouldBe("Plant/Line1/Modbus1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_single_device_folds_endpoint_up_and_injects_rawtags()
|
||||
{
|
||||
var merged = DriverDeviceConfigMerger.Merge(
|
||||
driverConfigJson: "{\"UnitId\":3}",
|
||||
devices: new[] { new DriverDeviceConfigMerger.DeviceRow("PLC-A", "{\"Host\":\"10.0.0.5\",\"Port\":502}") },
|
||||
rawTags: new[] { new RawTagEntry("Plant/Modbus1/PLC-A/Speed", "{\"address\":\"40001\"}", true, "PLC-A") });
|
||||
|
||||
using var doc = JsonDocument.Parse(merged);
|
||||
var root = doc.RootElement;
|
||||
root.GetProperty("UnitId").GetInt32().ShouldBe(3);
|
||||
root.GetProperty("Host").GetString().ShouldBe("10.0.0.5"); // single-device endpoint merged up
|
||||
root.GetProperty("Port").GetInt32().ShouldBe(502);
|
||||
var rawTags = root.GetProperty("RawTags").EnumerateArray().ToList();
|
||||
rawTags.ShouldHaveSingleItem();
|
||||
rawTags[0].GetProperty("RawPath").GetString().ShouldBe("Plant/Modbus1/PLC-A/Speed");
|
||||
var devices = root.GetProperty("Devices").EnumerateArray().ToList();
|
||||
devices.ShouldHaveSingleItem();
|
||||
devices[0].GetProperty("DeviceName").GetString().ShouldBe("PLC-A");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_multi_device_reconstructs_devices_array_with_names()
|
||||
{
|
||||
var merged = DriverDeviceConfigMerger.Merge(
|
||||
driverConfigJson: "{\"Series\":\"0i\"}",
|
||||
devices: new[]
|
||||
{
|
||||
new DriverDeviceConfigMerger.DeviceRow("PLC-A", "{\"HostAddress\":\"10.0.0.5:8193\"}"),
|
||||
new DriverDeviceConfigMerger.DeviceRow("PLC-B", "{\"HostAddress\":\"10.0.0.6:8193\"}"),
|
||||
},
|
||||
rawTags: Array.Empty<RawTagEntry>());
|
||||
|
||||
using var doc = JsonDocument.Parse(merged);
|
||||
var devices = doc.RootElement.GetProperty("Devices").EnumerateArray().ToList();
|
||||
devices.Count.ShouldBe(2);
|
||||
devices.Select(d => d.GetProperty("DeviceName").GetString()).ShouldBe(new[] { "PLC-A", "PLC-B" });
|
||||
devices[0].GetProperty("HostAddress").GetString().ShouldBe("10.0.0.5:8193");
|
||||
// Multiple devices: no top-level endpoint merge-up (per-device only).
|
||||
doc.RootElement.TryGetProperty("HostAddress", out _).ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user