Merge B1-wp6b-srv
This commit is contained in:
@@ -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,
|
||||
|
||||
+82
-27
@@ -13,20 +13,32 @@ using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end deploy of an Equipment-kind namespace through the <b>real</b>
|
||||
/// <c>ConfigComposer</c>: seed a 1-area / 1-line / 1-equipment / 1-tag Equipment namespace,
|
||||
/// <c>StartDeployment</c>, then assert the deployment's persisted artifact decodes (via
|
||||
/// <see cref="DeploymentArtifact.ParseComposition"/>) to the equipment signal + the friendly
|
||||
/// UNS folder name. This covers the <c>ConfigComposer → ArtifactBlob → ParseComposition.EquipmentTags</c>
|
||||
/// seam that the OpcUaServer unit tests only approximate with hand-built JSON.
|
||||
/// End-to-end deploy of a UNS folder hierarchy through the <b>real</b> <c>ConfigComposer</c>: seed a
|
||||
/// 1-area / 1-line / 1-equipment UNS plus a v3 raw-tag chain (RawFolder → DriverInstance → Device →
|
||||
/// Tag), <c>StartDeployment</c>, then assert the deployment's persisted artifact decodes (via
|
||||
/// <see cref="DeploymentArtifact.ParseComposition"/>).
|
||||
/// <para>
|
||||
/// <b>v3 DARK address space (Batch 1):</b> 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 <c>EquipmentTags</c> 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).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The OPC UA address-space browse is exercised separately against a real SDK node manager in
|
||||
/// <c>AddressSpaceApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK</c>,
|
||||
/// because the in-process <see cref="TwoNodeClusterHarness"/> binds the no-op address-space sink.
|
||||
/// <c>AddressSpaceApplierHierarchyTests</c>, because the in-process <see cref="TwoNodeClusterHarness"/>
|
||||
/// binds the no-op address-space sink.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>Verifies a deployed Equipment namespace carries its signal into the composed artifact.</summary>
|
||||
/// <summary>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).</summary>
|
||||
[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<IAdminOperationsClient>();
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>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.</summary>
|
||||
[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<IAdminOperationsClient>();
|
||||
|
||||
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<byte>();
|
||||
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\"}",
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
+12
-9
@@ -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.
|
||||
/// </summary>
|
||||
[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<ScriptedAlarm>(), new[] { tag }, new[] { ns });
|
||||
Array.Empty<ScriptedAlarm>());
|
||||
|
||||
// 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");
|
||||
|
||||
+18
-69
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) treats a Galaxy
|
||||
/// point as an ordinary equipment tag: <c>GalaxyMxGateway</c> is a standard Equipment-kind driver.
|
||||
/// An equipment-scoped <see cref="Tag"/> (non-null <see cref="Tag.EquipmentId"/>) bound to a
|
||||
/// <c>GalaxyMxGateway</c> driver living in an <c>Equipment</c>-kind namespace must surface under
|
||||
/// <see cref="AddressSpaceComposition.EquipmentTags"/> (carrying its driver-side FullName).
|
||||
/// Characterized the live-edit compose seam admitting a Galaxy point as an ordinary equipment tag:
|
||||
/// an equipment-scoped <c>Tag</c> bound to a <c>GalaxyMxGateway</c> driver surfaced under
|
||||
/// <see cref="AddressSpaceComposition.EquipmentTags"/> carrying its driver-side FullName (coalescing a
|
||||
/// null FolderPath to <c>string.Empty</c>).
|
||||
/// <para><b>v3 Batch-1 DARK + reshaped seed:</b> equipment-tag variable plans do not materialize until
|
||||
/// Batch 4 (the composer emits an empty <c>EquipmentTags</c> 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:
|
||||
/// <c>Tag</c> is now raw-only (no <c>EquipmentId</c>/<c>DriverInstanceId</c>/<c>FolderPath</c>), the
|
||||
/// Compose overload no longer takes tags/namespaces, and equipment references raw tags via
|
||||
/// <c>UnsTagReference</c> rather than a Galaxy point carrying an <c>EquipmentId</c>. 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.</para>
|
||||
/// </summary>
|
||||
public sealed class AddressSpaceComposerAliasTagTests
|
||||
{
|
||||
/// <summary>A <c>GalaxyMxGateway</c> 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
|
||||
/// <c>string.Empty</c>.</summary>
|
||||
[Fact]
|
||||
/// <summary>A <c>GalaxyMxGateway</c> equipment tag surfaces under EquipmentTags with its FullName —
|
||||
/// dark until Batch 4 (empty equipment-tag plan set) and re-authored via UnsTagReference.</summary>
|
||||
[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<ScriptedAlarm>(),
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
+15
-86
@@ -1,98 +1,27 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Covers follow-up E projection: <see cref="EquipmentNode"/> carries the equipment's
|
||||
/// <c>DriverInstanceId</c> / <c>DeviceId</c> bindings and the resolved <c>DeviceHost</c> (parsed from
|
||||
/// the bound <see cref="Device"/>'s schemaless <c>DeviceConfig</c> JSON via the shared
|
||||
/// <c>DeviceConfigIntent.TryExtractHost</c>). A later task grafts a driver's discovered
|
||||
/// FixedTree onto a zero-tag equipment and partitions a multi-device driver by host using these.
|
||||
/// <para>The direct host-extraction + normalization unit tests moved to
|
||||
/// <c>Commons.Tests/DeviceConfigIntentTests</c> (R2-11) — this suite keeps the Compose-level projection
|
||||
/// coverage.</para>
|
||||
/// RETIRED (v3): this suite characterized follow-up E's <see cref="EquipmentNode"/> projection of an
|
||||
/// equipment's <c>DriverInstanceId</c> / <c>DeviceId</c> bindings + the resolved <c>DeviceHost</c>
|
||||
/// (parsed from the bound device's <c>DeviceConfig</c>). In v3 the equipment↔device binding is
|
||||
/// architecturally removed — <c>Equipment</c> no longer carries <c>DriverInstanceId</c>/<c>DeviceId</c>,
|
||||
/// <c>AddressSpaceComposer.Compose</c> no longer takes a <c>devices</c> list, and equipment references
|
||||
/// raw tags via <c>UnsTagReference</c> instead of hosting a device. <c>EquipmentNode</c>'s
|
||||
/// driver/device/host fields are therefore always null.
|
||||
/// <para>The coverage did not vanish, it moved: the device-endpoint merge is exercised by
|
||||
/// <c>Runtime.Tests.Drivers.DeploymentArtifactRawPathTests</c> (nested RawPath + device-endpoint merge),
|
||||
/// and the host-extraction/normalization unit lives in <c>Commons.Tests.DeviceConfigIntentTests</c>
|
||||
/// (R2-11). The Skipped placeholder below preserves the retirement rationale in test output.</para>
|
||||
/// </summary>
|
||||
public sealed class AddressSpaceComposerDeviceHostTests
|
||||
{
|
||||
/// <summary>An equipment bound to a driver + a device whose config carries a top-level
|
||||
/// <c>HostAddress</c> resolves all three fields, with the host trimmed + lower-cased.</summary>
|
||||
[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<UnsArea>(), Array.Empty<UnsLine>(), equipment,
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), 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).
|
||||
}
|
||||
|
||||
/// <summary>An equipment with no driver and no device → all three new fields null (driver-less,
|
||||
/// no device to resolve a host from).</summary>
|
||||
[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<UnsArea>(), Array.Empty<UnsLine>(), equipment,
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), devices: Array.Empty<Device>())
|
||||
.EquipmentNodes.ShouldHaveSingleItem();
|
||||
|
||||
node.DriverInstanceId.ShouldBeNull();
|
||||
node.DeviceId.ShouldBeNull();
|
||||
node.DeviceHost.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>A bound DeviceId with no matching device row, or a device whose config has no
|
||||
/// <c>HostAddress</c>, resolves DeviceHost to null while DeviceId is still carried.</summary>
|
||||
[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<UnsArea>(), Array.Empty<UnsLine>(), equipment,
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), 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,
|
||||
};
|
||||
}
|
||||
|
||||
+14
-34
@@ -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 <c>{{equip}}</c> token in a shared VirtualTag script with each owning equipment's
|
||||
/// derived tag base (from its child-tag <c>FullName</c>s) — so one script reused across N
|
||||
/// identical machines resolves to N machine-specific dependency graphs.
|
||||
///
|
||||
/// <para><b>v3 Batch-1 DARK:</b> the per-equipment tag base is DERIVED from equipment-tag
|
||||
/// <c>FullName</c>s, and equipment-tag variable plans do not materialize until Batch 4 (the composer
|
||||
/// no longer takes tags — <c>baseByEquip</c> is empty this batch). With no base to substitute, the
|
||||
/// <c>{{equip}}</c> 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.</para>
|
||||
/// </summary>
|
||||
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.</summary>
|
||||
[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<ScriptedAlarm>(),
|
||||
new[] { tag1, tag2 }, new[] { ns },
|
||||
virtualTags: new[] { vt1, vt2 },
|
||||
scripts: new[] { script });
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ public sealed class AddressSpaceComposerPurityTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
virtualTags: new[] { vt },
|
||||
scripts: new[] { script });
|
||||
|
||||
@@ -174,7 +173,6 @@ public sealed class AddressSpaceComposerPurityTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
virtualTags: new[] { vt },
|
||||
scripts: new[] { script });
|
||||
|
||||
@@ -200,7 +198,6 @@ public sealed class AddressSpaceComposerPurityTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
virtualTags: new[] { vt },
|
||||
scripts: Array.Empty<Script>());
|
||||
|
||||
@@ -213,7 +210,6 @@ public sealed class AddressSpaceComposerPurityTests
|
||||
private static Equipment NewEquipment(string id) => new()
|
||||
{
|
||||
EquipmentId = id,
|
||||
DriverInstanceId = "drv-1",
|
||||
UnsLineId = "line-1",
|
||||
Name = id,
|
||||
MachineCode = id.ToUpperInvariant(),
|
||||
@@ -223,7 +219,6 @@ public sealed class AddressSpaceComposerPurityTests
|
||||
{
|
||||
DriverInstanceId = id,
|
||||
ClusterId = "cluster-1",
|
||||
NamespaceId = "ns-1",
|
||||
Name = id,
|
||||
DriverType = "Stub",
|
||||
DriverConfig = "{\"k\":\"v\"}",
|
||||
|
||||
+2
-7
@@ -65,7 +65,6 @@ public sealed class AddressSpaceComposerScriptedAlarmTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
virtualTags: Array.Empty<VirtualTag>(),
|
||||
scripts: new[] { script1, script2 });
|
||||
|
||||
@@ -121,7 +120,6 @@ public sealed class AddressSpaceComposerScriptedAlarmTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { alarm },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
scripts: new[] { script });
|
||||
|
||||
var plan = result.EquipmentScriptedAlarms.ShouldHaveSingleItem();
|
||||
@@ -158,7 +156,6 @@ public sealed class AddressSpaceComposerScriptedAlarmTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { alarm },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
scripts: new[] { script });
|
||||
|
||||
var plan = result.EquipmentScriptedAlarms.ShouldHaveSingleItem();
|
||||
@@ -202,7 +199,6 @@ public sealed class AddressSpaceComposerScriptedAlarmTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
scripts: new[] { goodScript });
|
||||
|
||||
var plan = result.EquipmentScriptedAlarms.ShouldHaveSingleItem();
|
||||
@@ -237,7 +233,6 @@ public sealed class AddressSpaceComposerScriptedAlarmTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { alarm },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||
scripts: new[] { script });
|
||||
|
||||
var plan = result.EquipmentScriptedAlarms.ShouldHaveSingleItem();
|
||||
@@ -264,12 +259,12 @@ public sealed class AddressSpaceComposerScriptedAlarmTests
|
||||
var r1 = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { a1, a2, a3 },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts);
|
||||
scripts: scripts);
|
||||
|
||||
var r2 = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||
Array.Empty<DriverInstance>(), new[] { a3, a1, a2 },
|
||||
Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts);
|
||||
scripts: scripts);
|
||||
|
||||
// Sorted by EquipmentId then ScriptedAlarmId: (eq-1,al-2), (eq-1,al-3), (eq-2,al-1).
|
||||
r1.EquipmentScriptedAlarms.Select(p => p.ScriptedAlarmId)
|
||||
|
||||
+4
-20
@@ -17,35 +17,20 @@ public sealed class AddressSpaceComposerVirtualTagHistorizeTests
|
||||
[Fact]
|
||||
public void Compose_carries_virtual_tag_historize_flag_onto_plan()
|
||||
{
|
||||
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. The dependency tag is not seeded here — the composer no longer takes tags
|
||||
// (they're dark until Batch 4) and the VirtualTag subject of this test is a LIVE surface.
|
||||
var driver = new DriverInstance
|
||||
{
|
||||
DriverInstanceId = "drv-1",
|
||||
ClusterId = "c1",
|
||||
NamespaceId = "ns-eq",
|
||||
Name = "Modbus1",
|
||||
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 equip = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" };
|
||||
var tag = 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 equip = new Equipment { EquipmentId = "eq-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" };
|
||||
var script = new Script
|
||||
{
|
||||
ScriptId = "s-1",
|
||||
@@ -60,7 +45,6 @@ public sealed class AddressSpaceComposerVirtualTagHistorizeTests
|
||||
var result = AddressSpaceComposer.Compose(
|
||||
new[] { area }, new[] { line }, new[] { equip },
|
||||
new[] { driver }, Array.Empty<ScriptedAlarm>(),
|
||||
new[] { tag }, new[] { ns },
|
||||
virtualTags: new[] { vtHist, vtPlain },
|
||||
scripts: new[] { script });
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Shared xUnit <c>Skip</c> reason constants for OpcUaServer tests whose intent is retained but
|
||||
/// whose subject is not materialized in the v3 Batch-1 DARK address space. The composer
|
||||
/// (<see cref="AddressSpaceComposer.Compose"/>) and the artifact both emit an EMPTY equipment-tag
|
||||
/// variable plan set this batch — raw + UNS variable nodes are lit up in Batch 4's dual-namespace
|
||||
/// UNS↔Raw fan-out. Until then any assertion whose subject is an equipment-tag variable node (its
|
||||
/// FullName, its <c>{{equip}}</c>-token-derived base, its device-host binding) has nothing to
|
||||
/// compare, so it is Skipped-with-reason rather than deleted. Mirrors
|
||||
/// <c>Runtime.Tests.Drivers.DarkAddressSpaceReasons</c>.
|
||||
/// </summary>
|
||||
internal static class DarkAddressSpaceReasons
|
||||
{
|
||||
/// <summary>
|
||||
/// Equipment-tag variable materialization + its per-field intent (FullName / array / historize /
|
||||
/// native-alarm / <c>{{equip}}</c>-token base derived from child-tag FullNames) is dark until Batch 4.
|
||||
/// </summary>
|
||||
public const string EquipmentTagsDarkBatch4 =
|
||||
"v3 dark address space: equipment-tag variable plans (FullName / array / historize / alarm / " +
|
||||
"{{equip}}-token base derived from child-tag FullNames) do not materialize until Batch 4 " +
|
||||
"(dual-namespace UNS↔Raw fan-out). The composer emits an empty equipment-tag plan set this batch, " +
|
||||
"so there is no per-equipment tag base to substitute and no equipment-tag variable node to assert. " +
|
||||
"Migrate + re-enable when Batch 4 lights the raw/UNS variable nodes.";
|
||||
|
||||
/// <summary>Equipment↔device host binding is architecturally retired in v3.</summary>
|
||||
public const string EquipmentDeviceBindingRetired =
|
||||
"v3: equipment no longer binds a driver/device — Equipment lost DriverInstanceId/DeviceId and " +
|
||||
"Compose no longer takes a devices list, so EquipmentNode's DriverInstanceId/DeviceId/DeviceHost " +
|
||||
"projection is always null. Equipment references raw tags via UnsTagReference. The device-endpoint " +
|
||||
"merge this suite characterized now lives in Runtime.Tests DeploymentArtifactRawPathTests; the " +
|
||||
"host-extraction unit is Commons.Tests DeviceConfigIntentTests.";
|
||||
}
|
||||
Reference in New Issue
Block a user