feat(opcua): remove SystemPlatform-mirror GalaxyTags contract end-to-end (composer+applier+artifact, byte-parity)
This commit is contained in:
@@ -36,8 +36,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
|
||||
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") },
|
||||
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: Array.Empty<GalaxyTagPlan>());
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>());
|
||||
|
||||
applier.MaterialiseHierarchy(composition);
|
||||
|
||||
@@ -60,8 +59,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
|
||||
UnsLines: Array.Empty<UnsLineProjection>(),
|
||||
EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") },
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: Array.Empty<GalaxyTagPlan>());
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>());
|
||||
|
||||
applier.MaterialiseHierarchy(composition);
|
||||
|
||||
@@ -95,8 +93,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
|
||||
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
|
||||
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: Array.Empty<GalaxyTagPlan>()));
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>()));
|
||||
|
||||
sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment
|
||||
|
||||
@@ -106,8 +103,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
|
||||
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
|
||||
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: Array.Empty<GalaxyTagPlan>()));
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>()));
|
||||
|
||||
sdkServer.NodeManager!.FolderCount.ShouldBe(5);
|
||||
}
|
||||
@@ -143,8 +139,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
|
||||
UnsLines: Array.Empty<UnsLineProjection>(),
|
||||
EquipmentNodes: new[] { new EquipmentNode("eq-1", "filling-eq", UnsLineId: "") },
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: Array.Empty<GalaxyTagPlan>())
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
EquipmentTags = new[]
|
||||
{
|
||||
@@ -201,7 +196,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
|
||||
new[] { area }, new[] { line }, new[] { equipment }, new[] { driver },
|
||||
Array.Empty<ScriptedAlarm>(), new[] { tag }, new[] { ns });
|
||||
|
||||
// Compose-side EquipmentTags extraction (the inverse of the Galaxy filter).
|
||||
// Compose-side EquipmentTags extraction.
|
||||
var planned = composition.EquipmentTags.ShouldHaveSingleItem();
|
||||
planned.EquipmentId.ShouldBe("eq-1");
|
||||
planned.FullName.ShouldBe("40001");
|
||||
|
||||
@@ -59,10 +59,7 @@ public sealed class Phase7ApplierTests
|
||||
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
|
||||
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
|
||||
AddedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
|
||||
|
||||
var outcome = applier.Apply(plan);
|
||||
|
||||
@@ -93,10 +90,7 @@ public sealed class Phase7ApplierTests
|
||||
},
|
||||
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
|
||||
AddedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
|
||||
|
||||
var outcome = applier.Apply(plan);
|
||||
|
||||
@@ -118,66 +112,6 @@ public sealed class Phase7ApplierTests
|
||||
outcome.RebuildCalled.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies MaterialiseGalaxyTags creates one folder per distinct FolderPath and one
|
||||
/// variable per tag, with root-level tags hung directly under the namespace root.</summary>
|
||||
[Fact]
|
||||
public void MaterialiseGalaxyTags_creates_folder_per_distinct_path_and_variable_per_tag()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var composition = new Phase7CompositionResult(
|
||||
UnsAreas: Array.Empty<UnsAreaProjection>(),
|
||||
UnsLines: Array.Empty<UnsLineProjection>(),
|
||||
EquipmentNodes: Array.Empty<EquipmentNode>(),
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: new[]
|
||||
{
|
||||
new GalaxyTagPlan("t1", "drv", "section.area", "Temperature", "Float", "section.area.Temperature"),
|
||||
new GalaxyTagPlan("t2", "drv", "", "Pressure", "Int32", "Pressure"),
|
||||
});
|
||||
|
||||
applier.MaterialiseGalaxyTags(composition);
|
||||
|
||||
// One folder for the single distinct non-empty FolderPath; the root-level tag adds none.
|
||||
sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("section.area", null, "section.area"));
|
||||
|
||||
// Foldered tag → NodeId is its MxAccessRef under the FolderPath parent.
|
||||
// Root-level tag → NodeId is its DisplayName under the root (null parent).
|
||||
sink.VariableCalls.ShouldContain(("section.area.Temperature", "section.area", "Temperature", "Float"));
|
||||
sink.VariableCalls.ShouldContain(("Pressure", (string?)null, "Pressure", "Int32"));
|
||||
sink.VariableCalls.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that two tags sharing a FolderPath produce a single EnsureFolder call
|
||||
/// (deduped) but one EnsureVariable per tag.</summary>
|
||||
[Fact]
|
||||
public void MaterialiseGalaxyTags_dedupes_folders_for_tags_sharing_a_path()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var composition = new Phase7CompositionResult(
|
||||
UnsAreas: Array.Empty<UnsAreaProjection>(),
|
||||
UnsLines: Array.Empty<UnsLineProjection>(),
|
||||
EquipmentNodes: Array.Empty<EquipmentNode>(),
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: new[]
|
||||
{
|
||||
new GalaxyTagPlan("t1", "drv", "line.cell", "Speed", "Float", "line.cell.Speed"),
|
||||
new GalaxyTagPlan("t2", "drv", "line.cell", "Torque", "Float", "line.cell.Torque"),
|
||||
});
|
||||
|
||||
applier.MaterialiseGalaxyTags(composition);
|
||||
|
||||
sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("line.cell", null, "line.cell"));
|
||||
sink.VariableCalls.Count.ShouldBe(2);
|
||||
sink.VariableCalls.ShouldContain(("line.cell.Speed", "line.cell", "Speed", "Float"));
|
||||
sink.VariableCalls.ShouldContain(("line.cell.Torque", "line.cell", "Torque", "Float"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies MaterialiseEquipmentTags creates one Variable per equipment tag directly
|
||||
/// under its existing equipment folder, with a folder-scoped NodeId (parent/Name — NOT the raw
|
||||
/// FullName), parent == EquipmentId, displayName == Name, and does NOT re-create the equipment
|
||||
@@ -193,8 +127,7 @@ public sealed class Phase7ApplierTests
|
||||
UnsLines: Array.Empty<UnsLineProjection>(),
|
||||
EquipmentNodes: Array.Empty<EquipmentNode>(),
|
||||
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
|
||||
GalaxyTags: Array.Empty<GalaxyTagPlan>())
|
||||
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
EquipmentTags = new[]
|
||||
{
|
||||
@@ -345,8 +278,8 @@ public sealed class Phase7ApplierTests
|
||||
}
|
||||
|
||||
/// <summary>Verifies that added equipment tags in an otherwise-empty plan trigger an
|
||||
/// address-space rebuild (parity with the Galaxy-tag path — the planner now diffs equipment
|
||||
/// tags, so a tags-only deploy is no longer a silent no-op).</summary>
|
||||
/// address-space rebuild (the planner now diffs equipment tags, so a tags-only deploy is no
|
||||
/// longer a silent no-op).</summary>
|
||||
[Fact]
|
||||
public void Added_equipment_tags_trigger_rebuild()
|
||||
{
|
||||
@@ -393,42 +326,10 @@ public sealed class Phase7ApplierTests
|
||||
sink.RebuildCalls.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that added Galaxy tags in an otherwise-empty plan trigger an address-space rebuild.</summary>
|
||||
[Fact]
|
||||
public void Added_galaxy_tags_trigger_rebuild()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var plan = new Phase7Plan(
|
||||
AddedEquipment: Array.Empty<EquipmentNode>(),
|
||||
RemovedEquipment: Array.Empty<EquipmentNode>(),
|
||||
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(),
|
||||
AddedDrivers: Array.Empty<DriverInstancePlan>(),
|
||||
RemovedDrivers: Array.Empty<DriverInstancePlan>(),
|
||||
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
|
||||
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
|
||||
AddedGalaxyTags: new[]
|
||||
{
|
||||
new GalaxyTagPlan("t1", "drv", "section.area", "Temperature", "Float", "section.area.Temperature"),
|
||||
},
|
||||
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
|
||||
|
||||
var outcome = applier.Apply(plan);
|
||||
|
||||
outcome.RebuildCalled.ShouldBeTrue();
|
||||
outcome.AddedNodes.ShouldBe(1);
|
||||
sink.RebuildCalls.ShouldBe(1);
|
||||
}
|
||||
|
||||
private static Phase7Plan EmptyPlan => new(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(), Array.Empty<Phase7Plan.EquipmentDelta>(),
|
||||
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(), Array.Empty<Phase7Plan.DriverDelta>(),
|
||||
Array.Empty<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<Phase7Plan.AlarmDelta>(),
|
||||
Array.Empty<GalaxyTagPlan>(), Array.Empty<GalaxyTagPlan>(), Array.Empty<Phase7Plan.GalaxyTagDelta>());
|
||||
Array.Empty<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<Phase7Plan.AlarmDelta>());
|
||||
|
||||
private static Phase7Plan WithEquipmentRemoval(params string[] ids) => new(
|
||||
AddedEquipment: Array.Empty<EquipmentNode>(),
|
||||
@@ -439,10 +340,7 @@ public sealed class Phase7ApplierTests
|
||||
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
|
||||
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
|
||||
AddedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
|
||||
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
|
||||
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
|
||||
|
||||
private sealed class RecordingSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
|
||||
@@ -11,16 +11,15 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
/// 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="Phase7CompositionResult.EquipmentTags"/> (carrying its driver-side FullName), and
|
||||
/// the retired SystemPlatform-mirror producer means <see cref="Phase7CompositionResult.GalaxyTags"/>
|
||||
/// is always empty.
|
||||
/// <see cref="Phase7CompositionResult.EquipmentTags"/> (carrying its driver-side FullName). The
|
||||
/// SystemPlatform-mirror <c>GalaxyTags</c> contract is retired entirely.
|
||||
/// </summary>
|
||||
public sealed class Phase7ComposerAliasTagTests
|
||||
{
|
||||
/// <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, and GalaxyTags must be empty
|
||||
/// (the SystemPlatform mirror producer is gone).</summary>
|
||||
/// ref). Compose must put it in EquipmentTags with its FullName, coalescing the null FolderPath to
|
||||
/// <c>string.Empty</c> (the SystemPlatform mirror producer is gone entirely).</summary>
|
||||
[Fact]
|
||||
public void Compose_admits_galaxy_equipment_tag_in_equipment_tags()
|
||||
{
|
||||
@@ -77,8 +76,8 @@ public sealed class Phase7ComposerAliasTagTests
|
||||
tag.Name.ShouldBe("TestChangingInt");
|
||||
tag.DataType.ShouldBe("Int32");
|
||||
tag.FullName.ShouldBe("TestMachine_020.TestChangingInt");
|
||||
|
||||
// The SystemPlatform-mirror producer is retired → GalaxyTags is always empty.
|
||||
result.GalaxyTags.ShouldBeEmpty();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user