106 lines
4.2 KiB
C#
106 lines
4.2 KiB
C#
using System.Reflection;
|
|
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
|
|
|
/// <summary>
|
|
/// M3.3: compile-surface parity guard. The shared
|
|
/// <see cref="ScriptCompileSurface"/> / <see cref="TriggerCompileSurface"/> are
|
|
/// compile-only stubs the design-time deploy gate binds candidate scripts
|
|
/// against; they must mirror the real SiteRuntime <see cref="ScriptGlobals"/> /
|
|
/// <see cref="TriggerExpressionGlobals"/> bind surfaces. If a member a script can
|
|
/// reference on the real globals is missing from the compile surface, a script
|
|
/// that uses it would pass the design-time gate but fail at the site — so the
|
|
/// compile surface's public top-level member NAMES must be a SUPERSET of the
|
|
/// real globals' names. This test fails loudly (listing the missing names) when
|
|
/// the surface drifts behind the runtime globals.
|
|
///
|
|
/// <para>
|
|
/// Top-level member-name parity is sufficient: the design-time compile against
|
|
/// the surface catches deeper signature mismatches itself; this guard only
|
|
/// ensures every entry point a script can name on the real globals exists on the
|
|
/// stub.
|
|
/// </para>
|
|
/// </summary>
|
|
public class CompileSurfaceParityTests
|
|
{
|
|
[Fact]
|
|
public void ScriptCompileSurface_MemberNames_AreSupersetOf_ScriptGlobals()
|
|
{
|
|
AssertSurfaceCoversGlobals(
|
|
surface: typeof(ScriptCompileSurface),
|
|
globals: typeof(ScriptGlobals));
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerCompileSurface_MemberNames_AreSupersetOf_TriggerExpressionGlobals()
|
|
{
|
|
AssertSurfaceCoversGlobals(
|
|
surface: typeof(TriggerCompileSurface),
|
|
globals: typeof(TriggerExpressionGlobals));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that the public instance property + method member names of
|
|
/// <paramref name="surface"/> are a superset of those of
|
|
/// <paramref name="globals"/>. Inherited <see cref="object"/> members
|
|
/// (ToString/GetHashCode/Equals/GetType) are excluded. Fails with the exact
|
|
/// list of missing names when the surface does not cover the globals.
|
|
/// </summary>
|
|
private static void AssertSurfaceCoversGlobals(Type surface, Type globals)
|
|
{
|
|
var surfaceNames = PublicInstanceMemberNames(surface);
|
|
var globalsNames = PublicInstanceMemberNames(globals);
|
|
|
|
var missing = globalsNames
|
|
.Where(name => !surfaceNames.Contains(name))
|
|
.OrderBy(name => name, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
Assert.True(
|
|
missing.Count == 0,
|
|
$"Compile surface '{surface.Name}' is missing {missing.Count} member name(s) "
|
|
+ $"present on '{globals.Name}': {string.Join(", ", missing)}. "
|
|
+ "The compile-only surface must mirror the runtime globals — add the "
|
|
+ "missing member(s) to the ScriptAnalysis surface type.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Public instance property + method member names declared on or inherited by
|
|
/// <paramref name="type"/>, excluding the inherited <see cref="object"/>
|
|
/// members (ToString, GetHashCode, Equals, GetType) so only script-reachable
|
|
/// API names remain. Property/method names are compared (no signatures).
|
|
/// </summary>
|
|
private static HashSet<string> PublicInstanceMemberNames(Type type)
|
|
{
|
|
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
|
|
|
|
var objectMembers = new HashSet<string>(StringComparer.Ordinal)
|
|
{
|
|
nameof(ToString),
|
|
nameof(GetHashCode),
|
|
nameof(Equals),
|
|
nameof(GetType),
|
|
};
|
|
|
|
var names = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var property in type.GetProperties(flags))
|
|
names.Add(property.Name);
|
|
|
|
foreach (var method in type.GetMethods(flags))
|
|
{
|
|
// Skip compiler-generated property accessors (get_X / set_X) — the
|
|
// property itself is already counted by name above.
|
|
if (method.IsSpecialName)
|
|
continue;
|
|
if (objectMembers.Contains(method.Name))
|
|
continue;
|
|
names.Add(method.Name);
|
|
}
|
|
|
|
return names;
|
|
}
|
|
}
|