perf(opcuaserver): non-rebuild applies materialise the delta, not the composition
This commit is contained in:
@@ -345,6 +345,100 @@ public sealed class AddressSpaceApplierRawUnsTests
|
||||
sink.Variables.ShouldHaveSingleItem().HistorianTagname.ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-08-15 surgical-deploys (Task 6) — the subset overload materialises ONLY the tags/containers it
|
||||
/// is handed, while still resolving native-alarm notifier targets from the FULL composition's equipment
|
||||
/// map. Both halves matter: narrowing the iteration is the whole point (a one-tag deploy used to re-walk
|
||||
/// every node on the server), and the map has to stay full because the equipment a raw alarm fans to is
|
||||
/// not part of any raw-tag delta — building it from the subset would silently drop the fan-out.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MaterialiseRawSubtree_with_a_subset_ensures_only_the_subset_but_resolves_notifiers_from_the_full_composition()
|
||||
{
|
||||
var sink = new RealmRecordingSink();
|
||||
var untouched = new RawTagPlan("t0", "Plant/A/dev/speed", "Plant/A/dev", "drv", "speed", "Float",
|
||||
Writable: true, Alarm: null, ReferencingEquipmentPaths: Array.Empty<string>());
|
||||
var added = new RawTagPlan("t1", "Plant/A/dev/OverTemp", "Plant/A/dev", "drv", "OverTemp", "Boolean",
|
||||
Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700),
|
||||
ReferencingEquipmentPaths: new[] { "filling/line1/station1" });
|
||||
var composition = new AddressSpaceComposition(
|
||||
new[] { new UnsAreaProjection("a1", "filling") },
|
||||
new[] { new UnsLineProjection("l1", "a1", "line1") },
|
||||
new[] { new EquipmentNode("EQ-1", "station1", "l1") },
|
||||
Array.Empty<DriverInstancePlan>(),
|
||||
Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
RawContainers = new[]
|
||||
{
|
||||
new RawContainerNode("Plant", null, "Plant", RawNodeKind.Folder),
|
||||
new RawContainerNode("Plant/A", "Plant", "A", RawNodeKind.Driver),
|
||||
new RawContainerNode("Plant/A/dev", "Plant/A", "dev", RawNodeKind.Device),
|
||||
},
|
||||
RawTags = new[] { untouched, added },
|
||||
};
|
||||
|
||||
// The delta of a one-tag add: no new containers, exactly one new tag.
|
||||
var failed = NewApplier(sink).MaterialiseRawSubtree(
|
||||
composition,
|
||||
onlyContainers: Array.Empty<RawContainerNode>(),
|
||||
onlyTags: new[] { added });
|
||||
|
||||
failed.ShouldBe(0);
|
||||
sink.Folders.ShouldBeEmpty(); // the three existing containers were NOT re-ensured
|
||||
sink.Variables.ShouldBeEmpty(); // the untouched value tag was NOT re-ensured
|
||||
sink.Conditions.ShouldHaveSingleItem().NodeId.ShouldBe("Plant/A/dev/OverTemp");
|
||||
// …and the notifier still resolved through the FULL composition's Area/Line/Equipment map.
|
||||
sink.NotifierWirings.ShouldHaveSingleItem().FolderNodeIds.ShouldBe(new[] { "EQ-1" });
|
||||
}
|
||||
|
||||
/// <summary>A null subset (the rebuild path) still means "the whole composition" — the overload is additive
|
||||
/// and every pre-existing call site keeps its behaviour.</summary>
|
||||
[Fact]
|
||||
public void MaterialiseRawSubtree_with_no_subset_still_materialises_the_whole_composition()
|
||||
{
|
||||
var sink = new RealmRecordingSink();
|
||||
var composition = new AddressSpaceComposition(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
RawContainers = new[] { new RawContainerNode("Plant", null, "Plant", RawNodeKind.Folder) },
|
||||
RawTags = new[]
|
||||
{
|
||||
new RawTagPlan("t0", "Plant/a", "Plant", "drv", "a", "Float", Writable: true, Alarm: null,
|
||||
ReferencingEquipmentPaths: Array.Empty<string>()),
|
||||
new RawTagPlan("t1", "Plant/b", "Plant", "drv", "b", "Float", Writable: true, Alarm: null,
|
||||
ReferencingEquipmentPaths: Array.Empty<string>()),
|
||||
},
|
||||
};
|
||||
|
||||
NewApplier(sink).MaterialiseRawSubtree(composition);
|
||||
|
||||
sink.Folders.Select(f => f.NodeId).ShouldBe(new[] { "Plant" });
|
||||
sink.Variables.Select(v => v.NodeId).ShouldBe(new[] { "Plant/a", "Plant/b" });
|
||||
}
|
||||
|
||||
/// <summary>2026-08-15 surgical-deploys (Task 6) — the UNS-reference subset overload materialises only the
|
||||
/// references it is handed (with their Organizes edges), leaving every other reference in the composition
|
||||
/// untouched.</summary>
|
||||
[Fact]
|
||||
public void MaterialiseUnsReferences_with_a_subset_ensures_only_the_subset()
|
||||
{
|
||||
var sink = new RealmRecordingSink();
|
||||
var untouched = new UnsReferenceVariable("r0", "EQ-1", "filling/line1/station1/speed", "speed",
|
||||
"Plant/A/dev/speed", "Float", Writable: true);
|
||||
var added = new UnsReferenceVariable("r1", "EQ-1", "filling/line1/station1/temp", "temp",
|
||||
"Plant/A/dev/temp", "Float", Writable: false);
|
||||
var composition = new AddressSpaceComposition(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
UnsReferenceVariables = new[] { untouched, added },
|
||||
};
|
||||
|
||||
NewApplier(sink).MaterialiseUnsReferences(composition, only: new[] { added });
|
||||
|
||||
sink.Variables.ShouldHaveSingleItem().NodeId.ShouldBe("filling/line1/station1/temp");
|
||||
sink.References.ShouldHaveSingleItem().Target.ShouldBe("Plant/A/dev/temp");
|
||||
}
|
||||
|
||||
/// <summary>A capturing sink that records (nodeId, realm) for every materialise + the Organizes edges.</summary>
|
||||
private sealed class RealmRecordingSink : IOpcUaAddressSpaceSink
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user