84a050420f
Bundles did not record that a template was a derived slot template, so the
importer re-created every slot as a BASE template named after the slot.
- Export: TemplateDto gains an additive, null-omitted `Derivation`
(owner path + base path by stable names); a derived template carries only
its own rows (overrides/additions, not inherited placeholders). The closure
pulls in each derived template's base/owner; base orders first.
schemaVersion 1.1 -> 1.2, bundleFormatVersion stays 1.
- Import: base templates go through the ordinary passes; derived templates are
ordered topologically (DerivedTemplatePlan; cycle / duplicate / malformed =
preview Blocker + SemanticValidationException before anything is staged),
their owner and base resolved by path (#100 root rule), and the slot is
created with TemplateService.AddCompositionAsync, synced in place when it
already exists with the same base, or deleted+recomposed when the base
differs; own rows applied as overrides, then ResyncInheritedMembersAsync.
Missing owner/base fails that record (BundleImportDerivedTemplateUnresolved
audit + ImportResult warning), never creating a base in its place.
- Preview/diff: derived rows are keyed by qualified name and compared against
the target slot at the same owner path (own rows + base), so an unchanged
slot is Identical and a same-named base template is never matched.
- Pre-#118 bundles import exactly as before (new fixture
pre-118-derived-plaintext.scadabundle, written by a13aabc7). The field is in
content, pinned by the signed manifest's contentHash.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
75 lines
3.3 KiB
C#
75 lines
3.3 KiB
C#
using ZB.MOM.WW.ScadaBridge.Transport.Import;
|
|
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
|
|
|
|
/// <summary>
|
|
/// ScadaBridge#118 — <see cref="DerivedTemplatePlan"/> orders a bundle's derived templates
|
|
/// after their owner and base whatever the bundle order, and reports cycles and malformed
|
|
/// derivations as errors.
|
|
/// </summary>
|
|
public sealed class DerivedTemplatePlanTests
|
|
{
|
|
private static TemplateDto Base(string name) =>
|
|
new(name, null, null, null, Array.Empty<TemplateAttributeDto>(), Array.Empty<TemplateAlarmDto>(),
|
|
Array.Empty<TemplateScriptDto>(), Array.Empty<TemplateCompositionDto>());
|
|
|
|
private static TemplateDto Derived(string owner, string[] slots, string baseName, params string[] baseSlots) =>
|
|
Base(slots[^1]) with { Derivation = new TemplateDerivationDto(owner, slots, baseName, baseSlots) };
|
|
|
|
/// <summary>Nested slots and cascade slots come after their owner and their (derived) base.</summary>
|
|
[Fact]
|
|
public void Orders_owner_and_base_before_the_derived_template()
|
|
{
|
|
var plan = DerivedTemplatePlan.Build(new[]
|
|
{
|
|
Derived("Station", new[] { "P", "M1" }, "Pump", "M1"), // owner Station.P, base Pump.M1
|
|
Base("Station"),
|
|
Derived("Station", new[] { "P" }, "Pump"),
|
|
Derived("Pump", new[] { "M1" }, "Motor"),
|
|
});
|
|
|
|
Assert.Empty(plan.Errors);
|
|
var order = plan.Ordered.Select(DerivedTemplatePlan.Key).ToList();
|
|
Assert.Equal(3, order.Count);
|
|
Assert.True(order.IndexOf("Station.P") < order.IndexOf("Station.P.M1"));
|
|
Assert.True(order.IndexOf("Pump.M1") < order.IndexOf("Station.P.M1"));
|
|
}
|
|
|
|
/// <summary>A two-node and a one-node cycle are both reported, naming the templates.</summary>
|
|
[Fact]
|
|
public void Cycles_are_errors()
|
|
{
|
|
var two = DerivedTemplatePlan.Build(new[]
|
|
{
|
|
Derived("Pump", new[] { "X" }, "Pump", "Y"),
|
|
Derived("Pump", new[] { "Y" }, "Pump", "X"),
|
|
});
|
|
var error = Assert.Single(two.Errors);
|
|
Assert.Contains("cycle", error, StringComparison.Ordinal);
|
|
Assert.Contains("Pump.X", error, StringComparison.Ordinal);
|
|
Assert.Contains("Pump.Y", error, StringComparison.Ordinal);
|
|
|
|
var self = DerivedTemplatePlan.Build(new[] { Derived("Pump", new[] { "X" }, "Pump", "X") });
|
|
Assert.Contains("cycle", Assert.Single(self.Errors), StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>Malformed derivations and duplicate identities are errors; base templates are ignored.</summary>
|
|
[Fact]
|
|
public void Malformed_and_duplicate_derivations_are_errors()
|
|
{
|
|
var plan = DerivedTemplatePlan.Build(new[]
|
|
{
|
|
Base("Pump"),
|
|
Base("M1") with { Derivation = new TemplateDerivationDto("Pump", Array.Empty<string>(), "Motor", Array.Empty<string>()) },
|
|
Derived("Pump", new[] { "M2" }, "Motor"),
|
|
Derived("Pump", new[] { "M2" }, "Motor"),
|
|
});
|
|
|
|
Assert.Equal(2, plan.Errors.Count);
|
|
Assert.Contains(plan.Errors, e => e.Contains("malformed", StringComparison.Ordinal));
|
|
Assert.Contains(plan.Errors, e => e.Contains("more than once", StringComparison.Ordinal));
|
|
Assert.Equal("Pump", DerivedTemplatePlan.Key(Base("Pump")));
|
|
}
|
|
}
|