perf(deploy): flatten-session caching, bulk DeploySiteAsync, paged management queries
This commit is contained in:
@@ -57,6 +57,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(parent);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent });
|
||||
|
||||
var result = await _service.CreateTemplateAsync("Child", null, 1, "admin");
|
||||
|
||||
@@ -78,7 +80,11 @@ public class TemplateServiceTests
|
||||
var result = await _service.CreateTemplateAsync("Child", null, 1, "admin");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
// No whole-graph walk of ANY shape — neither the tracked
|
||||
// GetAllTemplatesAsync nor the no-tracking analysis projection that the
|
||||
// collision/acyclicity checks now use.
|
||||
_repoMock.Verify(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
_repoMock.Verify(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -112,6 +118,8 @@ public class TemplateServiceTests
|
||||
.ReturnsAsync(new List<Instance>());
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
@@ -128,6 +136,8 @@ public class TemplateServiceTests
|
||||
.ReturnsAsync(new List<Instance> { new Instance("Pump1") { Id = 1, TemplateId = 1, SiteId = 1 } });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
@@ -145,6 +155,8 @@ public class TemplateServiceTests
|
||||
.ReturnsAsync(new List<Instance>());
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent, child });
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
@@ -164,6 +176,8 @@ public class TemplateServiceTests
|
||||
.ReturnsAsync(new List<Instance>());
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { moduleTemplate, composingTemplate });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { moduleTemplate, composingTemplate });
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
@@ -191,6 +205,13 @@ public class TemplateServiceTests
|
||||
new Template("Child") { Id = 2, ParentTemplateId = 1 },
|
||||
composer
|
||||
});
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template>
|
||||
{
|
||||
template,
|
||||
new Template("Child") { Id = 2, ParentTemplateId = 1 },
|
||||
composer
|
||||
});
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
@@ -211,6 +232,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var attr = new TemplateAttribute("Temperature") { DataType = DataType.Float, Value = "0.0" };
|
||||
var result = await _service.AddAttributeAsync(1, attr, "admin");
|
||||
@@ -240,6 +263,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(8, It.IsAny<CancellationToken>())).ReturnsAsync(child);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parent, child });
|
||||
|
||||
var attr = new TemplateAttribute("MoveInType") { DataType = DataType.String, Value = "" };
|
||||
var result = await _service.AddAttributeAsync(8, attr, "admin");
|
||||
@@ -360,6 +385,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var attr = new TemplateAttribute("Temperature") { DataType = DataType.Int32, Value = "not-a-number" };
|
||||
var result = await _service.AddAttributeAsync(1, attr, "admin");
|
||||
@@ -376,6 +403,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var attr = new TemplateAttribute("SetPoints")
|
||||
{
|
||||
@@ -447,6 +476,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var alarm = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
@@ -510,6 +541,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var script = new TemplateScript("OnStart", "return true;") { TriggerType = "Startup" };
|
||||
var result = await _service.AddScriptAsync(1, script, "admin");
|
||||
@@ -610,6 +643,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate });
|
||||
|
||||
Template? captured = null;
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
@@ -651,6 +686,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(11, It.IsAny<CancellationToken>())).ReturnsAsync(sensorProbe1);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { pump, sensor, probe, sensorProbe1 });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { pump, sensor, probe, sensorProbe1 });
|
||||
|
||||
var captured = new List<Template>();
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
@@ -705,6 +742,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate, existing });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate, existing });
|
||||
|
||||
var captured = new List<Template>();
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
@@ -871,6 +910,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateAttribute("SetPoint") { Value = "99", DataType = DataType.Float, IsInherited = false };
|
||||
var result = await _service.UpdateAttributeAsync(100, proposed, "admin");
|
||||
@@ -892,6 +933,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateScript("Sample", "return 2;") { IsInherited = false };
|
||||
var result = await _service.UpdateScriptAsync(200, proposed, "admin");
|
||||
@@ -919,6 +962,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate });
|
||||
|
||||
Template? captured = null;
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
@@ -965,6 +1010,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
@@ -1003,6 +1050,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
@@ -1030,6 +1079,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateAttribute("SetPoint") { Value = "99", DataType = DataType.Float, IsInherited = false };
|
||||
var result = await _service.UpdateAttributeAsync(100, proposed, "admin");
|
||||
@@ -1052,6 +1103,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(5, It.IsAny<CancellationToken>())).ReturnsAsync(new List<Instance>());
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, parent, derived });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, parent, derived });
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(5, "admin");
|
||||
|
||||
@@ -1083,6 +1136,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var result = await _service.AddCompositionAsync(1, 1, "self", "admin");
|
||||
|
||||
@@ -1206,6 +1261,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(childTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parentTemplate, childTemplate });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parentTemplate, childTemplate });
|
||||
|
||||
var proposed = new TemplateAttribute("Speed")
|
||||
{
|
||||
@@ -1241,6 +1298,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(childTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parentTemplate, childTemplate });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { parentTemplate, childTemplate });
|
||||
|
||||
var result = await _service.DeleteAttributeAsync(20, "admin");
|
||||
|
||||
@@ -1295,6 +1354,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(child);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { child });
|
||||
|
||||
var result = await _service.UpdateTemplateAsync(2, "ChildRenamed", null, null, "admin");
|
||||
|
||||
@@ -1313,6 +1374,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(t);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { t });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { t });
|
||||
|
||||
var result = await _service.UpdateTemplateAsync(1, "T", "", null, "admin");
|
||||
|
||||
@@ -1328,6 +1391,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(child);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { child });
|
||||
|
||||
// Idempotent pass — same parent value sent on update should succeed and apply name/description changes.
|
||||
var result = await _service.UpdateTemplateAsync(2, "ChildRenamed", "new", 1, "admin");
|
||||
@@ -1353,6 +1418,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(templateA);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { templateA, templateB, templateC });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { templateA, templateB, templateC });
|
||||
|
||||
var result = await _service.AddCompositionAsync(3, 1, "a1", "admin");
|
||||
|
||||
@@ -1373,6 +1440,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetFolderByIdAsync(7, It.IsAny<CancellationToken>())).ReturnsAsync(folder);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { t });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { t });
|
||||
|
||||
var result = await _service.MoveTemplateAsync(1, 7, "admin");
|
||||
|
||||
@@ -1387,6 +1456,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(t);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { t });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { t });
|
||||
|
||||
var result = await _service.MoveTemplateAsync(1, null, "admin");
|
||||
|
||||
@@ -1422,6 +1493,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetFolderByIdAsync(7, It.IsAny<CancellationToken>())).ReturnsAsync(folder);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { moving, existing });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { moving, existing });
|
||||
|
||||
var result = await _service.MoveTemplateAsync(1, 7, "admin");
|
||||
|
||||
@@ -1444,6 +1517,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetFolderByIdAsync(7, It.IsAny<CancellationToken>())).ReturnsAsync(folder);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { moving, unrelated });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { moving, unrelated });
|
||||
|
||||
var result = await _service.MoveTemplateAsync(1, 7, "admin");
|
||||
|
||||
@@ -1536,6 +1611,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
TemplateAttribute? added = null;
|
||||
_repoMock.Setup(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()))
|
||||
@@ -1576,6 +1653,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
TemplateAlarm? added = null;
|
||||
_repoMock.Setup(r => r.AddTemplateAlarmAsync(It.IsAny<TemplateAlarm>(), It.IsAny<CancellationToken>()))
|
||||
@@ -1621,6 +1700,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
TemplateScript? added = null;
|
||||
_repoMock.Setup(r => r.AddTemplateScriptAsync(It.IsAny<TemplateScript>(), It.IsAny<CancellationToken>()))
|
||||
@@ -1676,6 +1757,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
var added = new List<TemplateAttribute>();
|
||||
_repoMock.Setup(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<TemplateAttribute, CancellationToken>((a, _) => added.Add(a))
|
||||
@@ -1707,6 +1790,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
var deleted = new List<int>();
|
||||
_repoMock.Setup(r => r.DeleteTemplateAttributeAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<int, CancellationToken>((id, _) => deleted.Add(id))
|
||||
@@ -1731,6 +1816,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
var updated = new List<TemplateAttribute>();
|
||||
_repoMock.Setup(r => r.UpdateTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<TemplateAttribute, CancellationToken>((a, _) => updated.Add(a))
|
||||
@@ -1756,6 +1843,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
|
||||
var result = await _service.ResyncInheritedMembersAsync(8, "admin");
|
||||
|
||||
@@ -1781,6 +1870,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, left, right });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, left, right });
|
||||
var added = new List<TemplateAttribute>();
|
||||
_repoMock.Setup(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<TemplateAttribute, CancellationToken>((a, _) => added.Add(a))
|
||||
@@ -1809,6 +1900,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
var addedScripts = new List<TemplateScript>();
|
||||
var addedSources = new List<TemplateNativeAlarmSource>();
|
||||
_repoMock.Setup(r => r.AddTemplateScriptAsync(It.IsAny<TemplateScript>(), It.IsAny<CancellationToken>()))
|
||||
@@ -1846,6 +1939,8 @@ public class TemplateServiceTests
|
||||
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
var added = new List<TemplateAttribute>();
|
||||
_repoMock.Setup(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<TemplateAttribute, CancellationToken>((a, _) => added.Add(a)).Returns(Task.CompletedTask);
|
||||
@@ -1871,6 +1966,8 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(7, It.IsAny<CancellationToken>())).ReturnsAsync(baseT);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesForAnalysisAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseT, child });
|
||||
|
||||
var added = new List<TemplateAttribute>();
|
||||
_repoMock.Setup(r => r.AddTemplateAttributeAsync(It.IsAny<TemplateAttribute>(), It.IsAny<CancellationToken>()))
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// WP2.5: the verdict cache's eviction policy. Overflow used to <c>Clear()</c> the
|
||||
/// whole cache, which re-opened the non-collectible <c>InteractiveAssemblyLoader</c>
|
||||
/// leak the cache exists to bound — every hot script had to be recompiled, and every
|
||||
/// recompile loads another assembly that can never be unloaded. These tests pin the
|
||||
/// replacement: eviction is segmented, so entries in active use survive it.
|
||||
///
|
||||
/// <para>
|
||||
/// Serialised with the other verdict-cache tests: the cache is process-wide static
|
||||
/// state, so two test classes filling it concurrently would see each other's
|
||||
/// entries.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Collection("ScriptCompileVerdictCache")]
|
||||
public class ScriptCompileVerdictCacheEvictionTests
|
||||
{
|
||||
private const string Surface = "TestSurface";
|
||||
|
||||
/// <summary>Entries needed to force at least one generation rotation.</summary>
|
||||
private const int OverflowCount = 5000;
|
||||
|
||||
private static (bool Ok, string? Error) Lookup(string code, Func<(bool, string?)> factory) =>
|
||||
ScriptCompileVerdictCache.GetOrAdd(Surface, code, factory);
|
||||
|
||||
[Fact]
|
||||
public void Overflow_KeepsHotEntry_AndDoesNotClearEverything()
|
||||
{
|
||||
ScriptCompileVerdictCache.Clear();
|
||||
|
||||
const string hotCode = "// the script every deploy re-validates";
|
||||
var hotCompiles = 0;
|
||||
(bool, string?) HotFactory()
|
||||
{
|
||||
hotCompiles++;
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
Lookup(hotCode, HotFactory);
|
||||
Assert.Equal(1, hotCompiles);
|
||||
|
||||
// Push far more distinct scripts through than the cache can hold, touching
|
||||
// the hot entry as we go — which is exactly what a real workload does, and
|
||||
// exactly what wholesale Clear() used to throw away.
|
||||
for (var i = 0; i < OverflowCount; i++)
|
||||
{
|
||||
Lookup($"// filler {i}", static () => (true, null));
|
||||
if (i % 25 == 0)
|
||||
Lookup(hotCode, HotFactory);
|
||||
}
|
||||
|
||||
// At least one rotation happened...
|
||||
Assert.True(ScriptCompileVerdictCache.Evictions > 0,
|
||||
"the overflow did not trigger a single eviction — the test no longer exercises the policy");
|
||||
|
||||
// ...and the hot entry was never recompiled, because a hit in the cold
|
||||
// generation promotes it back into hot rather than letting it age out.
|
||||
Assert.Equal(1, hotCompiles);
|
||||
|
||||
// A final read still hits.
|
||||
var compilesBefore = hotCompiles;
|
||||
Lookup(hotCode, HotFactory);
|
||||
Assert.Equal(compilesBefore, hotCompiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Overflow_RetainsRecentEntries_RatherThanDroppingAll()
|
||||
{
|
||||
ScriptCompileVerdictCache.Clear();
|
||||
|
||||
for (var i = 0; i < OverflowCount; i++)
|
||||
Lookup($"// bulk {i}", static () => (true, null));
|
||||
|
||||
Assert.True(ScriptCompileVerdictCache.Evictions > 0);
|
||||
|
||||
// The most recently inserted entry is in the hot generation, so it must
|
||||
// still be cached. Under the old Clear()-on-overflow policy the cache could
|
||||
// be left holding a single entry after a rotation.
|
||||
var recompiled = false;
|
||||
Lookup($"// bulk {OverflowCount - 1}", () =>
|
||||
{
|
||||
recompiled = true;
|
||||
return (true, null);
|
||||
});
|
||||
|
||||
Assert.False(recompiled, "the most recent entry was evicted; eviction is not retaining the hot generation");
|
||||
Assert.True(ScriptCompileVerdictCache.Count > 1,
|
||||
$"cache retained only {ScriptCompileVerdictCache.Count} entries after eviction");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Overflow_KeepsCacheBounded()
|
||||
{
|
||||
ScriptCompileVerdictCache.Clear();
|
||||
|
||||
for (var i = 0; i < OverflowCount; i++)
|
||||
Lookup($"// bounded {i}", static () => (true, null));
|
||||
|
||||
// Two generations of 2048 — the same 4096 ceiling the previous policy had,
|
||||
// now reached by demotion rather than by dropping everything.
|
||||
Assert.True(ScriptCompileVerdictCache.Count <= 4096,
|
||||
$"cache grew to {ScriptCompileVerdictCache.Count} entries, exceeding its two-generation bound");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SurfaceIsPartOfTheKey_AcrossEviction()
|
||||
{
|
||||
ScriptCompileVerdictCache.Clear();
|
||||
|
||||
const string code = "// same body, two surfaces";
|
||||
Lookup(code, static () => (true, null));
|
||||
|
||||
var otherSurfaceCompiled = false;
|
||||
var verdict = ScriptCompileVerdictCache.GetOrAdd("OtherSurface", code, () =>
|
||||
{
|
||||
otherSurfaceCompiled = true;
|
||||
return (false, "not valid against this surface");
|
||||
});
|
||||
|
||||
// A verdict is never interchangeable across globals surfaces, and the
|
||||
// segmented cache must not weaken that.
|
||||
Assert.True(otherSurfaceCompiled);
|
||||
Assert.False(verdict.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialises every test that touches the process-wide
|
||||
/// <see cref="ScriptCompileVerdictCache"/> static.
|
||||
/// </summary>
|
||||
[CollectionDefinition("ScriptCompileVerdictCache")]
|
||||
public class ScriptCompileVerdictCacheCollection;
|
||||
@@ -10,6 +10,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
|
||||
/// authoritative behavior: bypasses the old substring scan missed are now caught,
|
||||
/// and undefined symbols (which a structural scan could never see) fail compile.
|
||||
/// </summary>
|
||||
// Serialised with ScriptCompileVerdictCacheEvictionTests: both drive the
|
||||
// process-wide ScriptCompileVerdictCache static, and an overflow test running
|
||||
// concurrently would evict the entries these hit-count assertions depend on.
|
||||
[Collection("ScriptCompileVerdictCache")]
|
||||
public class ScriptCompilerTests
|
||||
{
|
||||
private readonly ScriptCompiler _sut = new();
|
||||
|
||||
Reference in New Issue
Block a user