118933d5d5
Review 01 U2. The composer never emits EquipmentTags in v3 (device values live in the Raw subtree and reach equipment as UnsReferenceVariables), so the equipment-tag plan diff sets (Added/Removed/ChangedEquipmentTags, EquipmentTagDelta), the planner's diff over them, the classifier's TagDeltaIsSurgicalEligible and its Rebuild clause, the applier's MaterialiseEquipmentTags pass, surgical ChangedEquipmentTags loop, removed-tag teardown loop and add-announcement term, and the publish actor's equipment-tag pass were unreachable, maintained dead weight. IsEmpty keeps its semantics (an otherwise-empty plan is still Empty). Kept, deliberately: AddressSpaceComposition.EquipmentTags and the EquipmentTagPlan record — Runtime's DeploymentArtifact (outside this lane) still assigns the empty set and the artifact parity tests assert it; follow-up F2 removes both together. Tests: the equipment-tag vehicle tests for PureRemove / AddRemoveMix / the rebuild ratchet are re-expressed over raw tags (same applier paths); the equipment-tag surgical and classifier tests are deleted (their raw / UNS-reference equivalents already exist); SubscriptionSurvivalTests now materialises its tags through MaterialiseRawSubtree. Refs #732 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
229 lines
9.5 KiB
C#
229 lines
9.5 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
/// <summary>
|
|
/// R2-07 T1 — table-driven tests over <see cref="AddressSpaceChangeClassifier.Classify"/>. The
|
|
/// classifier is a pure function over an <see cref="AddressSpacePlan"/>: it names the delta class the
|
|
/// applier routes each plan through (Empty / AttributeOnly / PureAdd / PureRemove / AddRemoveMix /
|
|
/// Rebuild). First-match-wins per the plan's classification table, with the default-closed
|
|
/// Rebuild safety valve catching every node-affecting change.
|
|
/// </summary>
|
|
public sealed class AddressSpaceChangeClassifierTests
|
|
{
|
|
[Fact]
|
|
public void Empty_plan_classifies_as_Empty()
|
|
{
|
|
AddressSpaceChangeClassifier.Classify(Plan()).ShouldBe(AddressSpaceChangeKind.Empty);
|
|
}
|
|
|
|
// ----- single add class ⇒ PureAdd -----
|
|
|
|
[Fact]
|
|
public void Added_equipment_only_is_PureAdd()
|
|
{
|
|
var plan = Plan(addedEquipment: new[] { Eq("eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
|
|
}
|
|
|
|
[Fact]
|
|
public void Added_alarm_only_is_PureAdd()
|
|
{
|
|
var plan = Plan(addedAlarms: new[] { Alarm("alm-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
|
|
}
|
|
|
|
[Fact]
|
|
public void Added_raw_tag_only_is_PureAdd()
|
|
{
|
|
var plan = Plan(addedTags: new[] { Tag("tag-1", "eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
|
|
}
|
|
|
|
[Fact]
|
|
public void Added_virtual_tag_only_is_PureAdd()
|
|
{
|
|
var plan = Plan(addedVtags: new[] { Vtag("vt-1", "eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
|
|
}
|
|
|
|
// ----- single remove class ⇒ PureRemove -----
|
|
|
|
[Fact]
|
|
public void Removed_equipment_only_is_PureRemove()
|
|
{
|
|
var plan = Plan(removedEquipment: new[] { Eq("eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
|
|
}
|
|
|
|
[Fact]
|
|
public void Removed_alarm_only_is_PureRemove()
|
|
{
|
|
var plan = Plan(removedAlarms: new[] { Alarm("alm-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
|
|
}
|
|
|
|
[Fact]
|
|
public void Removed_raw_tag_only_is_PureRemove()
|
|
{
|
|
var plan = Plan(removedTags: new[] { Tag("tag-1", "eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
|
|
}
|
|
|
|
[Fact]
|
|
public void Removed_virtual_tag_only_is_PureRemove()
|
|
{
|
|
var plan = Plan(removedVtags: new[] { Vtag("vt-1", "eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
|
|
}
|
|
|
|
// ----- add AND remove ⇒ AddRemoveMix -----
|
|
|
|
[Fact]
|
|
public void Add_and_remove_is_AddRemoveMix()
|
|
{
|
|
var plan = Plan(addedTags: new[] { Tag("tag-1", "eq-1") }, removedTags: new[] { Tag("tag-2", "eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AddRemoveMix);
|
|
}
|
|
|
|
[Fact]
|
|
public void Add_equipment_and_remove_vtag_is_AddRemoveMix()
|
|
{
|
|
var plan = Plan(addedEquipment: new[] { Eq("eq-2") }, removedVtags: new[] { Vtag("vt-1", "eq-1") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AddRemoveMix);
|
|
}
|
|
|
|
// ----- node-affecting change ⇒ Rebuild -----
|
|
|
|
[Fact]
|
|
public void Changed_equipment_is_Rebuild()
|
|
{
|
|
var plan = Plan(changedEquipment: new[]
|
|
{
|
|
new AddressSpacePlan.EquipmentDelta(Eq("eq-1"), Eq("eq-1") with { DisplayName = "New" }),
|
|
});
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changed_alarm_is_Rebuild()
|
|
{
|
|
var plan = Plan(changedAlarms: new[]
|
|
{
|
|
new AddressSpacePlan.AlarmDelta(Alarm("alm-1"), Alarm("alm-1") with { MessageTemplate = "changed" }),
|
|
});
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
|
|
}
|
|
|
|
[Fact]
|
|
public void Node_relevant_changed_vtag_is_Rebuild()
|
|
{
|
|
// Name change re-derives the NodeId ⇒ node-relevant ⇒ Rebuild.
|
|
var plan = Plan(changedVtags: new[]
|
|
{
|
|
new AddressSpacePlan.EquipmentVirtualTagDelta(
|
|
Vtag("vt-1", "eq-1", name: "A"),
|
|
Vtag("vt-1", "eq-1", name: "B")),
|
|
});
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
|
|
}
|
|
|
|
// ----- surgical-only / rename-only / driver-only ⇒ AttributeOnly -----
|
|
|
|
[Fact]
|
|
public void Node_irrelevant_changed_vtag_only_is_AttributeOnly()
|
|
{
|
|
// Only Expression differs ⇒ node-irrelevant ⇒ AttributeOnly.
|
|
var plan = Plan(changedVtags: new[]
|
|
{
|
|
new AddressSpacePlan.EquipmentVirtualTagDelta(
|
|
Vtag("vt-1", "eq-1", expression: "a"),
|
|
Vtag("vt-1", "eq-1", expression: "a * 2")),
|
|
});
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Folder_renames_only_is_AttributeOnly()
|
|
{
|
|
var plan = Plan(renamedFolders: new[] { new AddressSpacePlan.FolderRename("area-1", "New Area") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
|
}
|
|
|
|
[Fact]
|
|
public void Driver_only_deltas_are_AttributeOnly_node_inert()
|
|
{
|
|
// Driver adds/removes/changes never touch the address space — excluded from hasAdds/hasRemoves.
|
|
var plan = Plan(
|
|
addedDrivers: new[] { new DriverInstancePlan("d-1", "Modbus", "{}") },
|
|
removedDrivers: new[] { new DriverInstancePlan("d-2", "Modbus", "{}") },
|
|
changedDrivers: new[]
|
|
{
|
|
new AddressSpacePlan.DriverDelta(
|
|
new DriverInstancePlan("d-3", "Modbus", "{\"v\":1}"),
|
|
new DriverInstancePlan("d-3", "Modbus", "{\"v\":2}")),
|
|
});
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
|
}
|
|
|
|
// ----- mixed-with-surgical: adds still win over coincident surgical changes -----
|
|
|
|
[Fact]
|
|
public void Adds_with_coincident_folder_rename_is_PureAdd()
|
|
{
|
|
var plan = Plan(
|
|
addedTags: new[] { Tag("tag-new", "eq-1") },
|
|
renamedFolders: new[] { new AddressSpacePlan.FolderRename("area-1", "New Area") });
|
|
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
|
|
}
|
|
|
|
// ----- construction helpers -----
|
|
|
|
private static EquipmentNode Eq(string id) => new(id, id, "line-1");
|
|
|
|
private static ScriptedAlarmPlan Alarm(string id) => new(id, "eq-1", "scr-1", "msg");
|
|
|
|
private static RawTagPlan Tag(string id, string device) =>
|
|
new(id, $"{device}/{id}", ParentNodeId: device, "drv", Name: id, DataType: "Float", Writable: false, Alarm: null,
|
|
ReferencingEquipmentPaths: Array.Empty<string>());
|
|
|
|
private static EquipmentVirtualTagPlan Vtag(string id, string equipmentId, string name = "Efficiency", string expression = "ctx.GetTag(\"a\")") =>
|
|
new(id, equipmentId, FolderPath: "", Name: name, DataType: "Float64", Expression: expression, DependencyRefs: new[] { "a" });
|
|
|
|
private static AddressSpacePlan Plan(
|
|
IReadOnlyList<EquipmentNode>? addedEquipment = null,
|
|
IReadOnlyList<EquipmentNode>? removedEquipment = null,
|
|
IReadOnlyList<AddressSpacePlan.EquipmentDelta>? changedEquipment = null,
|
|
IReadOnlyList<DriverInstancePlan>? addedDrivers = null,
|
|
IReadOnlyList<DriverInstancePlan>? removedDrivers = null,
|
|
IReadOnlyList<AddressSpacePlan.DriverDelta>? changedDrivers = null,
|
|
IReadOnlyList<ScriptedAlarmPlan>? addedAlarms = null,
|
|
IReadOnlyList<ScriptedAlarmPlan>? removedAlarms = null,
|
|
IReadOnlyList<AddressSpacePlan.AlarmDelta>? changedAlarms = null,
|
|
IReadOnlyList<RawTagPlan>? addedTags = null,
|
|
IReadOnlyList<RawTagPlan>? removedTags = null,
|
|
IReadOnlyList<EquipmentVirtualTagPlan>? addedVtags = null,
|
|
IReadOnlyList<EquipmentVirtualTagPlan>? removedVtags = null,
|
|
IReadOnlyList<AddressSpacePlan.EquipmentVirtualTagDelta>? changedVtags = null,
|
|
IReadOnlyList<AddressSpacePlan.FolderRename>? renamedFolders = null) =>
|
|
new(
|
|
addedEquipment ?? Array.Empty<EquipmentNode>(),
|
|
removedEquipment ?? Array.Empty<EquipmentNode>(),
|
|
changedEquipment ?? Array.Empty<AddressSpacePlan.EquipmentDelta>(),
|
|
addedDrivers ?? Array.Empty<DriverInstancePlan>(),
|
|
removedDrivers ?? Array.Empty<DriverInstancePlan>(),
|
|
changedDrivers ?? Array.Empty<AddressSpacePlan.DriverDelta>(),
|
|
addedAlarms ?? Array.Empty<ScriptedAlarmPlan>(),
|
|
removedAlarms ?? Array.Empty<ScriptedAlarmPlan>(),
|
|
changedAlarms ?? Array.Empty<AddressSpacePlan.AlarmDelta>())
|
|
{
|
|
AddedRawTags = addedTags ?? Array.Empty<RawTagPlan>(),
|
|
RemovedRawTags = removedTags ?? Array.Empty<RawTagPlan>(),
|
|
AddedEquipmentVirtualTags = addedVtags ?? Array.Empty<EquipmentVirtualTagPlan>(),
|
|
RemovedEquipmentVirtualTags = removedVtags ?? Array.Empty<EquipmentVirtualTagPlan>(),
|
|
ChangedEquipmentVirtualTags = changedVtags ?? Array.Empty<AddressSpacePlan.EquipmentVirtualTagDelta>(),
|
|
RenamedFolders = renamedFolders ?? Array.Empty<AddressSpacePlan.FolderRename>(),
|
|
};
|
|
}
|