6070cc2b9d
The last two whole-library template reads are gone. Both fed the inheritance-only TemplateInheritanceResolver, which walks one template's parent chain and never follows compositions. - TemplateService.ResyncInheritedMembersAsync (operator repair, CLI + editor) now reads #86's GetTemplateReconcileClosureAsync (derived subtree + the template's chain, tracked). When nothing derives from the template the closure is empty, so it falls back to the new tracked GetTemplateChainAsync - resyncing a leaf reads only its own chain. - ManagementActor.HandleGetResolvedTemplateMembers now reads GetTemplateChainAsync(id, trackChanges: false): the chain alone, untracked, with script bodies (the resolver projects script code, so #66's body-free analysis closure would not be equivalent). The chain walk is factored out of TemplateReconcileClosure into CollectChainAsync (one load per level, stops on parent cycles and dangling parents) and shared by both. Evidence: - TemplateChainReadTests (#86 fixture): for every template plus an unknown id, the closure-backed resync issues exactly the writes, audit row and result of the whole-graph resync with GetAllTemplatesAsync wired to throw; the resolver answers identically from the chain and the whole graph. 17 of these fail against the old service; 13 fail with the leaf chain fallback removed (mutation-checked). - TemplateChainRepositoryTests (SQLite, recorded SQL): a 4-deep chain loads 4 templates / 8 attributes instead of 46 / 92, one split-query set per level, untracked on request and tracked for the resync. - ResolvedTemplateMembersReadTests: the actor answers from the untracked chain read and never calls GetAllTemplatesAsync (fails on the old actor). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
114 lines
4.8 KiB
C#
114 lines
4.8 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #89: <see cref="TemplateEngineRepository.GetTemplateChainAsync"/> reads only a template's
|
|
/// inheritance chain — fully included like <see cref="TemplateEngineRepository.GetAllTemplatesAsync"/>,
|
|
/// tracked or not on request — instead of the whole library. Asserted against real SQL on SQLite
|
|
/// with the reader commands recorded.
|
|
/// </summary>
|
|
public class TemplateChainRepositoryTests : IDisposable
|
|
{
|
|
private const int UnrelatedTemplates = 40;
|
|
private const string Body = "/* script body the resolver projects */";
|
|
|
|
private readonly SqlReaderCommandRecorder _recorder = new();
|
|
private readonly ScadaBridgeDbContext _context;
|
|
private readonly TemplateEngineRepository _repository;
|
|
|
|
/// <summary>Initializes the test class with an in-memory context and repository.</summary>
|
|
public TemplateChainRepositoryTests()
|
|
{
|
|
_context = SqliteTestHelper.CreateInMemoryContext(_recorder);
|
|
_repository = new TemplateEngineRepository(_context, new TemplateGraphWatermark());
|
|
}
|
|
|
|
/// <summary>Releases the resources held by this test class.</summary>
|
|
public void Dispose()
|
|
{
|
|
_context.Database.CloseConnection();
|
|
_context.Dispose();
|
|
}
|
|
|
|
private async Task<Template> AddAsync(string name, int? parentId = null)
|
|
{
|
|
var t = new Template(name) { ParentTemplateId = parentId };
|
|
t.Attributes.Add(new TemplateAttribute($"{name}Attr1"));
|
|
t.Attributes.Add(new TemplateAttribute($"{name}Attr2"));
|
|
t.Scripts.Add(new TemplateScript($"{name}Script", Body));
|
|
_context.Templates.Add(t);
|
|
await _context.SaveChangesAsync();
|
|
return t;
|
|
}
|
|
|
|
private sealed record Seeded(int Root, int Base, int Mid, int Leaf, int Sibling, int Module);
|
|
|
|
private async Task<Seeded> SeedAsync()
|
|
{
|
|
var root = await AddAsync("Root");
|
|
var @base = await AddAsync("Base", root.Id);
|
|
var mid = await AddAsync("Mid", @base.Id);
|
|
var leaf = await AddAsync("Leaf", mid.Id);
|
|
var sibling = await AddAsync("Sibling", mid.Id); // Leaf's sibling: not on Leaf's chain
|
|
var module = await AddAsync("Module");
|
|
_context.Set<TemplateComposition>().Add(
|
|
new TemplateComposition("m") { TemplateId = @base.Id, ComposedTemplateId = module.Id });
|
|
await _context.SaveChangesAsync();
|
|
|
|
for (var i = 0; i < UnrelatedTemplates; i++)
|
|
await AddAsync($"Unrelated{i}");
|
|
|
|
_context.ChangeTracker.Clear();
|
|
_recorder.Reset();
|
|
return new Seeded(root.Id, @base.Id, mid.Id, leaf.Id, sibling.Id, module.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The chain is exactly the template and its ancestors — 4 templates instead of 46 — with script
|
|
/// bodies and every child collection, one full load per level; untracked when asked.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Chain_ReadsOnlyTemplateAndAncestors_Untracked()
|
|
{
|
|
var ids = await SeedAsync();
|
|
|
|
var whole = await _repository.GetAllTemplatesAsync();
|
|
var wholeQueries = _recorder.Commands.Count;
|
|
_context.ChangeTracker.Clear();
|
|
_recorder.Reset();
|
|
|
|
var chain = await _repository.GetTemplateChainAsync(ids.Leaf, trackChanges: false);
|
|
|
|
Assert.Equal(new[] { ids.Leaf, ids.Mid, ids.Base, ids.Root }, chain.Select(t => t.Id));
|
|
Assert.Equal(6 + UnrelatedTemplates, whole.Count);
|
|
Assert.Equal(8, chain.Sum(t => t.Attributes.Count));
|
|
Assert.All(chain.SelectMany(t => t.Scripts), s => Assert.Equal(Body, s.Code));
|
|
Assert.Single(chain.Single(t => t.Id == ids.Base).Compositions);
|
|
Assert.DoesNotContain(chain, t => t.Id == ids.Module || t.Id == ids.Sibling);
|
|
|
|
// One split-query set per level, each bounded by one template.
|
|
Assert.Equal(4 * wholeQueries, _recorder.Commands.Count);
|
|
Assert.Empty(_context.ChangeTracker.Entries());
|
|
}
|
|
|
|
/// <summary>The resync's fallback read is tracked, because the resync writes through it.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Chain_Tracked_WhenAsked_UnknownIdLoadsNothing()
|
|
{
|
|
var ids = await SeedAsync();
|
|
|
|
var chain = await _repository.GetTemplateChainAsync(ids.Mid, trackChanges: true);
|
|
|
|
Assert.Equal(new[] { ids.Mid, ids.Base, ids.Root }, chain.Select(t => t.Id));
|
|
Assert.Equal(3, _context.ChangeTracker.Entries<Template>().Count());
|
|
|
|
Assert.Empty(await _repository.GetTemplateChainAsync(987654, trackChanges: true));
|
|
}
|
|
}
|