feat(opcua): diff Equipment VirtualTags in Phase7Plan + rebuild trigger
This commit is contained in:
@@ -70,19 +70,23 @@ public sealed class Phase7Applier
|
|||||||
|
|
||||||
var changedCount =
|
var changedCount =
|
||||||
plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count +
|
plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count +
|
||||||
plan.ChangedGalaxyTags.Count + plan.ChangedEquipmentTags.Count;
|
plan.ChangedGalaxyTags.Count + plan.ChangedEquipmentTags.Count +
|
||||||
|
plan.ChangedEquipmentVirtualTags.Count;
|
||||||
var addedCount =
|
var addedCount =
|
||||||
plan.AddedEquipment.Count + plan.AddedDrivers.Count + plan.AddedAlarms.Count +
|
plan.AddedEquipment.Count + plan.AddedDrivers.Count + plan.AddedAlarms.Count +
|
||||||
plan.AddedGalaxyTags.Count + plan.AddedEquipmentTags.Count;
|
plan.AddedGalaxyTags.Count + plan.AddedEquipmentTags.Count +
|
||||||
|
plan.AddedEquipmentVirtualTags.Count;
|
||||||
|
|
||||||
// Any add/remove of Equipment, ScriptedAlarm, Galaxy tag, or Equipment tag topology requires
|
// Any add/remove of Equipment, ScriptedAlarm, Galaxy tag, Equipment tag, or Equipment
|
||||||
// a real address-space rebuild. Driver-instance changes don't touch the address-space
|
// VirtualTag topology requires a real address-space rebuild. Driver-instance changes don't
|
||||||
// topology directly — they go through DriverHostActor's spawn-plan in Runtime.
|
// touch the address-space topology directly — they go through DriverHostActor's spawn-plan
|
||||||
|
// in Runtime.
|
||||||
var needsRebuild =
|
var needsRebuild =
|
||||||
plan.AddedEquipment.Count > 0 || plan.RemovedEquipment.Count > 0 ||
|
plan.AddedEquipment.Count > 0 || plan.RemovedEquipment.Count > 0 ||
|
||||||
plan.AddedAlarms.Count > 0 || plan.RemovedAlarms.Count > 0 ||
|
plan.AddedAlarms.Count > 0 || plan.RemovedAlarms.Count > 0 ||
|
||||||
plan.AddedGalaxyTags.Count > 0 || plan.RemovedGalaxyTags.Count > 0 ||
|
plan.AddedGalaxyTags.Count > 0 || plan.RemovedGalaxyTags.Count > 0 ||
|
||||||
plan.AddedEquipmentTags.Count > 0 || plan.RemovedEquipmentTags.Count > 0;
|
plan.AddedEquipmentTags.Count > 0 || plan.RemovedEquipmentTags.Count > 0 ||
|
||||||
|
plan.AddedEquipmentVirtualTags.Count > 0 || plan.RemovedEquipmentVirtualTags.Count > 0;
|
||||||
|
|
||||||
if (needsRebuild)
|
if (needsRebuild)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,19 +40,36 @@ public sealed record Phase7Plan(
|
|||||||
/// <inheritdoc cref="AddedEquipmentTags"/>
|
/// <inheritdoc cref="AddedEquipmentTags"/>
|
||||||
public IReadOnlyList<EquipmentTagDelta> ChangedEquipmentTags { get; init; } = Array.Empty<EquipmentTagDelta>();
|
public IReadOnlyList<EquipmentTagDelta> ChangedEquipmentTags { get; init; } = Array.Empty<EquipmentTagDelta>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equipment-namespace VirtualTag diff sets, keyed by <see cref="EquipmentVirtualTagPlan.VirtualTagId"/>.
|
||||||
|
/// The value-side analogue of <see cref="AddedEquipmentTags"/>: a VirtualTag carries an
|
||||||
|
/// <c>Expression</c> evaluated over <c>DependencyRefs</c>, so a deploy that changes ONLY
|
||||||
|
/// VirtualTags (e.g. a new computed signal or an edited formula) must still produce a
|
||||||
|
/// non-empty plan and drive a rebuild — without these the diff was blind to VirtualTags and
|
||||||
|
/// such a deploy silently no-op'd. Added as init-only members (defaulting empty) for the same
|
||||||
|
/// compile-compatibility reason as <see cref="AddedEquipmentTags"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<EquipmentVirtualTagPlan> AddedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagPlan>();
|
||||||
|
/// <inheritdoc cref="AddedEquipmentVirtualTags"/>
|
||||||
|
public IReadOnlyList<EquipmentVirtualTagPlan> RemovedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagPlan>();
|
||||||
|
/// <inheritdoc cref="AddedEquipmentVirtualTags"/>
|
||||||
|
public IReadOnlyList<EquipmentVirtualTagDelta> ChangedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagDelta>();
|
||||||
|
|
||||||
/// <summary>Gets a value indicating whether the composition plan contains no changes.</summary>
|
/// <summary>Gets a value indicating whether the composition plan contains no changes.</summary>
|
||||||
public bool IsEmpty =>
|
public bool IsEmpty =>
|
||||||
AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 &&
|
AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 &&
|
||||||
AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 &&
|
AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 &&
|
||||||
AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 &&
|
AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 &&
|
||||||
AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0 &&
|
AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0 &&
|
||||||
AddedEquipmentTags.Count == 0 && RemovedEquipmentTags.Count == 0 && ChangedEquipmentTags.Count == 0;
|
AddedEquipmentTags.Count == 0 && RemovedEquipmentTags.Count == 0 && ChangedEquipmentTags.Count == 0 &&
|
||||||
|
AddedEquipmentVirtualTags.Count == 0 && RemovedEquipmentVirtualTags.Count == 0 && ChangedEquipmentVirtualTags.Count == 0;
|
||||||
|
|
||||||
public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current);
|
public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current);
|
||||||
public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current);
|
public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current);
|
||||||
public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current);
|
public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current);
|
||||||
public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current);
|
public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current);
|
||||||
public sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current);
|
public sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current);
|
||||||
|
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Phase7Planner
|
public static class Phase7Planner
|
||||||
@@ -95,6 +112,15 @@ public static class Phase7Planner
|
|||||||
t => t.TagId,
|
t => t.TagId,
|
||||||
(a, b) => new Phase7Plan.EquipmentTagDelta(a, b));
|
(a, b) => new Phase7Plan.EquipmentTagDelta(a, b));
|
||||||
|
|
||||||
|
// VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. Element equality on
|
||||||
|
// EquipmentVirtualTagPlan compares its scalar fields (Expression/DataType/Name/FolderPath/…)
|
||||||
|
// by value; DependencyRefs (an IReadOnlyList<string>) compares by reference, so a fresh list
|
||||||
|
// instance is conservatively treated as changed — fine for a rebuild trigger.
|
||||||
|
var (addedVTags, removedVTags, changedVTags) = DiffById(
|
||||||
|
previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
|
||||||
|
t => t.VirtualTagId,
|
||||||
|
(a, b) => new Phase7Plan.EquipmentVirtualTagDelta(a, b));
|
||||||
|
|
||||||
return new Phase7Plan(
|
return new Phase7Plan(
|
||||||
addedEq, removedEq, changedEq,
|
addedEq, removedEq, changedEq,
|
||||||
addedDrv, removedDrv, changedDrv,
|
addedDrv, removedDrv, changedDrv,
|
||||||
@@ -104,6 +130,9 @@ public static class Phase7Planner
|
|||||||
AddedEquipmentTags = addedEqTags,
|
AddedEquipmentTags = addedEqTags,
|
||||||
RemovedEquipmentTags = removedEqTags,
|
RemovedEquipmentTags = removedEqTags,
|
||||||
ChangedEquipmentTags = changedEqTags,
|
ChangedEquipmentTags = changedEqTags,
|
||||||
|
AddedEquipmentVirtualTags = addedVTags,
|
||||||
|
RemovedEquipmentVirtualTags = removedVTags,
|
||||||
|
ChangedEquipmentVirtualTags = changedVTags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,31 @@ public sealed class Phase7ApplierTests
|
|||||||
sink.RebuildCalls.ShouldBe(1);
|
sink.RebuildCalls.ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that added Equipment VirtualTags in an otherwise-empty plan trigger an
|
||||||
|
/// address-space rebuild (parity with the equipment-tag path — the planner now diffs VirtualTags,
|
||||||
|
/// so a VirtualTag-only deploy is no longer a silent no-op).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Added_equipment_virtual_tags_trigger_rebuild()
|
||||||
|
{
|
||||||
|
var sink = new RecordingSink();
|
||||||
|
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||||
|
|
||||||
|
var plan = EmptyPlan with
|
||||||
|
{
|
||||||
|
AddedEquipmentVirtualTags = new[]
|
||||||
|
{
|
||||||
|
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float",
|
||||||
|
Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var outcome = applier.Apply(plan);
|
||||||
|
|
||||||
|
outcome.RebuildCalled.ShouldBeTrue();
|
||||||
|
outcome.AddedNodes.ShouldBe(1);
|
||||||
|
sink.RebuildCalls.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that added Galaxy tags in an otherwise-empty plan trigger an address-space rebuild.</summary>
|
/// <summary>Verifies that added Galaxy tags in an otherwise-empty plan trigger an address-space rebuild.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Added_galaxy_tags_trigger_rebuild()
|
public void Added_galaxy_tags_trigger_rebuild()
|
||||||
|
|||||||
@@ -55,6 +55,90 @@ public sealed class Phase7PlannerTests
|
|||||||
plan.ChangedEquipmentTags.ShouldBeEmpty();
|
plan.ChangedEquipmentTags.ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a VirtualTag-only delta (no equipment/driver/alarm/galaxy/tag change)
|
||||||
|
/// yields a NON-empty plan with the new VirtualTag in AddedEquipmentVirtualTags, so a deploy that
|
||||||
|
/// only adds VirtualTags is no longer a silent no-op at the IsEmpty gate.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Equipment_virtual_tag_only_change_yields_non_empty_plan_with_added_tag()
|
||||||
|
{
|
||||||
|
var prev = new Phase7CompositionResult(
|
||||||
|
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
|
||||||
|
var next = new Phase7CompositionResult(
|
||||||
|
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||||
|
{
|
||||||
|
EquipmentVirtualTags = new[]
|
||||||
|
{
|
||||||
|
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float",
|
||||||
|
Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var plan = Phase7Planner.Compute(prev, next);
|
||||||
|
|
||||||
|
plan.IsEmpty.ShouldBeFalse();
|
||||||
|
plan.AddedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1");
|
||||||
|
plan.RemovedEquipmentVirtualTags.ShouldBeEmpty();
|
||||||
|
plan.ChangedEquipmentVirtualTags.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a disappeared VirtualTag routes to RemovedEquipmentVirtualTags.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Disappeared_virtual_tag_goes_to_RemovedEquipmentVirtualTags()
|
||||||
|
{
|
||||||
|
var prev = new Phase7CompositionResult(
|
||||||
|
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||||
|
{
|
||||||
|
EquipmentVirtualTags = new[]
|
||||||
|
{
|
||||||
|
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float",
|
||||||
|
Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var next = new Phase7CompositionResult(
|
||||||
|
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
|
||||||
|
|
||||||
|
var plan = Phase7Planner.Compute(prev, next);
|
||||||
|
|
||||||
|
plan.IsEmpty.ShouldBeFalse();
|
||||||
|
plan.RemovedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1");
|
||||||
|
plan.AddedEquipmentVirtualTags.ShouldBeEmpty();
|
||||||
|
plan.ChangedEquipmentVirtualTags.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a VirtualTag with the same id but a different Expression routes to
|
||||||
|
/// ChangedEquipmentVirtualTags (the diff identity is VirtualTagId; any field difference,
|
||||||
|
/// including the evaluated Expression, moves it from stable to changed).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Same_id_with_different_expression_routes_to_ChangedEquipmentVirtualTags()
|
||||||
|
{
|
||||||
|
var prev = new Phase7CompositionResult(
|
||||||
|
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||||
|
{
|
||||||
|
EquipmentVirtualTags = new[]
|
||||||
|
{
|
||||||
|
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float",
|
||||||
|
Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var next = new Phase7CompositionResult(
|
||||||
|
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||||
|
{
|
||||||
|
EquipmentVirtualTags = new[]
|
||||||
|
{
|
||||||
|
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float",
|
||||||
|
Expression: "a - b", DependencyRefs: new[] { "a", "b" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var plan = Phase7Planner.Compute(prev, next);
|
||||||
|
|
||||||
|
plan.IsEmpty.ShouldBeFalse();
|
||||||
|
plan.ChangedEquipmentVirtualTags.Single().Previous.Expression.ShouldBe("a + b");
|
||||||
|
plan.ChangedEquipmentVirtualTags.Single().Current.Expression.ShouldBe("a - b");
|
||||||
|
plan.AddedEquipmentVirtualTags.ShouldBeEmpty();
|
||||||
|
plan.RemovedEquipmentVirtualTags.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that new equipment goes to the AddedEquipment list.</summary>
|
/// <summary>Verifies that new equipment goes to the AddedEquipment list.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void New_equipment_goes_to_AddedEquipment()
|
public void New_equipment_goes_to_AddedEquipment()
|
||||||
|
|||||||
Reference in New Issue
Block a user