using System.Text.Json;
using System.Text.Json.Serialization;
using Moq;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
///
/// Gitea #86: TemplateService.ReconcileDescendantsAsync now reconciles against the
/// template's derived subtree + inheritance chain instead of the whole tracked library. These
/// tests pin that the closure is (a) sufficient — for every template of a graph with a deep
/// subtree, an ancestor chain above the edited template, a composed module, unrelated look-alike
/// trees, a parent cycle and a dangling parent, the reconcile issues exactly the same writes and
/// the resolver answers identically for every descendant — and (b) actually smaller, loading
/// nothing beyond one probe when there are no descendants.
///
public class TemplateReconcileClosureTests
{
internal const int Root = 1, Base = 2, Mid = 3, Leaf1 = 4, Leaf2 = 5, Leaf3 = 6, GreatLeaf = 7, Mod = 8;
internal const int U1 = 9, U2 = 10, CycA = 11, CycB = 12, CycC = 13, Dang = 14, DangKid = 15;
internal static readonly JsonSerializerOptions Json = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = false,
};
/// Builds a fresh, independently mutable copy of the fixture graph.
internal static List BuildGraph()
{
var memberId = 1000;
Template T(int id, string name, int? parent = null)
=> new(name) { Id = id, ParentTemplateId = parent };
void Attr(Template t, string name, string? value, bool inherited = false, bool locked = false) =>
t.Attributes.Add(new TemplateAttribute(name)
{
Id = memberId++, TemplateId = t.Id, Value = value, DataType = DataType.String,
IsInherited = inherited, IsLocked = locked,
});
void Script(Template t, string name, string code, bool inherited = false) =>
t.Scripts.Add(new TemplateScript(name, code)
{
Id = memberId++, TemplateId = t.Id, TriggerType = "Startup", IsInherited = inherited,
});
void Alarm(Template t, string name, int priority, bool inherited = false) =>
t.Alarms.Add(new TemplateAlarm(name)
{
Id = memberId++, TemplateId = t.Id, PriorityLevel = priority,
TriggerType = AlarmTriggerType.ValueMatch, IsInherited = inherited,
});
var root = T(Root, "Root");
Attr(root, "R", "root"); // inherited from ABOVE the edited template
Script(root, "Init", "root-body");
var @base = T(Base, "Base", Root);
Attr(@base, "A", "1");
Attr(@base, "L", "locked", locked: true);
Attr(@base, "New", "added-after-derivation"); // no placeholder anywhere yet
Script(@base, "Calc", "base-body-v2");
Alarm(@base, "Hi", 700);
@base.Compositions.Add(new TemplateComposition("m") { Id = memberId++, TemplateId = Base, ComposedTemplateId = Mod });
var mid = T(Mid, "Mid", Base);
Attr(mid, "A", "mid-override"); // own override, never touched
Attr(mid, "R", "stale-root", inherited: true);
Attr(mid, "L", "locked", inherited: true);
Script(mid, "Calc", "base-body-v1", inherited: true); // stale body
Alarm(mid, "Hi", 500, inherited: true); // stale priority
var leaf1 = T(Leaf1, "Leaf1", Mid);
Attr(leaf1, "A", "1", inherited: true); // stale: Mid overrides A
Attr(leaf1, "Gone", "orphan", inherited: true); // orphan placeholder
var leaf2 = T(Leaf2, "Leaf2", Mid);
Attr(leaf2, "L", "override-attempt"); // locked upstream
var leaf3 = T(Leaf3, "Leaf3", Base);
var greatLeaf = T(GreatLeaf, "GreatLeaf", Leaf1);
Script(greatLeaf, "Calc", "own-body");
var mod = T(Mod, "Mod");
Attr(mod, "ModOnly", "x");
// Unrelated look-alike tree: same member names, must never be loaded.
var u1 = T(U1, "U1");
Attr(u1, "A", "u");
Attr(u1, "New", "u");
var u2 = T(U2, "U2", U1);
Attr(u2, "A", "stale", inherited: true);
// Parent cycle with a hanging child, and a dangling parent edge.
var cycA = T(CycA, "CycA", CycB);
Attr(cycA, "CA", "a");
var cycB = T(CycB, "CycB", CycA);
Attr(cycB, "CB", "b");
var cycC = T(CycC, "CycC", CycA);
var dang = T(Dang, "Dang", 998);
Attr(dang, "D", "d");
var dangKid = T(DangKid, "DangKid", Dang);
return new List
{
root, @base, mid, leaf1, leaf2, leaf3, greatLeaf, mod, u1, u2, cycA, cycB, cycC, dang, dangKid,
};
}
internal static Task> Closure(
IReadOnlyList graph,
int id,
List? calls = null) =>
TemplateReconcileClosure.CollectAsync(
id,
(parentIds, _) =>
{
calls?.Add("children:" + string.Join(",", parentIds.OrderBy(i => i)));
return Task.FromResult>(
graph.Where(t => t.ParentTemplateId is int p && parentIds.Contains(p)).ToList());
},
(ids, _) =>
{
calls?.Add("ids:" + string.Join(",", ids.OrderBy(i => i)));
return Task.FromResult>(graph.Where(t => ids.Contains(t.Id)).ToList());
});
// Independent re-statement of the service's descendant walk over a given template list.
private static List Descendants(int rootId, IReadOnlyList all)
{
var result = new List();
var seen = new HashSet { rootId };
var queue = new Queue(new[] { rootId });
while (queue.Count > 0)
{
var id = queue.Dequeue();
foreach (var child in all.Where(t => t.ParentTemplateId == id))
{
if (seen.Add(child.Id))
{
result.Add(child.Id);
queue.Enqueue(child.Id);
}
}
}
return result;
}
/// Every fixture template id.
/// One row per template.
public static IEnumerable