using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
///
/// Gitea #66: member add/update validation now runs the analysis walkers over a template's
/// inheritance chain + composition closure instead of the whole library. These tests pin that
/// the closure is (a) sufficient — every walker answer is identical to the whole-graph answer,
/// for every template in a graph with chains, nested and diamond compositions, a derived
/// module, cycles, dangling edges and unrelated look-alikes — and (b) actually smaller.
///
public class TemplateAnalysisClosureTests
{
// Ids of the fixture graph.
private const int Grand = 1, Parent = 2, Child = 3, ModA = 4, ModB = 5, ModBBase = 6, Inner = 7;
private const int CycX = 8, CycY = 9, Dangling = 10;
private const int Twin = 18, TwinParent = 19, TwinGrand = 20;
private static readonly List Graph = BuildGraph();
private static List BuildGraph()
{
var memberId = 1000;
Template T(int id, string name, int? parent = null, params string[] attrs)
{
var t = new Template(name) { Id = id, ParentTemplateId = parent };
foreach (var a in attrs)
t.Attributes.Add(new TemplateAttribute(a) { Id = memberId++, TemplateId = id, IsLocked = a.StartsWith('L') });
return t;
}
void Compose(Template owner, Template module, string slot) =>
owner.Compositions.Add(new TemplateComposition(slot) { Id = memberId++, TemplateId = owner.Id, ComposedTemplateId = module.Id });
var grand = T(Grand, "Grand", null, "LSpeed", "Shared");
var parent = T(Parent, "Parent", Grand, "Flow");
var child = T(Child, "Child", Parent, "Shared", "m1.Level"); // overrides + a would-be collision
var modA = T(ModA, "ModA", null, "Level", "Temp");
var modB = T(ModB, "ModB", ModBBase, "Base1"); // derived module (placeholder row)
var modBBase = T(ModBBase, "ModBBase", null, "Base1", "OnlyInBase");
var inner = T(Inner, "Inner", null, "Deep");
modB.Alarms.Add(new TemplateAlarm("HiHi") { Id = memberId++, TemplateId = ModB });
modA.Scripts.Add(new TemplateScript("Calc", string.Empty) { Id = memberId++, TemplateId = ModA, IsLocked = true });
Compose(parent, modA, "m1");
Compose(child, modB, "m2");
Compose(child, modA, "m1b"); // diamond: ModA reached by two paths
Compose(modA, inner, "inner");
var cycX = T(CycX, "CycX", null, "X");
var cycY = T(CycY, "CycY", null, "Y");
Compose(cycX, cycY, "y");
Compose(cycY, cycX, "x"); // composition cycle
var dangling = T(Dangling, "Dangling", 998, "D");
dangling.Compositions.Add(new TemplateComposition("ghost") { Id = memberId++, TemplateId = Dangling, ComposedTemplateId = 999 });
// A template reached first as a module and then as a chain member: its parent must still load.
var twinGrand = T(TwinGrand, "TwinGrand", null, "G");
var twinParent = T(TwinParent, "TwinParent", TwinGrand, "P");
var twin = T(Twin, "Twin", TwinParent, "S");
Compose(twin, twinParent, "t");
var all = new List { grand, parent, child, modA, modB, modBBase, inner, cycX, cycY, dangling, twinGrand, twinParent, twin };
// Unrelated library content with colliding member names — must never be loaded.
for (var i = 0; i < 5; i++)
all.Add(T(11 + i, $"Unrelated{i}", i == 0 ? null : 11, "Shared", "Level", "m1.Level"));
return all;
}
private static Task> InMemoryClosure(int id, List>? batches = null) =>
TemplateAnalysisClosure.CollectAsync(id, (ids, _) =>
{
batches?.Add(ids);
return Task.FromResult>(Graph.Where(t => ids.Contains(t.Id)).ToList());
});
public static IEnumerable