fix(scriptanalysis): compile gate reads diagnostics only and never caches a faulted verdict (review 2026-10-21 04/M1, 04/C1)
RoslynScriptCompiler.Compile now uses Script.GetCompilation().GetDiagnostics() with one shared, never-loading InteractiveAssemblyLoader instead of Script.Compile(), which emitted and loaded a non-collectible assembly (and a fresh AssemblyLoadContext) per distinct script. The catch-all that turned any exception into a 'Compilation exception' diagnostic is gone; TryCompile and CheckExpressionSyntax report a fault for that call only, uncached.
This commit is contained in:
+2
-3
@@ -4,9 +4,8 @@ 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
|
||||
/// whole cache, so every hot script had to be re-analysed and recompiled at once.
|
||||
/// These tests pin the
|
||||
/// replacement: eviction is segmented, so entries in active use survive it.
|
||||
///
|
||||
/// <para>
|
||||
|
||||
@@ -259,4 +259,53 @@ public class ScriptCompilerTests
|
||||
Assert.Contains("'Beta'", second.Error);
|
||||
Assert.Contains("compile", second.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// --- Review 2026-10-21 04/C1: transient faults must not become sticky verdicts ---
|
||||
|
||||
/// <summary>Verifies that a factory that throws leaves the cache slot empty, so the next call re-runs it.</summary>
|
||||
[Fact]
|
||||
public void GetOrAdd_FactoryThrows_NextCallReRunsFactory()
|
||||
{
|
||||
ScriptCompileVerdictCache.Clear();
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
ScriptCompileVerdictCache.GetOrAdd("SurfaceC1", "return 7;",
|
||||
() => throw new InvalidOperationException("transient")));
|
||||
|
||||
var calls = 0;
|
||||
var verdict = ScriptCompileVerdictCache.GetOrAdd("SurfaceC1", "return 7;", () =>
|
||||
{
|
||||
calls++;
|
||||
return (true, null);
|
||||
});
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
Assert.True(verdict.Ok);
|
||||
Assert.Equal(0, ScriptCompileVerdictCache.Hits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a compile gate that faults once reports a failure for that call
|
||||
/// only — the next call with the same code re-runs the compile and succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryCompile_CompileFaultsOnce_FailureIsNotCached()
|
||||
{
|
||||
ScriptCompileVerdictCache.Clear();
|
||||
var faults = 1;
|
||||
var c = new ScriptCompiler(_ =>
|
||||
{
|
||||
if (faults-- > 0)
|
||||
throw new System.IO.IOException("metadata read failed");
|
||||
return [];
|
||||
});
|
||||
const string code = "return 8;";
|
||||
|
||||
var first = c.TryCompile(code, "Flaky");
|
||||
Assert.True(first.IsFailure);
|
||||
Assert.Contains("'Flaky'", first.Error);
|
||||
Assert.Contains("metadata read failed", first.Error);
|
||||
Assert.Equal(0, ScriptCompileVerdictCache.Count); // nothing memoised
|
||||
|
||||
Assert.True(c.TryCompile(code, "Flaky").IsSuccess); // pre-fix: sticky "failed to compile"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user