test: fix Galaxy-tag Phase7 test fixtures + S7 CLI enum; add MaterialiseGalaxyTags coverage
v2-ci / build (push) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Completes the test side of the in-progress Galaxy-tag workstream:
- Phase7ApplierTests / Phase7ApplierHierarchyTests: supply the now-required
  Galaxy-tag args to Phase7Plan / Phase7CompositionResult.
- Add genuine coverage for Phase7Applier.MaterialiseGalaxyTags (folder-per-distinct-path,
  variable-per-tag node-id derivation, folder dedupe) + added-Galaxy-tags-trigger-rebuild.
- S7.Cli.Tests: use the project's S7CpuType (CLI option type) instead of S7.Net.CpuType.
Whole solution now builds 0/0; OpcUaServer.Tests 52, S7.Cli.Tests 36 green.
This commit is contained in:
Joseph Doherty
2026-05-29 12:18:01 -04:00
parent a4fb97aef8
commit de666b24c3
3 changed files with 117 additions and 12 deletions
@@ -2,8 +2,8 @@ using CliFx.Attributes;
using CliFx.Infrastructure;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.S7;
using ZB.MOM.WW.OtOpcUa.Driver.S7.Cli;
using S7NetCpuType = global::S7.Net.CpuType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests;
@@ -41,7 +41,7 @@ public sealed class S7CommandBaseBuildOptionsTests
{
Host = "10.0.0.5",
Port = 102,
CpuType = S7NetCpuType.S71500,
CpuType = S7CpuType.S71500,
Rack = 0,
Slot = 0,
TimeoutMs = 5000,
@@ -72,7 +72,7 @@ public sealed class S7CommandBaseBuildOptionsTests
{
Host = "plc.shop.local",
Port = 4102,
CpuType = S7NetCpuType.S7300,
CpuType = S7CpuType.S7300,
Rack = 1,
Slot = 2,
TimeoutMs = 3000,
@@ -82,7 +82,7 @@ public sealed class S7CommandBaseBuildOptionsTests
options.Host.ShouldBe("plc.shop.local");
options.Port.ShouldBe(4102);
options.CpuType.ShouldBe(S7NetCpuType.S7300);
options.CpuType.ShouldBe(S7CpuType.S7300);
options.Rack.ShouldBe((short)1);
options.Slot.ShouldBe((short)2);
}
@@ -34,7 +34,8 @@ 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>());
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
GalaxyTags: Array.Empty<GalaxyTagPlan>());
applier.MaterialiseHierarchy(composition);
@@ -57,7 +58,8 @@ 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>());
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
GalaxyTags: Array.Empty<GalaxyTagPlan>());
applier.MaterialiseHierarchy(composition);
@@ -91,7 +93,8 @@ 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>()));
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
GalaxyTags: Array.Empty<GalaxyTagPlan>()));
sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment
@@ -101,7 +104,8 @@ 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>()));
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>(),
GalaxyTags: Array.Empty<GalaxyTagPlan>()));
sdkServer.NodeManager!.FolderCount.ShouldBe(5);
}
@@ -58,7 +58,10 @@ public sealed class Phase7ApplierTests
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
AddedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
var outcome = applier.Apply(plan);
@@ -89,7 +92,10 @@ public sealed class Phase7ApplierTests
},
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
AddedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
var outcome = applier.Apply(plan);
@@ -111,10 +117,102 @@ 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 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<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<Phase7Plan.AlarmDelta>(),
Array.Empty<GalaxyTagPlan>(), Array.Empty<GalaxyTagPlan>(), Array.Empty<Phase7Plan.GalaxyTagDelta>());
private static Phase7Plan WithEquipmentRemoval(params string[] ids) => new(
AddedEquipment: Array.Empty<EquipmentNode>(),
@@ -125,7 +223,10 @@ public sealed class Phase7ApplierTests
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(),
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>());
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>(),
AddedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
RemovedGalaxyTags: Array.Empty<GalaxyTagPlan>(),
ChangedGalaxyTags: Array.Empty<Phase7Plan.GalaxyTagDelta>());
private sealed class RecordingSink : IOpcUaAddressSpaceSink
{