c29b99c63b
TemplateService.ReconcileDescendantsAsync - run after every base member add/update/delete, template and native-alarm-source alike - loaded the whole tracked template graph (every template, member and script body) on each call, which after #66 was the dominant cost of those paths. It now calls the new ITemplateEngineRepository.GetTemplateReconcileClosureAsync: the same tracked, fully-included shape as GetAllTemplatesAsync, restricted to the edited template's derived subtree plus its own inheritance chain. The reconcile is inheritance-only (TemplateInheritanceResolver never follows compositions) and every descendant's chain runs through the edited template, so that set is everything it reads. The walk (Commons TemplateReconcileClosure, the descendant-direction twin of #66's TemplateAnalysisClosure) loads one children batch per subtree depth and one batch per ancestor level, terminates on parent cycles and stops at dangling parents. A template with no children - the common case - costs one empty probe and loads nothing. Evidence: - TemplateReconcileClosureTests: for every template of a graph with a deep subtree, an ancestor above the edited template, a composed module, look-alike unrelated trees, a parent cycle and a dangling parent, the closure-backed reconcile issues exactly the writes (add/update/delete, audit, save) of the old whole-graph reconcile, and the resolver answers identically for every descendant. Fails with the old service (whole-graph load wired to throw) and with the ancestor walk removed (mutation-checked). - TemplateReconcileClosureRepositoryTests (SQLite, recorded SQL): 5 templates / 10 attributes instead of 47 / 94, tracked with script bodies; a leaf costs one probe query and tracks nothing. - TemplateServiceTests: attribute add + update propagate two levels down with GetAllTemplatesAsync wired to throw; existing propagation tests now run against the closure via ServeReconcileClosureFromFullGraph. ResyncInheritedMembersAsync (operator action) still reads the whole graph. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
70 lines
3.4 KiB
C#
70 lines
3.4 KiB
C#
using Moq;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #66: member add/update now asks the repository for a template's analysis closure
|
|
/// rather than the whole analysis graph. Test classes keep seeding the whole graph through
|
|
/// <c>GetAllTemplatesForAnalysisAsync</c>; this wires <c>GetTemplateAnalysisClosureAsync</c>
|
|
/// to serve the real closure walk over that seeded graph, so every existing scenario runs the
|
|
/// service against the reduced set — the same set the production repository returns.
|
|
/// </summary>
|
|
internal static class AnalysisClosureMock
|
|
{
|
|
/// <summary>Wires the closure method to filter whatever the full-graph setup returns.</summary>
|
|
/// <param name="repoMock">The repository mock.</param>
|
|
/// <param name="closureRequests">Optional sink recording each requested closure root.</param>
|
|
public static void ServeClosureFromFullGraph(
|
|
this Mock<ITemplateEngineRepository> repoMock,
|
|
List<int>? closureRequests = null)
|
|
{
|
|
repoMock
|
|
.Setup(r => r.GetTemplateAnalysisClosureAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
|
.Returns((int templateId, CancellationToken ct) =>
|
|
{
|
|
closureRequests?.Add(templateId);
|
|
return TemplateAnalysisClosure.CollectAsync(
|
|
templateId,
|
|
async (ids, token) =>
|
|
{
|
|
var all = await repoMock.Object.GetAllTemplatesForAnalysisAsync(token)
|
|
?? Array.Empty<Template>();
|
|
return all.Where(t => ids.Contains(t.Id)).ToList();
|
|
},
|
|
ct);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gitea #86: the descendant reconcile now asks for the reconcile closure (derived subtree +
|
|
/// the template's chain) instead of <c>GetAllTemplatesAsync</c>. Wires it to run the real
|
|
/// closure walk over whatever the full tracked-graph setup returns, so every existing
|
|
/// propagation scenario exercises the service against the reduced set.
|
|
/// </summary>
|
|
/// <param name="repoMock">The repository mock.</param>
|
|
/// <param name="closureRequests">Optional sink recording each requested closure root.</param>
|
|
public static void ServeReconcileClosureFromFullGraph(
|
|
this Mock<ITemplateEngineRepository> repoMock,
|
|
List<int>? closureRequests = null)
|
|
{
|
|
async Task<IReadOnlyList<Template>> All(CancellationToken token) =>
|
|
await repoMock.Object.GetAllTemplatesAsync(token) ?? Array.Empty<Template>();
|
|
|
|
repoMock
|
|
.Setup(r => r.GetTemplateReconcileClosureAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
|
.Returns((int templateId, CancellationToken ct) =>
|
|
{
|
|
closureRequests?.Add(templateId);
|
|
return TemplateReconcileClosure.CollectAsync(
|
|
templateId,
|
|
async (parentIds, token) => (await All(token))
|
|
.Where(t => t.ParentTemplateId is int p && parentIds.Contains(p)).ToList(),
|
|
async (ids, token) => (await All(token)).Where(t => ids.Contains(t.Id)).ToList(),
|
|
ct);
|
|
});
|
|
}
|
|
}
|