- 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.
163 lines
5.9 KiB
C#
163 lines
5.9 KiB
C#
using ScadaLink.Commons.Entities.Templates;
|
|
|
|
namespace ScadaLink.TemplateEngine;
|
|
|
|
/// <summary>
|
|
/// Detects cycles in template inheritance and composition graphs.
|
|
/// Covers: self-inheritance, circular inheritance chains, self-composition,
|
|
/// circular composition chains, and cross-graph (inheritance + composition) cycles.
|
|
/// </summary>
|
|
public static class CycleDetector
|
|
{
|
|
/// <summary>
|
|
/// Checks whether setting <paramref name="parentId"/> as the parent of template
|
|
/// <paramref name="templateId"/> would introduce an inheritance cycle.
|
|
/// </summary>
|
|
/// <returns>A description of the cycle if one would be created, or null if safe.</returns>
|
|
public static string? DetectInheritanceCycle(
|
|
int templateId,
|
|
int parentId,
|
|
IReadOnlyList<Template> allTemplates)
|
|
{
|
|
if (templateId == parentId)
|
|
{
|
|
var selfName = allTemplates.FirstOrDefault(t => t.Id == templateId)?.Name ?? templateId.ToString();
|
|
return $"Template '{selfName}' cannot inherit from itself.";
|
|
}
|
|
|
|
// Walk the inheritance chain from the proposed parent upward.
|
|
// If we arrive back at templateId, there is a cycle.
|
|
var lookup = allTemplates.ToDictionary(t => t.Id);
|
|
var visited = new HashSet<int> { templateId };
|
|
var chain = new List<string>();
|
|
|
|
var templateName = lookup.TryGetValue(templateId, out var tmpl) ? tmpl.Name : templateId.ToString();
|
|
chain.Add(templateName);
|
|
|
|
var currentId = parentId;
|
|
while (currentId != 0)
|
|
{
|
|
if (!lookup.TryGetValue(currentId, out var current))
|
|
break;
|
|
|
|
chain.Add(current.Name);
|
|
|
|
if (visited.Contains(currentId))
|
|
{
|
|
return $"Inheritance cycle detected: {string.Join(" -> ", chain)}.";
|
|
}
|
|
|
|
visited.Add(currentId);
|
|
currentId = current.ParentTemplateId ?? 0;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether adding a composition of <paramref name="composedTemplateId"/> into
|
|
/// <paramref name="templateId"/> would introduce a composition cycle.
|
|
/// </summary>
|
|
/// <returns>A description of the cycle if one would be created, or null if safe.</returns>
|
|
public static string? DetectCompositionCycle(
|
|
int templateId,
|
|
int composedTemplateId,
|
|
IReadOnlyList<Template> allTemplates)
|
|
{
|
|
if (templateId == composedTemplateId)
|
|
{
|
|
var selfName = allTemplates.FirstOrDefault(t => t.Id == templateId)?.Name ?? templateId.ToString();
|
|
return $"Template '{selfName}' cannot compose itself.";
|
|
}
|
|
|
|
var lookup = allTemplates.ToDictionary(t => t.Id);
|
|
|
|
// BFS/DFS from composedTemplateId through all its compositions.
|
|
// If we reach templateId, that's a cycle.
|
|
var visited = new HashSet<int>();
|
|
var queue = new Queue<int>();
|
|
queue.Enqueue(composedTemplateId);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var currentId = queue.Dequeue();
|
|
if (currentId == templateId)
|
|
{
|
|
var tmplName = lookup.TryGetValue(templateId, out var t1) ? t1.Name : templateId.ToString();
|
|
var composedName = lookup.TryGetValue(composedTemplateId, out var t2) ? t2.Name : composedTemplateId.ToString();
|
|
return $"Composition cycle detected: '{tmplName}' -> '{composedName}' -> ... -> '{tmplName}'.";
|
|
}
|
|
|
|
if (!visited.Add(currentId))
|
|
continue;
|
|
|
|
if (!lookup.TryGetValue(currentId, out var current))
|
|
continue;
|
|
|
|
foreach (var comp in current.Compositions)
|
|
{
|
|
queue.Enqueue(comp.ComposedTemplateId);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects cross-graph cycles that span both inheritance and composition edges.
|
|
/// A cross-graph cycle exists when following any combination of inheritance (parent)
|
|
/// and composition edges from a template leads back to itself.
|
|
/// </summary>
|
|
/// <returns>A description of the cycle if found, or null if safe.</returns>
|
|
public static string? DetectCrossGraphCycle(
|
|
int templateId,
|
|
int? proposedParentId,
|
|
int? proposedComposedTemplateId,
|
|
IReadOnlyList<Template> allTemplates)
|
|
{
|
|
var lookup = allTemplates.ToDictionary(t => t.Id);
|
|
|
|
// Build adjacency: for each template, collect all reachable templates
|
|
// via inheritance (parent) and composition edges.
|
|
// We temporarily add the proposed edge and check for reachability back to templateId.
|
|
|
|
var visited = new HashSet<int>();
|
|
var queue = new Queue<int>();
|
|
|
|
// Seed with proposed targets
|
|
if (proposedParentId.HasValue && proposedParentId.Value != 0)
|
|
queue.Enqueue(proposedParentId.Value);
|
|
|
|
if (proposedComposedTemplateId.HasValue && proposedComposedTemplateId.Value != 0)
|
|
queue.Enqueue(proposedComposedTemplateId.Value);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var currentId = queue.Dequeue();
|
|
if (currentId == templateId)
|
|
{
|
|
var tmplName = lookup.TryGetValue(templateId, out var t) ? t.Name : templateId.ToString();
|
|
return $"Cross-graph cycle detected involving template '{tmplName}'.";
|
|
}
|
|
|
|
if (!visited.Add(currentId))
|
|
continue;
|
|
|
|
if (!lookup.TryGetValue(currentId, out var current))
|
|
continue;
|
|
|
|
// Follow inheritance edge
|
|
if (current.ParentTemplateId.HasValue && current.ParentTemplateId.Value != 0)
|
|
queue.Enqueue(current.ParentTemplateId.Value);
|
|
|
|
// Follow composition edges
|
|
foreach (var comp in current.Compositions)
|
|
{
|
|
queue.Enqueue(comp.ComposedTemplateId);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|