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:
+15
-118
@@ -1,130 +1,27 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Proves follow-up E: the equipment's <c>DriverInstanceId</c> / <c>DeviceId</c> bindings and the
|
||||
/// resolved <c>DeviceHost</c> (parsed from the bound <see cref="Device"/>'s schemaless
|
||||
/// <c>DeviceConfig</c> JSON) round-trip with byte-parity through both <see cref="EquipmentNode"/>
|
||||
/// producers: the live-edit composer (<see cref="AddressSpaceComposer.Compose(System.Collections.Generic.IReadOnlyList{UnsArea},System.Collections.Generic.IReadOnlyList{UnsLine},System.Collections.Generic.IReadOnlyList{Equipment},System.Collections.Generic.IReadOnlyList{DriverInstance},System.Collections.Generic.IReadOnlyList{ScriptedAlarm},System.Collections.Generic.IReadOnlyList{Device})"/>)
|
||||
/// and the artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>).
|
||||
/// A secondary/follower node decoding a serialized artifact MUST see the same DeviceHost as the
|
||||
/// primary so it grafts FixedTree / partitions multi-device drivers identically. Both sides resolve
|
||||
/// the host through the shared <c>DeviceConfigIntent.TryExtractHost</c> (single source
|
||||
/// of truth + identical trim + lower-case normalization).
|
||||
/// Equipment <c>DriverInstanceId</c> / <c>DeviceId</c> binding + resolved <c>DeviceHost</c> byte-parity
|
||||
/// across the two <c>EquipmentNode</c> producers (composer + artifact decode).
|
||||
/// <para>
|
||||
/// <b>v3 RETIRED:</b> equipment no longer binds a driver/device — <c>EquipmentNode</c> is
|
||||
/// <c>(EquipmentId, DisplayName, UnsLineId)</c> only, <c>Equipment</c> dropped
|
||||
/// <c>DriverInstanceId</c>/<c>DeviceId</c>, and devices bind to drivers (tags bind to devices) while
|
||||
/// equipment references raw tags via <c>UnsTagReference</c>. The device-endpoint resolution this test
|
||||
/// characterized (fold the sole device's <c>DeviceConfig</c> host into the driver config so a follower
|
||||
/// node dials the same endpoint) is now proven by
|
||||
/// <see cref="DeploymentArtifactRawPathTests.ParseDriverInstances_computes_nested_RawPath_and_merges_device_endpoint"/>.
|
||||
/// Kept as a skipped marker so the retired coverage is discoverable, not silently deleted.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class DeploymentArtifactDeviceHostParityTests
|
||||
{
|
||||
/// <summary>
|
||||
/// One draft exercising every branch: driver + device + host (with a mixed-case/whitespace host
|
||||
/// that must normalize identically on both sides); a driver-bound equipment with NO device
|
||||
/// (DeviceId null ⇒ DeviceHost null); a driver-less, device-less equipment (all three null); and a
|
||||
/// device whose config carries no <c>HostAddress</c> (DeviceId carried, DeviceHost null). The decoded
|
||||
/// <c>EquipmentNodes</c> must equal the composer's element-wise (positional-record value equality)
|
||||
/// and in the same order.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
/// <summary>Retired: equipment↔device host binding removed in v3; superseded by
|
||||
/// DeploymentArtifactRawPathTests device-endpoint merge. See class remarks.</summary>
|
||||
[Fact(Skip = DarkAddressSpaceReasons.EquipmentDeviceBindingRetired)]
|
||||
public void Composer_and_artifact_agree_on_equipment_node_device_host()
|
||||
{
|
||||
// eq-1: driver d1 + device dev1 (host needs trim + lower-case)
|
||||
var eq1 = NewEquipment("eq-1", driver: "d1", device: "dev1");
|
||||
// eq-2: driver d1, NO device → DeviceHost null
|
||||
var eq2 = NewEquipment("eq-2", driver: "d1", device: null);
|
||||
// eq-3: driver-less + device-less → all three null
|
||||
var eq3 = NewEquipment("eq-3", driver: null, device: null);
|
||||
// eq-4: device dev-nohost whose config has no HostAddress → DeviceHost null
|
||||
var eq4 = NewEquipment("eq-4", driver: "d1", device: "dev-nohost");
|
||||
|
||||
var dev1 = NewDevice("dev1", "d1", "{\"HostAddress\":\" HOST-A:8193 \"}"); // → host-a:8193
|
||||
var devNoHost = NewDevice("dev-nohost", "d1", "{\"Port\":502}"); // → null
|
||||
|
||||
var equipment = new[] { eq1, eq2, eq3, eq4 };
|
||||
var devices = new[] { dev1, devNoHost };
|
||||
|
||||
// ---- Side 1: the live-edit composer ----
|
||||
var composed = AddressSpaceComposer.Compose(
|
||||
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), equipment,
|
||||
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), devices: devices);
|
||||
|
||||
// ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ----
|
||||
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Equipment = equipment.Select(ToEquipmentSnapshot).ToArray(),
|
||||
Devices = devices.Select(ToDeviceSnapshot).ToArray(),
|
||||
});
|
||||
|
||||
var decoded = DeploymentArtifact.ParseComposition(blob);
|
||||
|
||||
// ---- Full byte-parity: every field, same order (positional-record value equality) ----
|
||||
decoded.EquipmentNodes.Count.ShouldBe(4);
|
||||
decoded.EquipmentNodes.SequenceEqual(composed.EquipmentNodes).ShouldBeTrue();
|
||||
|
||||
// Spell out per-equipment so a divergence names the offending node.
|
||||
var d1Node = decoded.EquipmentNodes.Single(e => e.EquipmentId == "eq-1");
|
||||
d1Node.DriverInstanceId.ShouldBe("d1");
|
||||
d1Node.DeviceId.ShouldBe("dev1");
|
||||
d1Node.DeviceHost.ShouldBe("host-a:8193"); // trimmed + lower-cased on both sides
|
||||
|
||||
var d2Node = decoded.EquipmentNodes.Single(e => e.EquipmentId == "eq-2");
|
||||
d2Node.DriverInstanceId.ShouldBe("d1");
|
||||
d2Node.DeviceId.ShouldBeNull();
|
||||
d2Node.DeviceHost.ShouldBeNull();
|
||||
|
||||
var d3Node = decoded.EquipmentNodes.Single(e => e.EquipmentId == "eq-3");
|
||||
d3Node.DriverInstanceId.ShouldBeNull();
|
||||
d3Node.DeviceId.ShouldBeNull();
|
||||
d3Node.DeviceHost.ShouldBeNull();
|
||||
|
||||
var d4Node = decoded.EquipmentNodes.Single(e => e.EquipmentId == "eq-4");
|
||||
d4Node.DriverInstanceId.ShouldBe("d1");
|
||||
d4Node.DeviceId.ShouldBe("dev-nohost");
|
||||
d4Node.DeviceHost.ShouldBeNull();
|
||||
}
|
||||
|
||||
private static Equipment NewEquipment(string id, string? driver, string? device) => new()
|
||||
{
|
||||
EquipmentId = id,
|
||||
DriverInstanceId = driver,
|
||||
DeviceId = device,
|
||||
UnsLineId = "line-1",
|
||||
Name = id,
|
||||
MachineCode = id.ToUpperInvariant(),
|
||||
};
|
||||
|
||||
private static Device NewDevice(string deviceId, string driverInstanceId, string deviceConfig) => new()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DriverInstanceId = driverInstanceId,
|
||||
Name = deviceId,
|
||||
DeviceConfig = deviceConfig,
|
||||
};
|
||||
|
||||
/// <summary>The Pascal-case snapshot an <see cref="Equipment"/> EF entity serialises to in the
|
||||
/// artifact (matches ConfigComposer) — including the nullable <c>DriverInstanceId</c> / <c>DeviceId</c>
|
||||
/// the equipment-node decoder re-reads.</summary>
|
||||
private static object ToEquipmentSnapshot(Equipment e) => new
|
||||
{
|
||||
e.EquipmentId,
|
||||
e.Name,
|
||||
e.MachineCode,
|
||||
e.UnsLineId,
|
||||
e.DriverInstanceId,
|
||||
e.DeviceId,
|
||||
};
|
||||
|
||||
/// <summary>The Pascal-case snapshot a <see cref="Device"/> EF entity serialises to in the artifact —
|
||||
/// the decoder re-reads <c>DeviceId</c> + the raw <c>DeviceConfig</c> blob the host rides inside.</summary>
|
||||
private static object ToDeviceSnapshot(Device d) => new
|
||||
{
|
||||
d.DeviceId,
|
||||
d.DriverInstanceId,
|
||||
d.Name,
|
||||
d.DeviceConfig,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user