230 lines
8.6 KiB
C#
230 lines
8.6 KiB
C#
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);
|
|
}
|
|
|
|
// ========================================================================
|
|
// TemplateEngine-013: robustness against duplicate Ids and Id 0
|
|
// ========================================================================
|
|
|
|
[Fact]
|
|
public void DetectInheritanceCycle_DuplicateIdsInList_DoesNotThrow()
|
|
{
|
|
// Two not-yet-saved templates both carry Id == 0. ToDictionary(t => t.Id)
|
|
// would throw ArgumentException; the detector must tolerate it.
|
|
var templateA = new Template("A") { Id = 0 };
|
|
var templateB = new Template("B") { Id = 0 };
|
|
var saved = new Template("Saved") { Id = 1 };
|
|
var all = new List<Template> { templateA, templateB, saved };
|
|
|
|
var ex = Record.Exception(() => CycleDetector.DetectInheritanceCycle(1, 0, all));
|
|
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectCompositionCycle_DuplicateIdsInList_DoesNotThrow()
|
|
{
|
|
var templateA = new Template("A") { Id = 0 };
|
|
var templateB = new Template("B") { Id = 0 };
|
|
var all = new List<Template> { templateA, templateB };
|
|
|
|
var ex = Record.Exception(() => CycleDetector.DetectCompositionCycle(1, 2, all));
|
|
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectCrossGraphCycle_DuplicateIdsInList_DoesNotThrow()
|
|
{
|
|
var templateA = new Template("A") { Id = 0 };
|
|
var templateB = new Template("B") { Id = 0 };
|
|
var all = new List<Template> { templateA, templateB };
|
|
|
|
var ex = Record.Exception(() => CycleDetector.DetectCrossGraphCycle(5, 1, 2, all));
|
|
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectInheritanceCycle_RealIdZero_StillDetectsCycle()
|
|
{
|
|
// A template legitimately stored with Id 0 (in-memory / test scenario):
|
|
// a self-inheritance attempt must still be detected, not skipped as
|
|
// "no parent" by a 0-as-sentinel overload.
|
|
var template = new Template("Zero") { Id = 0 };
|
|
var all = new List<Template> { template };
|
|
|
|
var result = CycleDetector.DetectInheritanceCycle(0, 0, all);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Contains("itself", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectInheritanceCycle_ParentChainThroughIdZero_DetectsCycle()
|
|
{
|
|
// Child(1) -> parent Zero(0) -> parent Child(1): a cycle running through
|
|
// a template whose real Id is 0 must be detected, not silently skipped.
|
|
var zero = new Template("Zero") { Id = 0, ParentTemplateId = 1 };
|
|
var child = new Template("Child") { Id = 1, ParentTemplateId = null };
|
|
var all = new List<Template> { zero, child };
|
|
|
|
var result = CycleDetector.DetectInheritanceCycle(1, 0, all);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|