7b0b9c7365
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
254 lines
11 KiB
C#
254 lines
11 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
|
|
|
public class TemplateResolverTests
|
|
{
|
|
// ========================================================================
|
|
// WP-7: Path-Qualified Canonical Naming
|
|
// ========================================================================
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_DirectMembers_NoPrefix()
|
|
{
|
|
var template = new Template("Pump") { Id = 1 };
|
|
template.Attributes.Add(new TemplateAttribute("Speed") { Id = 1, TemplateId = 1, DataType = DataType.Float });
|
|
template.Alarms.Add(new TemplateAlarm("HighTemp") { Id = 1, TemplateId = 1, TriggerType = AlarmTriggerType.ValueMatch });
|
|
template.Scripts.Add(new TemplateScript("OnStart", "code") { Id = 1, TemplateId = 1 });
|
|
|
|
var all = new List<Template> { template };
|
|
var members = TemplateResolver.ResolveAllMembers(1, all);
|
|
|
|
Assert.Equal(3, members.Count);
|
|
Assert.Contains(members, m => m.CanonicalName == "Speed" && m.MemberType == "Attribute");
|
|
Assert.Contains(members, m => m.CanonicalName == "HighTemp" && m.MemberType == "Alarm");
|
|
Assert.Contains(members, m => m.CanonicalName == "OnStart" && m.MemberType == "Script");
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_ComposedModule_PrefixedNames()
|
|
{
|
|
var module = new Template("Module") { Id = 2 };
|
|
module.Attributes.Add(new TemplateAttribute("Pressure") { Id = 10, TemplateId = 2, DataType = DataType.Float });
|
|
|
|
var template = new Template("Pump") { Id = 1 };
|
|
template.Compositions.Add(new TemplateComposition("sensor1") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
|
|
|
|
var all = new List<Template> { template, module };
|
|
var members = TemplateResolver.ResolveAllMembers(1, all);
|
|
|
|
Assert.Single(members);
|
|
Assert.Equal("sensor1.Pressure", members[0].CanonicalName);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_NestedComposition_MultiLevelPrefix()
|
|
{
|
|
var inner = new Template("Inner") { Id = 3 };
|
|
inner.Attributes.Add(new TemplateAttribute("Value") { Id = 30, TemplateId = 3, DataType = DataType.Float });
|
|
|
|
var outer = new Template("Outer") { Id = 2 };
|
|
outer.Compositions.Add(new TemplateComposition("innerMod") { Id = 1, TemplateId = 2, ComposedTemplateId = 3 });
|
|
|
|
var main = new Template("Main") { Id = 1 };
|
|
main.Compositions.Add(new TemplateComposition("outerMod") { Id = 2, TemplateId = 1, ComposedTemplateId = 2 });
|
|
|
|
var all = new List<Template> { main, outer, inner };
|
|
var members = TemplateResolver.ResolveAllMembers(1, all);
|
|
|
|
Assert.Single(members);
|
|
Assert.Equal("outerMod.innerMod.Value", members[0].CanonicalName);
|
|
}
|
|
|
|
// ========================================================================
|
|
// WP-10: Inheritance Override Scope
|
|
// ========================================================================
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_InheritedMembers_Included()
|
|
{
|
|
var parent = new Template("Base") { Id = 1 };
|
|
parent.Attributes.Add(new TemplateAttribute("Speed") { Id = 10, TemplateId = 1, DataType = DataType.Float });
|
|
|
|
var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
|
|
child.Attributes.Add(new TemplateAttribute("ExtraAttr") { Id = 20, TemplateId = 2, DataType = DataType.String });
|
|
|
|
var all = new List<Template> { parent, child };
|
|
var members = TemplateResolver.ResolveAllMembers(2, all);
|
|
|
|
Assert.Equal(2, members.Count);
|
|
Assert.Contains(members, m => m.CanonicalName == "Speed");
|
|
Assert.Contains(members, m => m.CanonicalName == "ExtraAttr");
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_ChildOverridesParentMember_UsesChildVersion()
|
|
{
|
|
var parent = new Template("Base") { Id = 1 };
|
|
parent.Attributes.Add(new TemplateAttribute("Speed")
|
|
{
|
|
Id = 10, TemplateId = 1, DataType = DataType.Float, Value = "0"
|
|
});
|
|
|
|
var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
|
|
child.Attributes.Add(new TemplateAttribute("Speed")
|
|
{
|
|
Id = 20, TemplateId = 2, DataType = DataType.Float, Value = "100"
|
|
});
|
|
|
|
var all = new List<Template> { parent, child };
|
|
var members = TemplateResolver.ResolveAllMembers(2, all);
|
|
|
|
// Should have one Speed member, from the child (override)
|
|
var speedMember = Assert.Single(members, m => m.CanonicalName == "Speed");
|
|
Assert.Equal(2, speedMember.SourceTemplateId); // Child's version
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_InheritedComposedModules_Included()
|
|
{
|
|
var module = new Template("Module") { Id = 3 };
|
|
module.Attributes.Add(new TemplateAttribute("Pressure") { Id = 30, TemplateId = 3, DataType = DataType.Float });
|
|
|
|
var parent = new Template("Base") { Id = 1 };
|
|
parent.Compositions.Add(new TemplateComposition("sensor1") { Id = 1, TemplateId = 1, ComposedTemplateId = 3 });
|
|
|
|
var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
|
|
|
|
var all = new List<Template> { parent, child, module };
|
|
var members = TemplateResolver.ResolveAllMembers(2, all);
|
|
|
|
Assert.Contains(members, m => m.CanonicalName == "sensor1.Pressure");
|
|
}
|
|
|
|
// ========================================================================
|
|
// WP-11: Composition Override Scope
|
|
// ========================================================================
|
|
|
|
[Fact]
|
|
public void FindMemberByCanonicalName_ComposedMember_Found()
|
|
{
|
|
var module = new Template("Module") { Id = 2 };
|
|
module.Attributes.Add(new TemplateAttribute("Value") { Id = 10, TemplateId = 2, DataType = DataType.Float, IsLocked = false });
|
|
|
|
var template = new Template("Parent") { Id = 1 };
|
|
template.Compositions.Add(new TemplateComposition("mod1") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
|
|
|
|
var all = new List<Template> { template, module };
|
|
var member = TemplateResolver.FindMemberByCanonicalName("mod1.Value", 1, all);
|
|
|
|
Assert.NotNull(member);
|
|
Assert.Equal("mod1.Value", member.CanonicalName);
|
|
Assert.False(member.IsLocked);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindMemberByCanonicalName_LockedComposedMember_ReturnsLocked()
|
|
{
|
|
var module = new Template("Module") { Id = 2 };
|
|
module.Attributes.Add(new TemplateAttribute("Value") { Id = 10, TemplateId = 2, DataType = DataType.Float, IsLocked = true });
|
|
|
|
var template = new Template("Parent") { Id = 1 };
|
|
template.Compositions.Add(new TemplateComposition("mod1") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
|
|
|
|
var all = new List<Template> { template, module };
|
|
var member = TemplateResolver.FindMemberByCanonicalName("mod1.Value", 1, all);
|
|
|
|
Assert.NotNull(member);
|
|
Assert.True(member.IsLocked);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildInheritanceChain_ThreeLevel_RootFirst()
|
|
{
|
|
var grandparent = new Template("GP") { Id = 1 };
|
|
var parent = new Template("P") { Id = 2, ParentTemplateId = 1 };
|
|
var child = new Template("C") { Id = 3, ParentTemplateId = 2 };
|
|
|
|
var lookup = new Dictionary<int, Template> { [1] = grandparent, [2] = parent, [3] = child };
|
|
var chain = TemplateResolver.BuildInheritanceChain(3, lookup);
|
|
|
|
Assert.Equal(3, chain.Count);
|
|
Assert.Equal("GP", chain[0].Name);
|
|
Assert.Equal("P", chain[1].Name);
|
|
Assert.Equal("C", chain[2].Name);
|
|
}
|
|
|
|
// ── TemplateEngine-019: a real Id of 0 must walk the inheritance chain ──
|
|
|
|
[Fact]
|
|
public void BuildInheritanceChain_RealIdZero_IsTreatedAsParentReferenceNotAsNoParent()
|
|
{
|
|
// TemplateEngine-013 removed the 0-as-no-parent sentinel from
|
|
// CycleDetector; the same fix had not propagated into the resolver,
|
|
// so seeding BuildInheritanceChain with templateId == 0 returned an
|
|
// empty chain even when a template with Id 0 existed (e.g. an
|
|
// import-staging row, or any in-memory test setup). Post-019: only a
|
|
// null ParentTemplateId means "no parent", and an Id of 0 walks the
|
|
// chain like any other node.
|
|
var orphaned = new Template("OrphanedId0") { Id = 0 };
|
|
orphaned.Attributes.Add(new TemplateAttribute("Speed")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 0,
|
|
DataType = DataType.Float,
|
|
});
|
|
|
|
var lookup = new Dictionary<int, Template> { [0] = orphaned };
|
|
var chain = TemplateResolver.BuildInheritanceChain(0, lookup);
|
|
|
|
// Pre-019: while (currentId != 0 && ...) was false on the first
|
|
// iteration, so the chain was empty and the orphaned template's
|
|
// members were silently dropped from every flatten/resolve through
|
|
// it. Post-019: the orphan is the chain.
|
|
Assert.Single(chain);
|
|
Assert.Equal("OrphanedId0", chain[0].Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildInheritanceChain_ParentChainThroughIdZero_DoesNotTruncateChainAtZero()
|
|
{
|
|
// A template whose real parent has Id 0 must include the Id 0 parent
|
|
// in the chain. Pre-019: `current.ParentTemplateId ?? 0` paired with
|
|
// `currentId != 0` exited the loop as soon as the walk reached a real
|
|
// Id of 0 — silently truncating the inheritance contribution from the
|
|
// root template.
|
|
var parent = new Template("ParentWithIdZero") { Id = 0 };
|
|
parent.Attributes.Add(new TemplateAttribute("RootAttr")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 0,
|
|
DataType = DataType.String,
|
|
});
|
|
var child = new Template("Child") { Id = 5, ParentTemplateId = 0 };
|
|
|
|
var lookup = new Dictionary<int, Template> { [0] = parent, [5] = child };
|
|
var chain = TemplateResolver.BuildInheritanceChain(5, lookup);
|
|
|
|
Assert.Equal(2, chain.Count);
|
|
Assert.Equal("ParentWithIdZero", chain[0].Name); // root first
|
|
Assert.Equal("Child", chain[1].Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveAllMembers_TemplateWithRealIdZero_StillResolvesItsMembers()
|
|
{
|
|
// End-to-end: ResolveAllMembers piggybacks on BuildInheritanceChain,
|
|
// so the chain truncation regression also dropped every member of an
|
|
// Id-0 template from the resolved-member set. Lock this in too.
|
|
var orphan = new Template("OrphanedId0") { Id = 0 };
|
|
orphan.Attributes.Add(new TemplateAttribute("Speed")
|
|
{
|
|
Id = 1,
|
|
TemplateId = 0,
|
|
DataType = DataType.Float,
|
|
});
|
|
|
|
var members = TemplateResolver.ResolveAllMembers(0, new List<Template> { orphan });
|
|
|
|
Assert.Single(members);
|
|
Assert.Equal("Speed", members[0].CanonicalName);
|
|
}
|
|
}
|