v3 B1-WP6: migrate Core + Server test projects to greenfield schema

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.
This commit is contained in:
Joseph Doherty
2026-07-15 21:26:00 -04:00
parent 604928b29d
commit 329144b1aa
26 changed files with 425 additions and 1445 deletions
@@ -9,12 +9,23 @@ 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([], [], [], []));
EquipmentNodeWalker.Walk(rec, new EquipmentNamespaceContent([], [], []));
rec.Children.ShouldBeEmpty();
}
@@ -26,8 +37,7 @@ public sealed class EquipmentNodeWalkerTests
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")],
Tags: []);
Equipment: [Eq("eq-1", "line-1", "oven-3"), Eq("eq-2", "line-2", "press-7")]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -49,7 +59,7 @@ public sealed class EquipmentNodeWalkerTests
eq.ZTag = null;
eq.SAPID = null;
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -72,7 +82,7 @@ public sealed class EquipmentNodeWalkerTests
eq.ZTag = "ZT-0042";
eq.SAPID = "10000042";
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -90,7 +100,7 @@ public sealed class EquipmentNodeWalkerTests
eq.Manufacturer = "Trumpf";
eq.Model = "TruLaser-3030";
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -108,7 +118,7 @@ public sealed class EquipmentNodeWalkerTests
{
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], []);
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -117,42 +127,18 @@ public sealed class EquipmentNodeWalkerTests
equipmentNode.Children.ShouldNotContain(c => c.BrowseName == "Identification");
}
/// <summary>Verifies that walking emits a variable for each bound tag under equipment.</summary>
[Fact]
public void Walk_Emits_Variable_Per_BoundTag_Under_Equipment()
/// <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()
{
var eq = Eq("eq-1", "line-1", "oven-3");
var tag1 = NewTag("tag-1", "Temperature", "Int32", "plcaddr-01", equipmentId: "eq-1");
var tag2 = NewTag("tag-2", "Setpoint", "Float32", "plcaddr-02", equipmentId: "eq-1");
var unboundTag = NewTag("tag-3", "Orphan", "Int32", "plcaddr-03", equipmentId: null); // SystemPlatform-style, walker skips
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
[eq], [tag1, tag2, unboundTag]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
var equipmentNode = rec.Children[0].Children[0].Children[0];
equipmentNode.Variables.Count.ShouldBe(2);
equipmentNode.Variables.Select(v => v.BrowseName).ShouldBe(["Setpoint", "Temperature"]);
equipmentNode.Variables.First(v => v.BrowseName == "Temperature").AttributeInfo.FullName.ShouldBe("plcaddr-01");
equipmentNode.Variables.First(v => v.BrowseName == "Setpoint").AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Float32);
}
/// <summary>Verifies that walking falls back to String type for unparseable data types.</summary>
[Fact]
public void Walk_FallsBack_To_String_For_Unparseable_DataType()
{
var eq = Eq("eq-1", "line-1", "oven-3");
var tag = NewTag("tag-1", "Mystery", "NotARealType", "plcaddr-42", equipmentId: "eq-1");
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], [tag]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
var variable = rec.Children[0].Children[0].Children[0].Variables.Single();
variable.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.String);
// 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>
@@ -168,7 +154,7 @@ public sealed class EquipmentNodeWalkerTests
};
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
[eq], [], VirtualTags: [vtag]);
[eq], VirtualTags: [vtag]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -196,7 +182,7 @@ public sealed class EquipmentNodeWalkerTests
};
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
[eq], [], ScriptedAlarms: [alarm]);
[eq], ScriptedAlarms: [alarm]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -229,7 +215,7 @@ public sealed class EquipmentNodeWalkerTests
};
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
[eq], [], VirtualTags: [vtag], ScriptedAlarms: [alarm]);
[eq], VirtualTags: [vtag], ScriptedAlarms: [alarm]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
@@ -241,10 +227,10 @@ public sealed class EquipmentNodeWalkerTests
[Fact]
public void Walk_Null_VirtualTags_And_ScriptedAlarms_Is_Safe()
{
// Backwards-compat — callers that don't populate the new collections still work.
// 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], []);
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content); // must not throw
@@ -252,81 +238,6 @@ public sealed class EquipmentNodeWalkerTests
rec.Children[0].Children[0].Children[0].Variables.ShouldBeEmpty();
}
/// <summary>Verifies that driver tag default NodeSourceKind is Driver.</summary>
[Fact]
public void Driver_tag_default_NodeSourceKind_is_Driver()
{
var eq = Eq("eq-1", "line-1", "oven-3");
var tag = NewTag("t-1", "Temp", "Int32", "plc-01", "eq-1");
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
[eq], [tag]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
var v = rec.Children[0].Children[0].Children[0].Variables.Single();
v.AttributeInfo.Source.ShouldBe(NodeSourceKind.Driver);
v.AttributeInfo.VirtualTagId.ShouldBeNull();
v.AttributeInfo.ScriptedAlarmId.ShouldBeNull();
}
/// <summary>Verifies that ExtractFullName unwraps a JSON object with FullName field.</summary>
[Fact]
public void ExtractFullName_unwraps_json_object_with_FullName_field()
{
EquipmentNodeWalker.ExtractFullName(
"{\"FullName\":\"MESReceiver_001.MoveInBatchID\",\"DataType\":\"Int32\"}")
.ShouldBe("MESReceiver_001.MoveInBatchID");
}
/// <summary>Verifies that ExtractFullName handles S7-style extra fields.</summary>
[Fact]
public void ExtractFullName_handles_S7_style_extra_fields()
{
EquipmentNodeWalker.ExtractFullName(
"{\"FullName\":\"DB1_DBW0\",\"Address\":\"DB1.DBW0\",\"DataType\":\"Int16\"}")
.ShouldBe("DB1_DBW0");
}
/// <summary>Verifies that ExtractFullName returns raw string when input is not JSON.</summary>
[Fact]
public void ExtractFullName_returns_raw_when_not_json()
{
// Drivers that opt out of JSON TagConfig still work — fallback preserves the literal
// string so the driver's IReadable sees whatever the row author stored.
EquipmentNodeWalker.ExtractFullName("raw-tag-ref").ShouldBe("raw-tag-ref");
}
/// <summary>Verifies that ExtractFullName returns raw string when JSON is missing FullName field.</summary>
[Fact]
public void ExtractFullName_returns_raw_when_json_missing_FullName_field()
{
EquipmentNodeWalker.ExtractFullName("{\"Address\":\"DB1.DBW0\"}")
.ShouldBe("{\"Address\":\"DB1.DBW0\"}");
}
/// <summary>Verifies that driver tag FullName passes through from TagConfig JSON.</summary>
[Fact]
public void Driver_tag_FullName_passes_through_from_TagConfig_json()
{
// The walker hands the driver the unwrapped FullName string so IReadable.ReadAsync
// sees the plain address, not the raw TagConfig JSON. Verifies the dispatch contract
// the path-based NodeId refactor relies on.
var eq = Eq("eq-1", "line-1", "oven-3");
var tag = NewTag("t-1", "Temp", "Int32", "plc-01", "eq-1",
tagConfig: "{\"FullName\":\"plc-01/HR200\",\"DataType\":\"Int32\"}");
var content = new EquipmentNamespaceContent(
[Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
[eq], [tag]);
var rec = new RecordingBuilder("root");
EquipmentNodeWalker.Walk(rec, content);
var v = rec.Children[0].Children[0].Children[0].Variables.Single();
v.AttributeInfo.FullName.ShouldBe("plc-01/HR200");
}
// ----- builders for test seed rows -----
private static UnsArea Area(string id, string name) => new()
@@ -344,25 +255,11 @@ public sealed class EquipmentNodeWalkerTests
EquipmentRowId = Guid.NewGuid(),
EquipmentId = equipmentId,
EquipmentUuid = Guid.NewGuid(),
DriverInstanceId = "drv",
UnsLineId = lineId,
Name = name,
MachineCode = "MC-" + name,
};
private static Tag NewTag(string tagId, string name, string dataType, string address,
string? equipmentId, string? tagConfig = null) => new()
{
TagRowId = Guid.NewGuid(),
TagId = tagId,
DriverInstanceId = "drv",
EquipmentId = equipmentId,
Name = name,
DataType = dataType,
AccessLevel = ZB.MOM.WW.OtOpcUa.Configuration.Enums.TagAccessLevel.ReadWrite,
TagConfig = tagConfig ?? address,
};
// ----- recording IAddressSpaceBuilder -----
/// <summary>Test implementation of IAddressSpaceBuilder that records calls.</summary>
@@ -45,7 +45,6 @@ public sealed class IdentificationFolderBuilderTests
private static Equipment EmptyEquipment() => new()
{
EquipmentId = "EQ-000000000001",
DriverInstanceId = "drv-1",
UnsLineId = "line-1",
Name = "eq-1",
MachineCode = "machine_001",
@@ -54,7 +53,6 @@ public sealed class IdentificationFolderBuilderTests
private static Equipment FullyPopulatedEquipment() => new()
{
EquipmentId = "EQ-000000000001",
DriverInstanceId = "drv-1",
UnsLineId = "line-1",
Name = "eq-1",
MachineCode = "machine_001",