Files
scadalink-design/tests/ScadaLink.TemplateEngine.Tests/TemplateResolverTests.cs
Joseph Doherty faef2d0de6 Phase 2 WP-1–13+23: Template Engine CRUD, composition, overrides, locking, collision detection, acyclicity
- WP-23: ITemplateEngineRepository full EF Core implementation
- WP-1: Template CRUD with deletion constraints (instances, children, compositions)
- WP-2–4: Attribute, alarm, script definitions with lock flags and override granularity
- WP-5: Shared script CRUD with syntax validation
- WP-6–7: Composition with recursive nesting and canonical naming
- WP-8–11: Override granularity, locking rules, inheritance/composition scope
- WP-12: Naming collision detection on canonical names (recursive)
- WP-13: Graph acyclicity (inheritance + composition cycles)
Core services: TemplateService, SharedScriptService, TemplateResolver,
LockEnforcer, CollisionDetector, CycleDetector. 358 tests pass.
2026-03-16 20:10:34 -04:00

178 lines
7.5 KiB
C#

using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.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);
}
}