Files
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

115 lines
5.3 KiB
C#

using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.TemplateEngine.Tests;
public class CollisionDetectorTests
{
// ========================================================================
// WP-12: Naming Collision Detection
// ========================================================================
[Fact]
public void DetectCollisions_NoCollisions_ReturnsEmpty()
{
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 });
var all = new List<Template> { template };
var collisions = CollisionDetector.DetectCollisions(template, all);
Assert.Empty(collisions);
}
[Fact]
public void DetectCollisions_DifferentModulesNoPrefixCollision_ReturnsEmpty()
{
// Two composed modules with same member name but different instance names
var moduleA = new Template("ModuleA") { Id = 2 };
moduleA.Attributes.Add(new TemplateAttribute("Value") { Id = 10, TemplateId = 2, DataType = DataType.Float });
var moduleB = new Template("ModuleB") { Id = 3 };
moduleB.Attributes.Add(new TemplateAttribute("Value") { Id = 11, TemplateId = 3, DataType = DataType.Float });
var template = new Template("Pump") { Id = 1 };
template.Compositions.Add(new TemplateComposition("modA") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
template.Compositions.Add(new TemplateComposition("modB") { Id = 2, TemplateId = 1, ComposedTemplateId = 3 });
var all = new List<Template> { template, moduleA, moduleB };
var collisions = CollisionDetector.DetectCollisions(template, all);
// modA.Value and modB.Value are different canonical names => no collision
Assert.Empty(collisions);
}
[Fact]
public void DetectCollisions_DirectAndComposedNameCollision_ReturnsCollision()
{
// Template has a direct attribute "Speed"
// Composed module also has an attribute that would produce canonical name "Speed"
// This happens when a module's member has no prefix collision — actually
// composed members always have a prefix so this shouldn't collide.
// But a direct member "modA.Value" would collide with modA.Value from composition.
// Let's test: direct attr named "modA.Value" and composition modA with member "Value"
var module = new Template("Module") { Id = 2 };
module.Attributes.Add(new TemplateAttribute("Value") { Id = 10, TemplateId = 2, DataType = DataType.Float });
var template = new Template("Pump") { Id = 1 };
template.Attributes.Add(new TemplateAttribute("modA.Value") { Id = 1, TemplateId = 1, DataType = DataType.Float });
template.Compositions.Add(new TemplateComposition("modA") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
var all = new List<Template> { template, module };
var collisions = CollisionDetector.DetectCollisions(template, all);
Assert.NotEmpty(collisions);
Assert.Contains(collisions, c => c.Contains("modA.Value"));
}
[Fact]
public void DetectCollisions_NestedComposition_ReturnsCorrectCanonicalNames()
{
// Inner module
var inner = new Template("Inner") { Id = 3 };
inner.Attributes.Add(new TemplateAttribute("Pressure") { Id = 30, TemplateId = 3, DataType = DataType.Float });
// Outer module composes inner
var outer = new Template("Outer") { Id = 2 };
outer.Compositions.Add(new TemplateComposition("inner1") { Id = 1, TemplateId = 2, ComposedTemplateId = 3 });
// Main template composes outer
var main = new Template("Main") { Id = 1 };
main.Compositions.Add(new TemplateComposition("outer1") { Id = 2, TemplateId = 1, ComposedTemplateId = 2 });
var all = new List<Template> { main, outer, inner };
var collisions = CollisionDetector.DetectCollisions(main, all);
// No collision, just checking it doesn't crash on nested compositions
Assert.Empty(collisions);
}
[Fact]
public void DetectCollisions_InheritedMembersCollideWithComposed_ReturnsCollision()
{
// Parent has a direct attribute "modA.Temp"
var parent = new Template("Base") { Id = 1 };
parent.Attributes.Add(new TemplateAttribute("modA.Temp") { Id = 10, TemplateId = 1, DataType = DataType.Float });
// Module has attribute "Temp"
var module = new Template("Module") { Id = 3 };
module.Attributes.Add(new TemplateAttribute("Temp") { Id = 30, TemplateId = 3, DataType = DataType.Float });
// Child inherits from parent and composes module as "modA"
var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
child.Compositions.Add(new TemplateComposition("modA") { Id = 1, TemplateId = 2, ComposedTemplateId = 3 });
var all = new List<Template> { parent, child, module };
var collisions = CollisionDetector.DetectCollisions(child, all);
// "modA.Temp" from parent and "modA.Temp" from composed module
Assert.NotEmpty(collisions);
Assert.Contains(collisions, c => c.Contains("modA.Temp"));
}
}