From de666b24c39c6eb04d8b36fa524cfcea7d6173ee Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 29 May 2026 12:18:01 -0400 Subject: [PATCH] test: fix Galaxy-tag Phase7 test fixtures + S7 CLI enum; add MaterialiseGalaxyTags coverage 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. --- .../S7CommandBaseBuildOptionsTests.cs | 8 +- .../Phase7ApplierHierarchyTests.cs | 12 +- .../Phase7ApplierTests.cs | 109 +++++++++++++++++- 3 files changed, 117 insertions(+), 12 deletions(-) diff --git a/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/S7CommandBaseBuildOptionsTests.cs b/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/S7CommandBaseBuildOptionsTests.cs index 0d5ab98b..e68ad437 100644 --- a/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/S7CommandBaseBuildOptionsTests.cs +++ b/tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli.Tests/S7CommandBaseBuildOptionsTests.cs @@ -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); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs index 9f914e9b..befdcdd7 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs @@ -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(), - ScriptedAlarmPlans: Array.Empty()); + ScriptedAlarmPlans: Array.Empty(), + GalaxyTags: Array.Empty()); applier.MaterialiseHierarchy(composition); @@ -57,7 +58,8 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable UnsLines: Array.Empty(), EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") }, DriverInstancePlans: Array.Empty(), - ScriptedAlarmPlans: Array.Empty()); + ScriptedAlarmPlans: Array.Empty(), + GalaxyTags: Array.Empty()); 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(), - ScriptedAlarmPlans: Array.Empty())); + ScriptedAlarmPlans: Array.Empty(), + GalaxyTags: Array.Empty())); 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(), - ScriptedAlarmPlans: Array.Empty())); + ScriptedAlarmPlans: Array.Empty(), + GalaxyTags: Array.Empty())); sdkServer.NodeManager!.FolderCount.ShouldBe(5); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs index 5d7d02eb..7cbae453 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs @@ -58,7 +58,10 @@ public sealed class Phase7ApplierTests ChangedDrivers: Array.Empty(), AddedAlarms: Array.Empty(), RemovedAlarms: Array.Empty(), - ChangedAlarms: Array.Empty()); + ChangedAlarms: Array.Empty(), + AddedGalaxyTags: Array.Empty(), + RemovedGalaxyTags: Array.Empty(), + ChangedGalaxyTags: Array.Empty()); var outcome = applier.Apply(plan); @@ -89,7 +92,10 @@ public sealed class Phase7ApplierTests }, AddedAlarms: Array.Empty(), RemovedAlarms: Array.Empty(), - ChangedAlarms: Array.Empty()); + ChangedAlarms: Array.Empty(), + AddedGalaxyTags: Array.Empty(), + RemovedGalaxyTags: Array.Empty(), + ChangedGalaxyTags: Array.Empty()); var outcome = applier.Apply(plan); @@ -111,10 +117,102 @@ public sealed class Phase7ApplierTests outcome.RebuildCalled.ShouldBeTrue(); } + /// Verifies MaterialiseGalaxyTags creates one folder per distinct FolderPath and one + /// variable per tag, with root-level tags hung directly under the namespace root. + [Fact] + public void MaterialiseGalaxyTags_creates_folder_per_distinct_path_and_variable_per_tag() + { + var sink = new RecordingSink(); + var applier = new Phase7Applier(sink, NullLogger.Instance); + + var composition = new Phase7CompositionResult( + UnsAreas: Array.Empty(), + UnsLines: Array.Empty(), + EquipmentNodes: Array.Empty(), + DriverInstancePlans: Array.Empty(), + ScriptedAlarmPlans: Array.Empty(), + 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); + } + + /// Verifies that two tags sharing a FolderPath produce a single EnsureFolder call + /// (deduped) but one EnsureVariable per tag. + [Fact] + public void MaterialiseGalaxyTags_dedupes_folders_for_tags_sharing_a_path() + { + var sink = new RecordingSink(); + var applier = new Phase7Applier(sink, NullLogger.Instance); + + var composition = new Phase7CompositionResult( + UnsAreas: Array.Empty(), + UnsLines: Array.Empty(), + EquipmentNodes: Array.Empty(), + DriverInstancePlans: Array.Empty(), + ScriptedAlarmPlans: Array.Empty(), + 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")); + } + + /// Verifies that added Galaxy tags in an otherwise-empty plan trigger an address-space rebuild. + [Fact] + public void Added_galaxy_tags_trigger_rebuild() + { + var sink = new RecordingSink(); + var applier = new Phase7Applier(sink, NullLogger.Instance); + + var plan = new Phase7Plan( + AddedEquipment: Array.Empty(), + RemovedEquipment: Array.Empty(), + ChangedEquipment: Array.Empty(), + AddedDrivers: Array.Empty(), + RemovedDrivers: Array.Empty(), + ChangedDrivers: Array.Empty(), + AddedAlarms: Array.Empty(), + RemovedAlarms: Array.Empty(), + ChangedAlarms: Array.Empty(), + AddedGalaxyTags: new[] + { + new GalaxyTagPlan("t1", "drv", "section.area", "Temperature", "Float", "section.area.Temperature"), + }, + RemovedGalaxyTags: Array.Empty(), + ChangedGalaxyTags: Array.Empty()); + + var outcome = applier.Apply(plan); + + outcome.RebuildCalled.ShouldBeTrue(); + outcome.AddedNodes.ShouldBe(1); + sink.RebuildCalls.ShouldBe(1); + } + private static Phase7Plan EmptyPlan => new( Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty()); + Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), Array.Empty(), Array.Empty()); private static Phase7Plan WithEquipmentRemoval(params string[] ids) => new( AddedEquipment: Array.Empty(), @@ -125,7 +223,10 @@ public sealed class Phase7ApplierTests ChangedDrivers: Array.Empty(), AddedAlarms: Array.Empty(), RemovedAlarms: Array.Empty(), - ChangedAlarms: Array.Empty()); + ChangedAlarms: Array.Empty(), + AddedGalaxyTags: Array.Empty(), + RemovedGalaxyTags: Array.Empty(), + ChangedGalaxyTags: Array.Empty()); private sealed class RecordingSink : IOpcUaAddressSpaceSink {