Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerPurityTests.cs
T
Joseph Doherty 08cddfe128 fix(opcua): UNS equipment folders browse by friendly Name, NodeId stays the logical Id
Equipment folder DisplayName was the colloquial MachineCode; the live rebuild (artifact
ReadEquipmentNode) + composer now use the UNS level-5 Name segment, matching Area/Line
folders + EquipmentNodeWalker. NodeId stays the logical EquipmentId so browse-path
resolution + ACLs are unaffected.
2026-06-06 14:51:12 -04:00

123 lines
4.6 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7ComposerPurityTests
{
/// <summary>Verifies empty inputs produce empty result.</summary>
[Fact]
public void Empty_inputs_produce_empty_result()
{
var result = Phase7Composer.Compose(
equipment: Array.Empty<Equipment>(),
driverInstances: Array.Empty<DriverInstance>(),
scriptedAlarms: Array.Empty<ScriptedAlarm>());
result.EquipmentNodes.ShouldBeEmpty();
result.DriverInstancePlans.ShouldBeEmpty();
result.ScriptedAlarmPlans.ShouldBeEmpty();
}
/// <summary>Verifies same inputs in different order produce structurally equal results.</summary>
[Fact]
public void Same_inputs_in_different_order_produce_structurally_equal_results()
{
var e1 = NewEquipment("eq-1");
var e2 = NewEquipment("eq-2");
var d1 = NewDriver("drv-1");
var d2 = NewDriver("drv-2");
var a1 = NewAlarm("a-1", "eq-1");
var a2 = NewAlarm("a-2", "eq-2");
var r1 = Phase7Composer.Compose(
equipment: new[] { e1, e2 },
driverInstances: new[] { d1, d2 },
scriptedAlarms: new[] { a1, a2 });
var r2 = Phase7Composer.Compose(
equipment: new[] { e2, e1 },
driverInstances: new[] { d2, d1 },
scriptedAlarms: new[] { a2, a1 });
r1.EquipmentNodes.ShouldBe(r2.EquipmentNodes);
r1.DriverInstancePlans.ShouldBe(r2.DriverInstancePlans);
r1.ScriptedAlarmPlans.ShouldBe(r2.ScriptedAlarmPlans);
}
/// <summary>Verifies Compose is pure with identical repeated calls.</summary>
[Fact]
public void Compose_is_pure_repeated_call_returns_element_identical_output()
{
var equipment = new[] { NewEquipment("eq-a"), NewEquipment("eq-b") };
var drivers = new[] { NewDriver("drv-x") };
var alarms = new[] { NewAlarm("alarm-1", "eq-a") };
var r1 = Phase7Composer.Compose(equipment, drivers, alarms);
var r2 = Phase7Composer.Compose(equipment, drivers, alarms);
// Record equality won't help here — IReadOnlyList<T> uses reference equality. Compare
// element-wise to verify the pure-function contract.
r1.EquipmentNodes.ShouldBe(r2.EquipmentNodes);
r1.DriverInstancePlans.ShouldBe(r2.DriverInstancePlans);
r1.ScriptedAlarmPlans.ShouldBe(r2.ScriptedAlarmPlans);
}
/// <summary>Verifies output is sorted by natural key.</summary>
[Fact]
public void Output_is_sorted_by_natural_key()
{
var equipment = new[] { NewEquipment("z"), NewEquipment("a"), NewEquipment("m") };
var result = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>());
result.EquipmentNodes.Select(e => e.EquipmentId)
.ShouldBe(new[] { "a", "m", "z" });
}
/// <summary>Verifies UNS equipment folders browse by the friendly UNS <c>Name</c> segment
/// (not the colloquial MachineCode, not the logical EquipmentId) while the NodeId stays the
/// logical EquipmentId — so browse-path resolution + ACLs are unaffected (decision #3).</summary>
[Fact]
public void Equipment_node_DisplayName_is_the_UNS_Name_not_MachineCode()
{
var equipment = new[] { NewEquipment("filling-eq") }; // Name="filling-eq", MachineCode="FILLING-EQ"
var node = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>())
.EquipmentNodes.ShouldHaveSingleItem();
node.EquipmentId.ShouldBe("filling-eq"); // NodeId stays the logical Id
node.DisplayName.ShouldBe("filling-eq"); // browse name = UNS Name segment
node.DisplayName.ShouldNotBe("FILLING-EQ"); // not the colloquial MachineCode
}
private static Equipment NewEquipment(string id) => new()
{
EquipmentId = id,
DriverInstanceId = "drv-1",
UnsLineId = "line-1",
Name = id,
MachineCode = id.ToUpperInvariant(),
};
private static DriverInstance NewDriver(string id) => new()
{
DriverInstanceId = id,
ClusterId = "cluster-1",
NamespaceId = "ns-1",
Name = id,
DriverType = "Stub",
DriverConfig = "{\"k\":\"v\"}",
};
private static ScriptedAlarm NewAlarm(string id, string equipmentId) => new()
{
ScriptedAlarmId = id,
EquipmentId = equipmentId,
Name = id,
AlarmType = "AlarmCondition",
MessageTemplate = "{TagPath} alarm",
PredicateScriptId = "script-1",
};
}