test(scripts): per-row exclusion seam in CompileSurfaceParityTests; guard ScriptLogger/CompileLog (ScadaBridge#57)
AccessorMirrorPairs rows now carry an ignoredRuntimeMethods list. An entry must exist on the runtime accessor and must be ABSENT from the mirror, so a stale or over-broad exclusion fails the theory. The (ScriptLogger, CompileLog) pair joins the guarded set with FlushDropped excluded (run-end flush called by ScriptRunLauncher, not script-reachable). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -67,14 +67,44 @@ public class CompileSurfaceParityTests
|
||||
/// </summary>
|
||||
/// <param name="runtimeAccessor">The runtime script accessor type whose public methods are enumerated.</param>
|
||||
/// <param name="compileMirror">The compile-only mirror type that must cover those methods.</param>
|
||||
/// <param name="ignoredRuntimeMethods">
|
||||
/// Runtime method names that are public but NOT script-reachable and must therefore be absent
|
||||
/// from the mirror (e.g. <c>ScriptLogger.FlushDropped</c>, called by the run launcher). Each
|
||||
/// entry must exist on the runtime accessor and must NOT exist on the mirror, so a stale or
|
||||
/// over-broad exclusion fails the theory instead of silently widening the hole (Gitea #57).
|
||||
/// </param>
|
||||
[Theory]
|
||||
[MemberData(nameof(AccessorMirrorPairs))]
|
||||
public void CompileAccessorMirror_Covers_RuntimeAccessor_MethodArities(
|
||||
Type runtimeAccessor, Type compileMirror)
|
||||
Type runtimeAccessor, Type compileMirror, string[] ignoredRuntimeMethods)
|
||||
{
|
||||
var ignored = new HashSet<string>(ignoredRuntimeMethods, StringComparer.Ordinal);
|
||||
var mirrorCounts = PublicInstanceMethodArityCounts(compileMirror);
|
||||
var runtimeCounts = PublicInstanceMethodArityCounts(runtimeAccessor);
|
||||
|
||||
var staleExclusions = ignored
|
||||
.Where(name => !runtimeCounts.Keys.Any(k => k.Name == name))
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
Assert.True(
|
||||
staleExclusions.Count == 0,
|
||||
$"Exclusion(s) {string.Join(", ", staleExclusions)} name no public method on runtime accessor "
|
||||
+ $"'{runtimeAccessor.Name}' — remove the stale entry from AccessorMirrorPairs.");
|
||||
|
||||
var exposedExclusions = ignored
|
||||
.Where(name => mirrorCounts.Keys.Any(k => k.Name == name))
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
Assert.True(
|
||||
exposedExclusions.Count == 0,
|
||||
$"Excluded runtime method(s) {string.Join(", ", exposedExclusions)} are present on mirror "
|
||||
+ $"'{compileMirror.Name}'. An exclusion means the member is NOT script-reachable, so the "
|
||||
+ "mirror must not offer it — either drop it from the mirror or drop the exclusion.");
|
||||
|
||||
runtimeCounts = runtimeCounts
|
||||
.Where(kvp => !ignored.Contains(kvp.Key.Name))
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
var shortfalls = runtimeCounts
|
||||
.Where(kvp => mirrorCounts.GetValueOrDefault(kvp.Key) < kvp.Value)
|
||||
.OrderBy(kvp => kvp.Key.Name, StringComparer.Ordinal)
|
||||
@@ -99,27 +129,39 @@ public class CompileSurfaceParityTests
|
||||
/// Runtime script-accessor type ↔ compile-surface mirror pairs guarded by
|
||||
/// <see cref="CompileAccessorMirror_Covers_RuntimeAccessor_MethodArities"/>.
|
||||
/// </summary>
|
||||
/// <returns>The runtime accessor and compile mirror type pairs, one per theory case.</returns>
|
||||
/// <returns>
|
||||
/// The runtime accessor, compile mirror and ignored-runtime-method triples, one per theory case.
|
||||
/// </returns>
|
||||
public static IEnumerable<object[]> AccessorMirrorPairs() =>
|
||||
new[]
|
||||
{
|
||||
new object[] { typeof(AttributeAccessor), typeof(ScriptCompileSurface.CompileAttributeAccessor) },
|
||||
new object[] { typeof(CompositionAccessor), typeof(ScriptCompileSurface.CompileCompositionAccessor) },
|
||||
new object[] { typeof(AttributeAccessor), typeof(ScriptCompileSurface.CompileAttributeAccessor), NoExclusions },
|
||||
new object[] { typeof(CompositionAccessor), typeof(ScriptCompileSurface.CompileCompositionAccessor), NoExclusions },
|
||||
// ChildrenAccessor currently exposes only an indexer (no public instance
|
||||
// methods), so this row guards nothing today. It is kept so that if a
|
||||
// callable method is ever added to ChildrenAccessor, this Theory begins
|
||||
// enforcing a mirror for it automatically — rather than silently
|
||||
// re-opening the WaitAsync-class gap.
|
||||
new object[] { typeof(ChildrenAccessor), typeof(ScriptCompileSurface.CompileChildrenAccessor) },
|
||||
new object[] { typeof(ChildrenAccessor), typeof(ScriptCompileSurface.CompileChildrenAccessor), NoExclusions },
|
||||
// MES alarm-status API §5.2: the Alarms accessor joins the guarded set so a
|
||||
// future read method (e.g. an ActiveAsync convenience) cannot ship on the
|
||||
// runtime accessor while the design-time gate stays blind to it.
|
||||
new object[] { typeof(AlarmsAccessor), typeof(ScriptCompileSurface.CompileAlarmsAccessor) },
|
||||
new object[] { typeof(AlarmsAccessor), typeof(ScriptCompileSurface.CompileAlarmsAccessor), NoExclusions },
|
||||
// Script file access (design 2026-09-28): a new Files.* read method cannot ship on the
|
||||
// runtime accessor without its design-time mirror.
|
||||
new object[] { typeof(FilesAccessor), typeof(ScriptCompileSurface.CompileFiles) },
|
||||
new object[] { typeof(FilesAccessor), typeof(ScriptCompileSurface.CompileFiles), NoExclusions },
|
||||
// Step logs (Gitea #57, register row 116): the Log global. FlushDropped() is the run-end
|
||||
// closing-row flush called by ScriptRunLauncher, not by a script, so the mirror must omit it.
|
||||
new object[]
|
||||
{
|
||||
typeof(ScriptLogger), typeof(ScriptCompileSurface.CompileLog),
|
||||
new[] { nameof(ScriptLogger.FlushDropped) },
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>Exclusion list for a row whose mirror must cover every public runtime method.</summary>
|
||||
private static readonly string[] NoExclusions = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that the public instance property + method member names of
|
||||
/// <paramref name="surface"/> are a superset of those of
|
||||
|
||||
Reference in New Issue
Block a user