diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs index 645fbb5e..72c1ea61 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/DriverReconnectE2eTests.cs @@ -225,14 +225,7 @@ public sealed class DriverReconnectE2eTests CreatedBy = "test", }); - db.Namespaces.Add(new Namespace - { - NamespaceId = "RECONNECT-E2E-equipment", - ClusterId = ClusterId, - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:zb:reconnect-e2e:equipment", - }); - + // v3: the Namespace entity is deleted (DriverInstance no longer binds a Namespace). db.ClusterNodes.Add(new ClusterNode { NodeId = harness.NodeANodeId, @@ -258,7 +251,6 @@ public sealed class DriverReconnectE2eTests { DriverInstanceId = DriverId, ClusterId = ClusterId, - NamespaceId = "RECONNECT-E2E-equipment", Name = DriverId, DriverType = "Modbus", Enabled = true, diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs index e95e2e1e..03fd19ca 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/EquipmentNamespaceMaterializationTests.cs @@ -13,20 +13,32 @@ using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// -/// End-to-end deploy of an Equipment-kind namespace through the real -/// ConfigComposer: seed a 1-area / 1-line / 1-equipment / 1-tag Equipment namespace, -/// StartDeployment, then assert the deployment's persisted artifact decodes (via -/// ) to the equipment signal + the friendly -/// UNS folder name. This covers the ConfigComposer → ArtifactBlob → ParseComposition.EquipmentTags -/// seam that the OpcUaServer unit tests only approximate with hand-built JSON. +/// End-to-end deploy of a UNS folder hierarchy through the real ConfigComposer: seed a +/// 1-area / 1-line / 1-equipment UNS plus a v3 raw-tag chain (RawFolder → DriverInstance → Device → +/// Tag), StartDeployment, then assert the deployment's persisted artifact decodes (via +/// ). +/// +/// v3 DARK address space (Batch 1): the composer + artifact deliberately emit an EMPTY +/// equipment-tag plan set — raw-tag variable nodes are lit up in Batch 4's dual-namespace UNS↔Raw +/// fan-out. So the live assertion here is the DARK contract: the Area/Line/Equipment folder nodes +/// decode with their friendly UNS names, and EquipmentTags is empty EVEN THOUGH a raw tag was +/// seeded. The full equipment-signal materialization (raw tag → EquipmentTag with FullName) is +/// preserved as a Batch-4-pending Skipped test below (the plan explicitly says keep it, don't delete). +/// /// /// The OPC UA address-space browse is exercised separately against a real SDK node manager in -/// AddressSpaceApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK, -/// because the in-process binds the no-op address-space sink. +/// AddressSpaceApplierHierarchyTests, because the in-process +/// binds the no-op address-space sink. /// /// public sealed class EquipmentNamespaceMaterializationTests { + private const string Batch4Pending = + "v3 dark address space: equipment-tag variable plans (raw tag → EquipmentTag with FullName) do " + + "not materialize until Batch 4 (dual-namespace UNS↔Raw fan-out). The composer + artifact emit an " + + "empty equipment-tag plan set this batch; re-enable this full-materialization assertion (re-authored " + + "against the UnsTagReference fan-out) when Batch 4 lights the raw/UNS variable nodes."; + private static CancellationToken Ct => TestContext.Current.CancellationToken; // Equipment.EquipmentId must equal DraftValidator.DeriveEquipmentId(EquipmentUuid) — the @@ -35,16 +47,18 @@ public sealed class EquipmentNamespaceMaterializationTests private static readonly Guid EquipmentUuid = Guid.Parse("11111111-1111-1111-1111-111111111111"); private static readonly string EquipmentId = DraftValidator.DeriveEquipmentId(EquipmentUuid); // EQ-111111111111 - /// Verifies a deployed Equipment namespace carries its signal into the composed artifact. + /// Verifies a deployed UNS hierarchy carries its Area/Line/Equipment folder nodes into the + /// composed artifact with their friendly UNS names, and — the v3 Batch-1 DARK contract — that the + /// equipment-tag plan set is EMPTY even though a raw tag was seeded (raw-tag variables are Batch 4). [Fact] - public async Task Deploying_an_equipment_namespace_carries_the_signal_into_the_artifact() + public async Task Deploying_a_uns_hierarchy_carries_folder_nodes_and_leaves_equipment_tags_dark() { await using var harness = await TwoNodeClusterHarness.StartAsync(); // Seed the parent ServerCluster + ClusterNode rows the SQL FKs require (the c1 config below // FKs to ServerCluster; on the in-memory provider this is unnecessary — FKs aren't enforced). await harness.SeedDefaultClusterAsync("c1"); - await SeedEquipmentNamespaceAsync(harness); + await SeedUnsHierarchyAsync(harness); await using var scope = harness.NodeA.Services.CreateAsyncScope(); var client = scope.ServiceProvider.GetRequiredService(); @@ -71,47 +85,88 @@ public sealed class EquipmentNamespaceMaterializationTests var composition = DeploymentArtifact.ParseComposition(artifact); - // The real ConfigComposer serialised the equipment Tag (incl. its TagConfig), and - // ParseComposition extracted it as an EquipmentTag with FullName pulled from TagConfig. + // The UNS folder hierarchy decodes with its friendly UNS names (browse names), not MachineCodes. + composition.EquipmentNodes.ShouldContain(e => e.EquipmentId == EquipmentId && e.DisplayName == "station-1"); + composition.UnsAreas.ShouldContain(a => a.UnsAreaId == "nw-area-filling" && a.DisplayName == "filling"); + composition.UnsLines.ShouldContain(l => l.UnsLineId == "nw-line-1" && l.DisplayName == "line-1"); + + // v3 DARK contract: no equipment-tag variable plans materialize this batch — even though a raw + // tag ("Speed" / FullName 40001) was seeded under the device. Batch 4 lights these via the UNS↔Raw fan-out. + composition.EquipmentTags.ShouldBeEmpty(); + } + + /// BATCH-4-PENDING: the full equipment-signal materialization — the seeded raw tag decodes + /// (via the real ConfigComposer → ParseComposition) into an EquipmentTag carrying its FullName. Dark + /// until Batch 4's UNS↔Raw fan-out; preserved (not deleted) so re-enabling is a one-line change once + /// the equipment-tag plan set is lit up. + [Fact(Skip = Batch4Pending)] + public async Task Deploying_an_equipment_namespace_carries_the_signal_into_the_artifact() + { + await using var harness = await TwoNodeClusterHarness.StartAsync(); + await harness.SeedDefaultClusterAsync("c1"); + await SeedUnsHierarchyAsync(harness); + + await using var scope = harness.NodeA.Services.CreateAsyncScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + var result = await client.StartDeploymentAsync(createdBy: "alice@test", Ct); + result.Outcome.ShouldBe(StartDeploymentOutcome.Accepted, $"Deploy not accepted: {result.Message}"); + var deploymentId = result.DeploymentId!.Value.Value; + + var artifact = Array.Empty(); + await WaitForAsync(async () => + { + await using var db = await CreateDbAsync(harness); + var d = await db.Deployments.AsNoTracking() + .FirstOrDefaultAsync(x => x.DeploymentId == deploymentId, Ct); + if (d is { ArtifactBlob.Length: > 0 }) + { + artifact = d.ArtifactBlob; + return true; + } + return false; + }, TimeSpan.FromSeconds(15)); + + var composition = DeploymentArtifact.ParseComposition(artifact); + + // Batch 4: the raw tag surfaces as an EquipmentTag with FullName pulled from TagConfig. var tag = composition.EquipmentTags.ShouldHaveSingleItem(); tag.TagId.ShouldBe("tag-speed"); tag.EquipmentId.ShouldBe(EquipmentId); tag.Name.ShouldBe("Speed"); tag.DataType.ShouldBe("Float"); tag.FullName.ShouldBe("40001"); - - // The equipment folder browses by its friendly UNS Name, not the MachineCode. - composition.EquipmentNodes.ShouldContain(e => e.EquipmentId == EquipmentId && e.DisplayName == "station-1"); - composition.UnsAreas.ShouldContain(a => a.UnsAreaId == "nw-area-filling" && a.DisplayName == "filling"); - composition.UnsLines.ShouldContain(l => l.UnsLineId == "nw-line-1" && l.DisplayName == "line-1"); } - private static async Task SeedEquipmentNamespaceAsync(TwoNodeClusterHarness harness) + private static async Task SeedUnsHierarchyAsync(TwoNodeClusterHarness harness) { await using var db = await CreateDbAsync(harness); - db.Namespaces.Add(new Namespace - { - NamespaceId = "ns-eq", ClusterId = "c1", Kind = NamespaceKind.Equipment, NamespaceUri = "urn:eq", - }); + // v3 raw-tag chain: RawFolder → DriverInstance(RawFolderId) → Device(DriverInstanceId) → + // Tag(DeviceId). The Namespace entity is deleted; equipment no longer binds a driver/device. + db.RawFolders.Add(new RawFolder { RawFolderId = "raw-plant", ClusterId = "c1", Name = "Plant" }); // Disabled so the driver-role nodes don't spawn a live Modbus driver (no endpoint to reach); // the composition still carries the instance (ParseComposition does not filter on Enabled). db.DriverInstances.Add(new DriverInstance { - DriverInstanceId = "drv-modbus", ClusterId = "c1", NamespaceId = "ns-eq", + DriverInstanceId = "drv-modbus", ClusterId = "c1", RawFolderId = "raw-plant", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}", Enabled = false, }); + db.Devices.Add(new Device + { + DeviceId = "dev-1", DriverInstanceId = "drv-modbus", + Name = "dev-1", DeviceConfig = "{}", + }); db.UnsAreas.Add(new UnsArea { UnsAreaId = "nw-area-filling", ClusterId = "c1", Name = "filling" }); db.UnsLines.Add(new UnsLine { UnsLineId = "nw-line-1", UnsAreaId = "nw-area-filling", Name = "line-1" }); db.Equipment.Add(new Equipment { EquipmentId = EquipmentId, EquipmentUuid = EquipmentUuid, - DriverInstanceId = "drv-modbus", UnsLineId = "nw-line-1", - Name = "station-1", MachineCode = "STATION_001", + UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001", }); db.Tags.Add(new Tag { - TagId = "tag-speed", DriverInstanceId = "drv-modbus", EquipmentId = EquipmentId, + TagId = "tag-speed", DeviceId = "dev-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"40001\"}", }); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs index a554b8d2..c7d75e5e 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/MultiClusterScopingTests.cs @@ -120,17 +120,15 @@ public sealed class MultiClusterScopingTests NewCluster(MainCluster, "Main Cluster", "central"), NewCluster(SiteACluster, "Site A Cluster", "site-a")); - db.Namespaces.AddRange( - NewNamespace(MainCluster, "MAIN-equipment", "urn:zb:central:equipment"), - NewNamespace(SiteACluster, "SITE-A-equipment", "urn:zb:site-a:equipment")); - + // v3: the Namespace entity is deleted (DriverInstance no longer binds a Namespace). Cluster + // scoping filters by NodeId/ClusterId, so the namespace seed is dropped here. db.ClusterNodes.AddRange( NewNode(mainNodeId, MainCluster, "urn:zb:central:node-a"), NewNode(siteANodeId, SiteACluster, "urn:zb:site-a:node-b")); db.DriverInstances.AddRange( - NewDriver(MainDriverId, MainCluster, "MAIN-equipment"), - NewDriver(SiteADriverId, SiteACluster, "SITE-A-equipment")); + NewDriver(MainDriverId, MainCluster), + NewDriver(SiteADriverId, SiteACluster)); await db.SaveChangesAsync(Ct); } @@ -146,14 +144,6 @@ public sealed class MultiClusterScopingTests CreatedBy = "test", }; - private static Namespace NewNamespace(string clusterId, string namespaceId, string uri) => new() - { - NamespaceId = namespaceId, - ClusterId = clusterId, - Kind = NamespaceKind.Equipment, - NamespaceUri = uri, - }; - private static ClusterNode NewNode(string nodeId, string clusterId, string applicationUri) => new() { NodeId = nodeId, @@ -163,11 +153,10 @@ public sealed class MultiClusterScopingTests CreatedBy = "test", }; - private static DriverInstance NewDriver(string driverInstanceId, string clusterId, string namespaceId) => new() + private static DriverInstance NewDriver(string driverInstanceId, string clusterId) => new() { DriverInstanceId = driverInstanceId, ClusterId = clusterId, - NamespaceId = namespaceId, Name = driverInstanceId, DriverType = "Modbus", Enabled = true, diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs index 6215eea9..14cc8f87 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs @@ -165,7 +165,7 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable /// network-browse E2E (Host.IntegrationTests) needs the docker-dev fixture and is tracked /// as a follow-up. /// - [Fact] + [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)] public async Task Equipment_namespace_structure_materialises_end_to_end_against_real_SDK() { await using var host = new OpcUaApplicationHost( @@ -183,20 +183,23 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable await host.StartAsync(sdkServer, Ct); sdkServer.NodeManager.ShouldNotBeNull(); - // One area / line / equipment + a Modbus FK driver in an Equipment-kind namespace, with a - // single equipment-bound Tag (the signal). Equipment.Name is the UNS browse segment. - var ns = new Namespace { NamespaceId = "ns-eq", ClusterId = "c1", Kind = NamespaceKind.Equipment, NamespaceUri = "urn:eq" }; - var driver = new DriverInstance { DriverInstanceId = "drv-modbus", ClusterId = "c1", NamespaceId = "ns-eq", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}" }; + // One area / line / equipment + a Modbus driver. v3: no Namespace entity, DriverInstance no + // longer binds a Namespace, and Equipment no longer binds a driver. The equipment-bound Tag + // (the Speed signal) that this end-to-end test materialised as a Variable node is DARK until + // Batch 4 — the composer emits an empty EquipmentTags set, so it is no longer seeded/passed + // here. Equipment.Name is the UNS browse segment. (Folder-only materialisation is covered live + // by the sibling MaterialiseHierarchy_against_real_SDK test; this suite's equipment-tag + // Variable end-to-end leg re-enables in Batch 4 via the UnsTagReference fan-out.) + var driver = new DriverInstance { DriverInstanceId = "drv-modbus", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}" }; var area = new UnsArea { UnsAreaId = "nw-area-filling", ClusterId = "c1", Name = "filling" }; var line = new UnsLine { UnsLineId = "nw-line-1", UnsAreaId = "nw-area-filling", Name = "line-1" }; - var equipment = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-modbus", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" }; - var tag = new Tag { TagId = "tag-speed", DriverInstanceId = "drv-modbus", EquipmentId = "eq-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"40001\"}" }; + var equipment = new Equipment { EquipmentId = "eq-1", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" }; var composition = AddressSpaceComposer.Compose( new[] { area }, new[] { line }, new[] { equipment }, new[] { driver }, - Array.Empty(), new[] { tag }, new[] { ns }); + Array.Empty()); - // Compose-side EquipmentTags extraction. + // Compose-side EquipmentTags extraction (Batch-4 subject — empty this batch). var planned = composition.EquipmentTags.ShouldHaveSingleItem(); planned.EquipmentId.ShouldBe("eq-1"); planned.FullName.ShouldBe("40001"); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs index c01d71c6..70138b07 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerAliasTagTests.cs @@ -1,81 +1,30 @@ -using Shouldly; using Xunit; -using ZB.MOM.WW.OtOpcUa.Configuration.Entities; -using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// -/// Verifies the live-edit compose seam () treats a Galaxy -/// point as an ordinary equipment tag: GalaxyMxGateway is a standard Equipment-kind driver. -/// An equipment-scoped (non-null ) bound to a -/// GalaxyMxGateway driver living in an Equipment-kind namespace must surface under -/// (carrying its driver-side FullName). +/// Characterized the live-edit compose seam admitting a Galaxy point as an ordinary equipment tag: +/// an equipment-scoped Tag bound to a GalaxyMxGateway driver surfaced under +/// carrying its driver-side FullName (coalescing a +/// null FolderPath to string.Empty). +/// v3 Batch-1 DARK + reshaped seed: equipment-tag variable plans do not materialize until +/// Batch 4 (the composer emits an empty EquipmentTags set — the raw + UNS variable nodes light +/// up in Batch 4's dual-namespace UNS↔Raw fan-out). The seed mechanism this test used is also gone: +/// Tag is now raw-only (no EquipmentId/DriverInstanceId/FolderPath), the +/// Compose overload no longer takes tags/namespaces, and equipment references raw tags via +/// UnsTagReference rather than a Galaxy point carrying an EquipmentId. So there is no +/// Batch-1 analog to assert. Re-author against the UnsTagReference → EquipmentTags fan-out when Batch 4 +/// lights the equipment-tag variable nodes. /// public sealed class AddressSpaceComposerAliasTagTests { - /// A GalaxyMxGateway driver in an Equipment-kind namespace carries an - /// equipment-scoped Galaxy tag (EquipmentId set, FolderPath null, TagConfig FullName = the Galaxy - /// ref). Compose must put it in EquipmentTags with its FullName, coalescing the null FolderPath to - /// string.Empty. - [Fact] + /// A GalaxyMxGateway equipment tag surfaces under EquipmentTags with its FullName — + /// dark until Batch 4 (empty equipment-tag plan set) and re-authored via UnsTagReference. + [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)] public void Compose_admits_galaxy_equipment_tag_in_equipment_tags() { - var ns = new Namespace - { - NamespaceId = "ns-eq", - ClusterId = "c1", - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:eq", - }; - var driver = new DriverInstance - { - DriverInstanceId = "drv-galaxy", - ClusterId = "c1", - NamespaceId = "ns-eq", - Name = "Galaxy1", - DriverType = "GalaxyMxGateway", - DriverConfig = "{}", - }; - var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" }; - var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" }; - var equip = new Equipment - { - EquipmentId = "eq-1", - DriverInstanceId = "drv-galaxy", - UnsLineId = "line-1", - Name = "TestMachine_020", - MachineCode = "TESTMACHINE_020", - }; - - // Equipment-scoped Galaxy tag: EquipmentId set, FolderPath null, FullName = Galaxy ref. - var galaxyTag = new Tag - { - TagId = "tag-galaxy", - DriverInstanceId = "drv-galaxy", - EquipmentId = "eq-1", - FolderPath = null, - Name = "TestChangingInt", - DataType = "Int32", - AccessLevel = TagAccessLevel.Read, - TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}", - }; - - var result = AddressSpaceComposer.Compose( - new[] { area }, new[] { line }, new[] { equip }, - new[] { driver }, Array.Empty(), - new[] { galaxyTag }, new[] { ns }); - - // The Galaxy point survives composition as an equipment tag, carrying its Galaxy FullName. - var tag = result.EquipmentTags.ShouldHaveSingleItem(); - tag.TagId.ShouldBe("tag-galaxy"); - tag.EquipmentId.ShouldBe("eq-1"); - tag.DriverInstanceId.ShouldBe("drv-galaxy"); - tag.Name.ShouldBe("TestChangingInt"); - tag.DataType.ShouldBe("Int32"); - tag.FullName.ShouldBe("TestMachine_020.TestChangingInt"); - // The input Tag.FolderPath is null; the composer coalesces it to string.Empty (the explicit - // byte-parity null-coalesce the artifact-decode side mirrors). - tag.FolderPath.ShouldBe(string.Empty); + // Dark until Batch 4: see class summary. EquipmentTags is emitted empty this batch and the + // Tag.EquipmentId binding this test relied on is retired (Tag is raw-only). The Batch-4 seam + // fans equipment tags in via UnsTagReference; re-author the assertion then. } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs index 36285256..8393f9a3 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerDeviceHostTests.cs @@ -1,98 +1,27 @@ -using Shouldly; using Xunit; -using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// -/// Covers follow-up E projection: carries the equipment's -/// DriverInstanceId / DeviceId bindings and the resolved DeviceHost (parsed from -/// the bound 's schemaless DeviceConfig JSON via the shared -/// DeviceConfigIntent.TryExtractHost). A later task grafts a driver's discovered -/// FixedTree onto a zero-tag equipment and partitions a multi-device driver by host using these. -/// The direct host-extraction + normalization unit tests moved to -/// Commons.Tests/DeviceConfigIntentTests (R2-11) — this suite keeps the Compose-level projection -/// coverage. +/// RETIRED (v3): this suite characterized follow-up E's projection of an +/// equipment's DriverInstanceId / DeviceId bindings + the resolved DeviceHost +/// (parsed from the bound device's DeviceConfig). In v3 the equipment↔device binding is +/// architecturally removed — Equipment no longer carries DriverInstanceId/DeviceId, +/// AddressSpaceComposer.Compose no longer takes a devices list, and equipment references +/// raw tags via UnsTagReference instead of hosting a device. EquipmentNode's +/// driver/device/host fields are therefore always null. +/// The coverage did not vanish, it moved: the device-endpoint merge is exercised by +/// Runtime.Tests.Drivers.DeploymentArtifactRawPathTests (nested RawPath + device-endpoint merge), +/// and the host-extraction/normalization unit lives in Commons.Tests.DeviceConfigIntentTests +/// (R2-11). The Skipped placeholder below preserves the retirement rationale in test output. /// public sealed class AddressSpaceComposerDeviceHostTests { - /// An equipment bound to a driver + a device whose config carries a top-level - /// HostAddress resolves all three fields, with the host trimmed + lower-cased. - [Fact] + [Fact(Skip = DarkAddressSpaceReasons.EquipmentDeviceBindingRetired)] public void Compose_resolves_driver_device_and_device_host() { - var equipment = new[] { NewEquipment("eq-1", driver: "d1", device: "dev1") }; - var devices = new[] { NewDevice("dev1", "d1", "{\"HostAddress\":\"10.0.0.5:8193\"}") }; - - var node = AddressSpaceComposer.Compose( - Array.Empty(), Array.Empty(), equipment, - Array.Empty(), Array.Empty(), devices: devices) - .EquipmentNodes.ShouldHaveSingleItem(); - - node.EquipmentId.ShouldBe("eq-1"); - node.DriverInstanceId.ShouldBe("d1"); - node.DeviceId.ShouldBe("dev1"); - node.DeviceHost.ShouldBe("10.0.0.5:8193"); + // Retired: see class summary. Equipment no longer binds a driver/device; the EquipmentNode + // driver/device/host projection is gone. Coverage moved to DeploymentArtifactRawPathTests + // (device-endpoint merge) + DeviceConfigIntentTests (host extraction). } - - /// An equipment with no driver and no device → all three new fields null (driver-less, - /// no device to resolve a host from). - [Fact] - public void Compose_equipment_without_driver_or_device_yields_null_bindings() - { - var equipment = new[] { NewEquipment("eq-1", driver: null, device: null) }; - - var node = AddressSpaceComposer.Compose( - Array.Empty(), Array.Empty(), equipment, - Array.Empty(), Array.Empty(), devices: Array.Empty()) - .EquipmentNodes.ShouldHaveSingleItem(); - - node.DriverInstanceId.ShouldBeNull(); - node.DeviceId.ShouldBeNull(); - node.DeviceHost.ShouldBeNull(); - } - - /// A bound DeviceId with no matching device row, or a device whose config has no - /// HostAddress, resolves DeviceHost to null while DeviceId is still carried. - [Fact] - public void Compose_device_host_is_null_when_unresolvable() - { - var equipment = new[] - { - NewEquipment("eq-missing", driver: "d1", device: "dev-missing"), - NewEquipment("eq-nohost", driver: "d1", device: "dev-nohost"), - }; - var devices = new[] { NewDevice("dev-nohost", "d1", "{\"Port\":502}") }; - - var nodes = AddressSpaceComposer.Compose( - Array.Empty(), Array.Empty(), equipment, - Array.Empty(), Array.Empty(), devices: devices) - .EquipmentNodes; - - var missing = nodes.Single(n => n.EquipmentId == "eq-missing"); - missing.DeviceId.ShouldBe("dev-missing"); - missing.DeviceHost.ShouldBeNull(); - - var noHost = nodes.Single(n => n.EquipmentId == "eq-nohost"); - noHost.DeviceId.ShouldBe("dev-nohost"); - noHost.DeviceHost.ShouldBeNull(); - } - - private static Equipment NewEquipment(string id, string? driver, string? device) => new() - { - EquipmentId = id, - DriverInstanceId = driver, - DeviceId = device, - UnsLineId = "line-1", - Name = id, - MachineCode = id.ToUpperInvariant(), - }; - - private static Device NewDevice(string deviceId, string driverInstanceId, string deviceConfig) => new() - { - DeviceId = deviceId, - DriverInstanceId = driverInstanceId, - Name = deviceId, - DeviceConfig = deviceConfig, - }; } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs index 3f216ab3..8f7c32c9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerEquipTokenTests.cs @@ -1,7 +1,6 @@ using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; -using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; @@ -10,6 +9,13 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// reserved {{equip}} token in a shared VirtualTag script with each owning equipment's /// derived tag base (from its child-tag FullNames) — so one script reused across N /// identical machines resolves to N machine-specific dependency graphs. +/// +/// v3 Batch-1 DARK: the per-equipment tag base is DERIVED from equipment-tag +/// FullNames, and equipment-tag variable plans do not materialize until Batch 4 (the composer +/// no longer takes tags — baseByEquip is empty this batch). With no base to substitute, the +/// {{equip}} token stays literal and these per-machine assertions have nothing to compare, so +/// the test is Skipped-with-reason. The seed + assertions are preserved verbatim (minus the retired +/// entity columns) so re-enabling in Batch 4 is a one-line change once the fan-out feeds the base. /// public sealed class AddressSpaceComposerEquipTokenTests { @@ -17,21 +23,17 @@ public sealed class AddressSpaceComposerEquipTokenTests /// to two equipments (TestMachine_001 / _002) each with one equipment Tag whose FullName carries /// the per-machine base. Compose must expand the token per equipment in both the Expression and /// the parsed DependencyRefs. - [Fact] + [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)] public void Compose_substitutes_equip_token_per_equipment() { - var ns = new Namespace - { - NamespaceId = "ns-eq", - ClusterId = "c1", - Kind = NamespaceKind.Equipment, - NamespaceUri = "urn:eq", - }; + // v3: no Namespace entity; DriverInstance no longer binds a Namespace; Equipment no longer + // binds a driver. In Batch 4 the {{equip}} base derives from the equipment's child-tag + // FullNames — here TestMachine_001.Source / TestMachine_002.Source — which flow through the + // (dark this batch) equipment-tag plan set. Seeded for intent; not passed to Compose. var driver1 = new DriverInstance { DriverInstanceId = "drv-1", ClusterId = "c1", - NamespaceId = "ns-eq", Name = "Modbus1", DriverType = "Modbus", DriverConfig = "{}", @@ -40,35 +42,14 @@ public sealed class AddressSpaceComposerEquipTokenTests { DriverInstanceId = "drv-2", ClusterId = "c1", - NamespaceId = "ns-eq", Name = "Modbus2", DriverType = "Modbus", DriverConfig = "{}", }; var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" }; var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" }; - var equip1 = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" }; - var equip2 = new Equipment { EquipmentId = "eq-2", DriverInstanceId = "drv-2", UnsLineId = "line-1", Name = "TestMachine_002", MachineCode = "TESTMACHINE_002" }; - var tag1 = new Tag - { - TagId = "tag-1", - DriverInstanceId = "drv-1", - EquipmentId = "eq-1", - Name = "Source", - DataType = "Int32", - AccessLevel = TagAccessLevel.Read, - TagConfig = "{\"FullName\":\"TestMachine_001.Source\",\"DataType\":\"Int32\"}", - }; - var tag2 = new Tag - { - TagId = "tag-2", - DriverInstanceId = "drv-2", - EquipmentId = "eq-2", - Name = "Source", - DataType = "Int32", - AccessLevel = TagAccessLevel.Read, - TagConfig = "{\"FullName\":\"TestMachine_002.Source\",\"DataType\":\"Int32\"}", - }; + var equip1 = new Equipment { EquipmentId = "eq-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" }; + var equip2 = new Equipment { EquipmentId = "eq-2", UnsLineId = "line-1", Name = "TestMachine_002", MachineCode = "TESTMACHINE_002" }; var script = new Script { ScriptId = "s-equip", @@ -82,7 +63,6 @@ public sealed class AddressSpaceComposerEquipTokenTests var result = AddressSpaceComposer.Compose( new[] { area }, new[] { line }, new[] { equip1, equip2 }, new[] { driver1, driver2 }, Array.Empty(), - new[] { tag1, tag2 }, new[] { ns }, virtualTags: new[] { vt1, vt2 }, scripts: new[] { script }); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs index 075cd5f9..13baacae 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceComposerPurityTests.cs @@ -136,7 +136,6 @@ public sealed class AddressSpaceComposerPurityTests var result = AddressSpaceComposer.Compose( Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), Array.Empty(), virtualTags: new[] { vt }, scripts: new[] { script }); @@ -174,7 +173,6 @@ public sealed class AddressSpaceComposerPurityTests var result = AddressSpaceComposer.Compose( Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), Array.Empty(), virtualTags: new[] { vt }, scripts: new[] { script }); @@ -200,7 +198,6 @@ public sealed class AddressSpaceComposerPurityTests var result = AddressSpaceComposer.Compose( Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), Array.Empty(), virtualTags: new[] { vt }, scripts: Array.Empty