Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Validation/ScriptCompileVerdictCacheEvictionTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

170 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Validation;
/// <summary>
/// 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);
/// <summary>Verifies that an overflow keeps the hot entry instead of clearing the whole cache.</summary>
[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);
}
/// <summary>Verifies that an overflow retains recent entries rather than dropping all of them.</summary>
[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");
}
/// <summary>Verifies that an overflow keeps the cache within its size bound.</summary>
[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");
}
/// <summary>
/// The bound must hold under a PROMOTION-heavy workload too, not just a
/// pure-insert one. Promotion used to write straight into hot
/// (<c>_hot[key] = verdict</c>) with no capacity check, so re-reading a working
/// set bigger than one segment pulled the whole cold generation back into hot
/// on top of what hot already held — hot alone reached 2 × SegmentCapacity and
/// the total 3 ×, against a documented 2 ×. Pure-insert overflow never hits
/// that path because every key is distinct.
/// </summary>
[Fact]
public void PromotionOverflow_KeepsCacheBounded()
{
ScriptCompileVerdictCache.Clear();
// Fill past one rotation so a large working set is sitting in cold with
// hot already partly full.
const int WorkingSet = 3000;
for (var i = 0; i < WorkingSet; i++)
Lookup($"// promo {i}", static () => (true, null));
Assert.True(ScriptCompileVerdictCache.Evictions > 0);
// Re-read the whole working set: every entry still in cold promotes.
for (var i = 0; i < WorkingSet; i++)
Lookup($"// promo {i}", static () => (true, null));
Assert.True(ScriptCompileVerdictCache.Count <= 4096,
$"cache grew to {ScriptCompileVerdictCache.Count} entries under promotion, exceeding its two-generation bound");
}
/// <summary>Verifies that the compile surface remains part of the cache key across eviction.</summary>
[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;