using System.Reflection; using ZB.MOM.WW.ScadaBridge.ScriptAnalysis; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts; /// /// M3.3: compile-surface parity guard. The shared /// / are /// compile-only stubs the design-time deploy gate binds candidate scripts /// against; they must mirror the real SiteRuntime / /// 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. /// /// /// 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. /// /// 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)); } /// /// Asserts that the public instance property + method member names of /// are a superset of those of /// . Inherited members /// (ToString/GetHashCode/Equals/GetType) are excluded. Fails with the exact /// list of missing names when the surface does not cover the globals. /// 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."); } /// /// Public instance property + method member names declared on or inherited by /// , excluding the inherited /// members (ToString, GetHashCode, Equals, GetType) so only script-reachable /// API names remain. Property/method names are compared (no signatures). /// private static HashSet PublicInstanceMemberNames(Type type) { const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; var objectMembers = new HashSet(StringComparer.Ordinal) { nameof(ToString), nameof(GetHashCode), nameof(Equals), nameof(GetType), }; var names = new HashSet(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; } }