329144b1aa
Fix TESTS only (production already green) so the four in-scope test projects compile + pass under the v3 dark address space: - Core.Abstractions.Tests: drop AllowedNamespaceKinds/NamespaceKindCompatibility from DriverTypeMetadata; rewrite EquipmentTagRefResolver tests to the single-func RawPath-lookup contract (no parseRef/transient cache). - Core.Tests: rewrite EquipmentNodeWalkerTests to EquipmentNamespaceContent(Areas, Lines,Equipment,VirtualTags?,ScriptedAlarms?) — folders + VirtualTags + ScriptedAlarms only; raw-tag variables dark (skipped Batch-4 placeholder). Drop retired Equipment.DriverInstanceId. - Runtime.Tests: rewrite golden corpus + TagConfigCorpusParityTests to the v3 RawPath/RawTagEntry round-trip (byte + TagConfigIntent parity; EquipmentTags dark). Salvage VirtualTag + ScriptedAlarm artifact parity to the new Compose signature. Retire the equipment-tag-materialization parity files (Array/Historize/Alias) and the equipment-device-binding DeviceHost parity to documented skipped placeholders. Skip 34 dark equipment-tag routing/value/alarm/write/discovery actor tests with a shared DarkAddressSpaceReasons constant. Re-key cluster-scoped tests to v3 UNS line->area attribution. - ControlPlane.Tests: ConfigComposer round-trips re-keyed to RawFolder/Device (no Namespaces); tag-config gate resolves driver via Device; deploy-gate collision test re-keyed to UnsEffectiveNameCollision; drop retired BadCrossClusterNamespaceBinding case.
319 lines
14 KiB
C#
319 lines
14 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class EquipmentNodeWalkerTests
|
|
{
|
|
/// <summary>
|
|
/// v3: raw tags no longer flow into <see cref="EquipmentNamespaceContent"/> and the walker
|
|
/// emits no raw-tag variable nodes. Raw tags reach the UNS namespace via
|
|
/// <c>UnsTagReference</c> as the dual-namespace fan-out of their backing raw nodes, which lands
|
|
/// with the Batch-4 address space. Tests that asserted the old equipment-tag variable
|
|
/// materialization are skipped with this reason until that projection is built.
|
|
/// </summary>
|
|
internal const string DarkBatch4 =
|
|
"v3 dark address space: raw-tag → UNS variable materialization lands in Batch 4 " +
|
|
"(UnsTagReference fan-out), not in EquipmentNodeWalker.";
|
|
|
|
/// <summary>Verifies that walking empty content emits no nodes.</summary>
|
|
[Fact]
|
|
public void Walk_EmptyContent_EmitsNothing()
|
|
{
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, new EquipmentNamespaceContent([], [], []));
|
|
|
|
rec.Children.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that walking emits Area, Line, and Equipment folders in unsorted order.</summary>
|
|
[Fact]
|
|
public void Walk_EmitsArea_Line_Equipment_Folders_In_UnsOrder()
|
|
{
|
|
var content = new EquipmentNamespaceContent(
|
|
Areas: [Area("area-1", "warsaw"), Area("area-2", "berlin")],
|
|
Lines: [Line("line-1", "area-1", "oven-line"), Line("line-2", "area-2", "press-line")],
|
|
Equipment: [Eq("eq-1", "line-1", "oven-3"), Eq("eq-2", "line-2", "press-7")]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
rec.Children.Select(c => c.BrowseName).ShouldBe(["berlin", "warsaw"]); // ordered by Name
|
|
var warsaw = rec.Children.First(c => c.BrowseName == "warsaw");
|
|
warsaw.Children.Select(c => c.BrowseName).ShouldBe(["oven-line"]);
|
|
warsaw.Children[0].Children.Select(c => c.BrowseName).ShouldBe(["oven-3"]);
|
|
}
|
|
|
|
/// <summary>Verifies that walking adds five identifier properties on equipment nodes, skipping null ZTag and SAPID.</summary>
|
|
[Fact]
|
|
public void Walk_AddsFiveIdentifierProperties_OnEquipmentNode_Skipping_NullZTagSapid()
|
|
{
|
|
var uuid = Guid.NewGuid();
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
eq.EquipmentUuid = uuid;
|
|
eq.MachineCode = "MC-42";
|
|
eq.ZTag = null;
|
|
eq.SAPID = null;
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
var equipmentNode = rec.Children[0].Children[0].Children[0];
|
|
var props = equipmentNode.Properties.Select(p => p.BrowseName).ToList();
|
|
props.ShouldContain("EquipmentId");
|
|
props.ShouldContain("EquipmentUuid");
|
|
props.ShouldContain("MachineCode");
|
|
props.ShouldNotContain("ZTag");
|
|
props.ShouldNotContain("SAPID");
|
|
equipmentNode.Properties.First(p => p.BrowseName == "EquipmentUuid").Value.ShouldBe(uuid.ToString());
|
|
}
|
|
|
|
/// <summary>Verifies that walking adds ZTag and SAPID properties when present.</summary>
|
|
[Fact]
|
|
public void Walk_Adds_ZTag_And_SAPID_When_Present()
|
|
{
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
eq.ZTag = "ZT-0042";
|
|
eq.SAPID = "10000042";
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
var equipmentNode = rec.Children[0].Children[0].Children[0];
|
|
equipmentNode.Properties.First(p => p.BrowseName == "ZTag").Value.ShouldBe("ZT-0042");
|
|
equipmentNode.Properties.First(p => p.BrowseName == "SAPID").Value.ShouldBe("10000042");
|
|
}
|
|
|
|
/// <summary>Verifies that walking materializes an Identification subfolder when any identification field is present.</summary>
|
|
[Fact]
|
|
public void Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent()
|
|
{
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
eq.Manufacturer = "Trumpf";
|
|
eq.Model = "TruLaser-3030";
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
var equipmentNode = rec.Children[0].Children[0].Children[0];
|
|
var identification = equipmentNode.Children.FirstOrDefault(c => c.BrowseName == "Identification");
|
|
identification.ShouldNotBeNull();
|
|
identification!.Properties.Select(p => p.BrowseName).ShouldContain("Manufacturer");
|
|
identification.Properties.Select(p => p.BrowseName).ShouldContain("Model");
|
|
}
|
|
|
|
/// <summary>Verifies that walking omits the Identification subfolder when all identification fields are null.</summary>
|
|
[Fact]
|
|
public void Walk_Omits_Identification_Subfolder_When_AllFieldsNull()
|
|
{
|
|
var eq = Eq("eq-1", "line-1", "oven-3"); // no identification fields
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
var equipmentNode = rec.Children[0].Children[0].Children[0];
|
|
equipmentNode.Children.ShouldNotContain(c => c.BrowseName == "Identification");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch-4 pending: the walker must emit a raw-tag variable per <c>UnsTagReference</c> under
|
|
/// equipment (dual-namespace fan-out). Preserved as a skipped placeholder — the pre-v3 test
|
|
/// asserted equipment-tag variable materialization directly off an equipment-bound <c>Tag</c>,
|
|
/// which no longer exists (tags bind to Devices, not equipment). Intent retained; unskip when
|
|
/// the Batch-4 UNS projection lands.
|
|
/// </summary>
|
|
[Fact(Skip = DarkBatch4)]
|
|
public void Walk_Emits_RawTag_Variables_Under_Equipment_DARK_Batch4()
|
|
{
|
|
// When Batch 4 lands, EquipmentNamespaceContent carries UnsTagReference rows and the walker
|
|
// emits one Variable per referenced raw node (correct DriverDataType, address, source kind).
|
|
}
|
|
|
|
/// <summary>Verifies that walking emits virtual tag variables with Virtual source discriminator.</summary>
|
|
[Fact]
|
|
public void Walk_Emits_VirtualTag_Variables_With_Virtual_Source_Discriminator()
|
|
{
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
var vtag = new VirtualTag
|
|
{
|
|
VirtualTagRowId = Guid.NewGuid(),
|
|
VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "LineRate",
|
|
DataType = "Float32", ScriptId = "scr-1", Historize = true,
|
|
};
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
|
|
[eq], VirtualTags: [vtag]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
var equipmentNode = rec.Children[0].Children[0].Children[0];
|
|
var v = equipmentNode.Variables.Single(x => x.BrowseName == "LineRate");
|
|
v.AttributeInfo.Source.ShouldBe(NodeSourceKind.Virtual);
|
|
v.AttributeInfo.VirtualTagId.ShouldBe("vt-1");
|
|
v.AttributeInfo.ScriptedAlarmId.ShouldBeNull();
|
|
v.AttributeInfo.IsHistorized.ShouldBeTrue();
|
|
v.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Float32);
|
|
}
|
|
|
|
/// <summary>Verifies that walking emits scripted alarm variables with ScriptedAlarm source and IsAlarm flag.</summary>
|
|
[Fact]
|
|
public void Walk_Emits_ScriptedAlarm_Variables_With_ScriptedAlarm_Source_And_IsAlarm()
|
|
{
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
var alarm = new ScriptedAlarm
|
|
{
|
|
ScriptedAlarmRowId = Guid.NewGuid(),
|
|
ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "HighTemp",
|
|
AlarmType = "LimitAlarm", MessageTemplate = "{Temp} exceeded",
|
|
PredicateScriptId = "scr-9", Severity = 800,
|
|
};
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
|
|
[eq], ScriptedAlarms: [alarm]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
var v = rec.Children[0].Children[0].Children[0].Variables.Single(x => x.BrowseName == "HighTemp");
|
|
v.AttributeInfo.Source.ShouldBe(NodeSourceKind.ScriptedAlarm);
|
|
v.AttributeInfo.ScriptedAlarmId.ShouldBe("al-1");
|
|
v.AttributeInfo.VirtualTagId.ShouldBeNull();
|
|
v.AttributeInfo.IsAlarm.ShouldBeTrue();
|
|
v.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Boolean);
|
|
}
|
|
|
|
/// <summary>Verifies that walking skips disabled virtual tags and alarms.</summary>
|
|
[Fact]
|
|
public void Walk_Skips_Disabled_VirtualTags_And_Alarms()
|
|
{
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
var vtag = new VirtualTag
|
|
{
|
|
VirtualTagRowId = Guid.NewGuid(),
|
|
VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Disabled",
|
|
DataType = "Float32", ScriptId = "scr-1", Enabled = false,
|
|
};
|
|
var alarm = new ScriptedAlarm
|
|
{
|
|
ScriptedAlarmRowId = Guid.NewGuid(),
|
|
ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "DisabledAlarm",
|
|
AlarmType = "LimitAlarm", MessageTemplate = "x",
|
|
PredicateScriptId = "scr-9", Enabled = false,
|
|
};
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
|
|
[eq], VirtualTags: [vtag], ScriptedAlarms: [alarm]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content);
|
|
|
|
rec.Children[0].Children[0].Children[0].Variables.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that walking with null virtual tags and scripted alarms is safe.</summary>
|
|
[Fact]
|
|
public void Walk_Null_VirtualTags_And_ScriptedAlarms_Is_Safe()
|
|
{
|
|
// Backwards-compat — callers that don't populate the optional collections still work.
|
|
var eq = Eq("eq-1", "line-1", "oven-3");
|
|
var content = new EquipmentNamespaceContent(
|
|
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
|
|
|
|
var rec = new RecordingBuilder("root");
|
|
EquipmentNodeWalker.Walk(rec, content); // must not throw
|
|
|
|
rec.Children[0].Children[0].Children[0].Variables.ShouldBeEmpty();
|
|
}
|
|
|
|
// ----- builders for test seed rows -----
|
|
|
|
private static UnsArea Area(string id, string name) => new()
|
|
{
|
|
UnsAreaId = id, ClusterId = "c1", Name = name,
|
|
};
|
|
|
|
private static UnsLine Line(string id, string areaId, string name) => new()
|
|
{
|
|
UnsLineId = id, UnsAreaId = areaId, Name = name,
|
|
};
|
|
|
|
private static Equipment Eq(string equipmentId, string lineId, string name) => new()
|
|
{
|
|
EquipmentRowId = Guid.NewGuid(),
|
|
EquipmentId = equipmentId,
|
|
EquipmentUuid = Guid.NewGuid(),
|
|
UnsLineId = lineId,
|
|
Name = name,
|
|
MachineCode = "MC-" + name,
|
|
};
|
|
|
|
// ----- recording IAddressSpaceBuilder -----
|
|
|
|
/// <summary>Test implementation of IAddressSpaceBuilder that records calls.</summary>
|
|
private sealed class RecordingBuilder(string browseName) : IAddressSpaceBuilder
|
|
{
|
|
/// <summary>Gets the browse name of this node.</summary>
|
|
public string BrowseName { get; } = browseName;
|
|
/// <summary>Gets the list of child nodes.</summary>
|
|
public List<RecordingBuilder> Children { get; } = new();
|
|
/// <summary>Gets the list of variables.</summary>
|
|
public List<RecordingVariable> Variables { get; } = new();
|
|
/// <summary>Gets the list of properties.</summary>
|
|
public List<RecordingProperty> Properties { get; } = new();
|
|
|
|
/// <summary>Creates a folder child node.</summary>
|
|
/// <param name="name">The browse name of the folder.</param>
|
|
/// <param name="_">The display name (unused).</param>
|
|
public IAddressSpaceBuilder Folder(string name, string _)
|
|
{
|
|
var child = new RecordingBuilder(name);
|
|
Children.Add(child);
|
|
return child;
|
|
}
|
|
|
|
/// <summary>Creates a variable node.</summary>
|
|
/// <param name="name">The browse name of the variable.</param>
|
|
/// <param name="_">The display name (unused).</param>
|
|
/// <param name="attr">The attribute information for the variable.</param>
|
|
public IVariableHandle Variable(string name, string _, DriverAttributeInfo attr)
|
|
{
|
|
var v = new RecordingVariable(name, attr);
|
|
Variables.Add(v);
|
|
return v;
|
|
}
|
|
|
|
/// <summary>Adds a property to the node.</summary>
|
|
/// <param name="name">The browse name of the property.</param>
|
|
/// <param name="_">The data type (unused).</param>
|
|
/// <param name="value">The value of the property.</param>
|
|
public void AddProperty(string name, DriverDataType _, object? value) =>
|
|
Properties.Add(new RecordingProperty(name, value));
|
|
}
|
|
|
|
/// <summary>Recorded property for test verification.</summary>
|
|
private sealed record RecordingProperty(string BrowseName, object? Value);
|
|
|
|
/// <summary>Recorded variable for test verification.</summary>
|
|
private sealed record RecordingVariable(string BrowseName, DriverAttributeInfo AttributeInfo) : IVariableHandle
|
|
{
|
|
/// <summary>Gets the full reference of the variable.</summary>
|
|
public string FullReference => AttributeInfo.FullName;
|
|
/// <summary>Marks the variable as an alarm condition.</summary>
|
|
/// <param name="info">The alarm condition information.</param>
|
|
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => throw new NotSupportedException();
|
|
}
|
|
}
|