Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7PlannerTests.cs
T
Joseph Doherty aaf869145a fix(opcua): equipment-tag planner diff + folder-scoped NodeIds (review findings)
Two bundle-review fixes + idempotency coverage:
- CRITICAL: the planner ignored EquipmentTags, so an incremental deploy changing only
  equipment tags produced an empty plan and HandleRebuild short-circuited before
  materialising them. Add TagId to EquipmentTagPlan + Added/Removed/ChangedEquipmentTags
  to Phase7Plan (diffed by TagId, in IsEmpty, driving Apply's needsRebuild) — mirroring
  the GalaxyTags treatment.
- IMPORTANT: equipment variable NodeId was the raw driver FullName, which collides across
  identical machines (e.g. two PLCs both exposing register 40001) — the second variable
  was silently dropped. NodeId is now folder-scoped (parent/Name); FullName stays on
  EquipmentTagPlan for the later values-routing milestone.
- Task 4: SDK-backed idempotency test (double-apply -> single variable); restart-safety
  confirmed (RestoreApplied reuses the same RebuildAddressSpace -> HandleRebuild path).
- Minor: align composer equipment-tag sort with the artifact decoder (coalesce FolderPath).
2026-06-06 15:02:50 -04:00

185 lines
8.2 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7PlannerTests
{
/// <summary>Verifies that empty inputs produce an empty plan.</summary>
[Fact]
public void Empty_inputs_produce_empty_plan()
{
var prev = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = prev;
var plan = Phase7Planner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue();
}
/// <summary>Verifies that identical compositions produce an empty plan.</summary>
[Fact]
public void Identical_compositions_produce_empty_plan()
{
var eq = new EquipmentNode("eq-1", "Eq 1", "line-1");
var prev = new Phase7CompositionResult(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue();
}
/// <summary>Verifies an equipment-tag-only delta (no equipment/driver/alarm/galaxy change)
/// yields a NON-empty plan, so OpcUaPublishActor.HandleRebuild does not short-circuit at the
/// IsEmpty gate before materialising the new equipment variables.</summary>
[Fact]
public void Equipment_tag_only_change_yields_non_empty_plan_with_added_tag()
{
var prev = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
{
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001"),
},
};
var plan = Phase7Planner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.AddedEquipmentTags.Single().TagId.ShouldBe("tag-1");
plan.RemovedEquipmentTags.ShouldBeEmpty();
plan.ChangedEquipmentTags.ShouldBeEmpty();
}
/// <summary>Verifies that new equipment goes to the AddedEquipment list.</summary>
[Fact]
public void New_equipment_goes_to_AddedEquipment()
{
var prev = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
new[] { new EquipmentNode("eq-1", "A", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-1");
plan.RemovedEquipment.ShouldBeEmpty();
plan.ChangedEquipment.ShouldBeEmpty();
}
/// <summary>Verifies that disappeared equipment goes to the RemovedEquipment list.</summary>
[Fact]
public void Disappeared_equipment_goes_to_RemovedEquipment()
{
var prev = new Phase7CompositionResult(
new[] { new EquipmentNode("eq-1", "A", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-1");
plan.AddedEquipment.ShouldBeEmpty();
}
/// <summary>Verifies that equipment with same id but different display name routes to ChangedEquipment.</summary>
[Fact]
public void Same_id_with_different_display_name_routes_to_ChangedEquipment()
{
var prev = new Phase7CompositionResult(
new[] { new EquipmentNode("eq-1", "Old", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
new[] { new EquipmentNode("eq-1", "New", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
plan.ChangedEquipment.Single().Previous.DisplayName.ShouldBe("Old");
plan.ChangedEquipment.Single().Current.DisplayName.ShouldBe("New");
plan.AddedEquipment.ShouldBeEmpty();
plan.RemovedEquipment.ShouldBeEmpty();
}
/// <summary>Verifies that driver config changes route to ChangedDrivers.</summary>
[Fact]
public void Driver_config_change_routes_to_ChangedDrivers()
{
var prev = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(),
new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"old\"}") },
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(),
new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"new\"}") },
Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
plan.ChangedDrivers.Single().Current.ConfigJson.ShouldContain("new");
}
/// <summary>Verifies that alarm message template changes route to ChangedAlarms.</summary>
[Fact]
public void Alarm_message_template_change_routes_to_ChangedAlarms()
{
var prev = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(),
Array.Empty<DriverInstancePlan>(),
new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "old") });
var next = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(),
Array.Empty<DriverInstancePlan>(),
new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "new") });
var plan = Phase7Planner.Compute(prev, next);
plan.ChangedAlarms.Single().Current.MessageTemplate.ShouldBe("new");
}
/// <summary>Verifies that added and removed lists are sorted by id for deterministic ordering.</summary>
[Fact]
public void Added_and_removed_lists_are_sorted_by_id_for_deterministic_ordering()
{
var prev = new Phase7CompositionResult(
new[] { new EquipmentNode("z", "Z", "line-1"), new EquipmentNode("a", "A", "line-1") },
Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next);
plan.RemovedEquipment.Select(e => e.EquipmentId).ShouldBe(new[] { "a", "z" });
}
/// <summary>Verifies that mixed changes across all three classes are captured in one pass.</summary>
[Fact]
public void Mixed_changes_across_all_three_classes_are_captured_in_one_pass()
{
var prev = new Phase7CompositionResult(
new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-drop", "Drop", "line-1") },
new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":1}") },
new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1") });
var next = new Phase7CompositionResult(
new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-new", "New", "line-1") },
new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":2}") },
new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1"), new ScriptedAlarmPlan("a-new", "eq-new", "s2", "t2") });
var plan = Phase7Planner.Compute(prev, next);
plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-new");
plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-drop");
plan.ChangedEquipment.ShouldBeEmpty();
plan.ChangedDrivers.Single().Current.DriverInstanceId.ShouldBe("drv-change");
plan.AddedAlarms.Single().ScriptedAlarmId.ShouldBe("a-new");
}
}