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.
This commit is contained in:
156
tests/ScadaLink.TemplateEngine.Tests/CycleDetectorTests.cs
Normal file
156
tests/ScadaLink.TemplateEngine.Tests/CycleDetectorTests.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using ScadaLink.Commons.Entities.Templates;
|
||||
|
||||
namespace ScadaLink.TemplateEngine.Tests;
|
||||
|
||||
public class CycleDetectorTests
|
||||
{
|
||||
// ========================================================================
|
||||
// WP-13: Inheritance cycle detection
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_SelfInheritance_ReturnsCycle()
|
||||
{
|
||||
var template = new Template("A") { Id = 1 };
|
||||
var all = new List<Template> { template };
|
||||
|
||||
var result = CycleDetector.DetectInheritanceCycle(1, 1, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("itself", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_DirectCycle_ReturnsCycle()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1, ParentTemplateId = null };
|
||||
var templateB = new Template("B") { Id = 2, ParentTemplateId = 1 };
|
||||
var all = new List<Template> { templateA, templateB };
|
||||
|
||||
// A tries to inherit from B (B already inherits from A)
|
||||
var result = CycleDetector.DetectInheritanceCycle(1, 2, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_ThreeNodeCycle_ReturnsCycle()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1, ParentTemplateId = null };
|
||||
var templateB = new Template("B") { Id = 2, ParentTemplateId = 1 };
|
||||
var templateC = new Template("C") { Id = 3, ParentTemplateId = 2 };
|
||||
var all = new List<Template> { templateA, templateB, templateC };
|
||||
|
||||
// A tries to inherit from C (C -> B -> A creates a cycle)
|
||||
var result = CycleDetector.DetectInheritanceCycle(1, 3, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_NoCycle_ReturnsNull()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1, ParentTemplateId = null };
|
||||
var templateB = new Template("B") { Id = 2, ParentTemplateId = null };
|
||||
var all = new List<Template> { templateA, templateB };
|
||||
|
||||
var result = CycleDetector.DetectInheritanceCycle(2, 1, all);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-13: Composition cycle detection
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_SelfComposition_ReturnsCycle()
|
||||
{
|
||||
var template = new Template("A") { Id = 1 };
|
||||
var all = new List<Template> { template };
|
||||
|
||||
var result = CycleDetector.DetectCompositionCycle(1, 1, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("compose itself", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_DirectCycle_ReturnsCycle()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1 };
|
||||
templateA.Compositions.Add(new TemplateComposition("b1") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
|
||||
var templateB = new Template("B") { Id = 2 };
|
||||
var all = new List<Template> { templateA, templateB };
|
||||
|
||||
// B tries to compose A (A already composes B)
|
||||
var result = CycleDetector.DetectCompositionCycle(2, 1, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_TransitiveCycle_ReturnsCycle()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1 };
|
||||
templateA.Compositions.Add(new TemplateComposition("b1") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });
|
||||
var templateB = new Template("B") { Id = 2 };
|
||||
templateB.Compositions.Add(new TemplateComposition("c1") { Id = 2, TemplateId = 2, ComposedTemplateId = 3 });
|
||||
var templateC = new Template("C") { Id = 3 };
|
||||
var all = new List<Template> { templateA, templateB, templateC };
|
||||
|
||||
// C tries to compose A => C -> A -> B -> C
|
||||
var result = CycleDetector.DetectCompositionCycle(3, 1, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_NoCycle_ReturnsNull()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1 };
|
||||
var templateB = new Template("B") { Id = 2 };
|
||||
var all = new List<Template> { templateA, templateB };
|
||||
|
||||
var result = CycleDetector.DetectCompositionCycle(1, 2, all);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WP-13: Cross-graph cycle detection (inheritance + composition)
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DetectCrossGraphCycle_InheritanceCompositionCross_ReturnsCycle()
|
||||
{
|
||||
// A inherits from B, B composes C. If C tries to set parent = A, that's a cross-graph cycle.
|
||||
var templateA = new Template("A") { Id = 1, ParentTemplateId = 2 };
|
||||
var templateB = new Template("B") { Id = 2 };
|
||||
templateB.Compositions.Add(new TemplateComposition("c1") { Id = 1, TemplateId = 2, ComposedTemplateId = 3 });
|
||||
var templateC = new Template("C") { Id = 3 };
|
||||
var all = new List<Template> { templateA, templateB, templateC };
|
||||
|
||||
// C tries to add parent = A
|
||||
var result = CycleDetector.DetectCrossGraphCycle(3, 1, null, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("Cross-graph cycle", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectCrossGraphCycle_NoCycle_ReturnsNull()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 1 };
|
||||
var templateB = new Template("B") { Id = 2 };
|
||||
var templateC = new Template("C") { Id = 3 };
|
||||
var all = new List<Template> { templateA, templateB, templateC };
|
||||
|
||||
var result = CycleDetector.DetectCrossGraphCycle(3, 1, 2, all);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user