using ZB.MOM.WW.ScadaBridge.ScriptAnalysis; namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests; /// /// M3.1: parse + compile gate tests. The "representative real script" corpus is /// the PRIMARY guard that faithfully mirrors /// the runtime ScriptGlobals surface — if a member or signature drifts, /// the corpus stops binding and this test fails. /// public class RoslynScriptCompilerTests { [Fact] public void ParseDiagnostics_NonEmpty_ForSyntaxError() { Assert.NotEmpty(RoslynScriptCompiler.ParseDiagnostics("var x = ;")); } [Fact] public void ParseDiagnostics_Empty_ForValidSyntax() { Assert.Empty(RoslynScriptCompiler.ParseDiagnostics("var x = 1;")); } [Fact] public void Compile_NonEmpty_ForUndefinedSymbol() { var code = "var x = NoSuchThing.Foo();"; Assert.NotEmpty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface))); } [Fact] public void Compile_Empty_ForRepresentativeRealScript() { const string code = """ var temp = Attributes["Temperature"]; Attributes["Setpoint"] = 42; var r = await ExternalSystem.Call("erp", "sync"); var op = await Database.CachedWrite("hist", "INSERT ..."); await Notify.To("ops").Send("subj", "msg"); var shared = await Scripts.CallShared("Helper"); var child = Children["Pump"].Attributes["Speed"]; // Widen coverage across the rest of the surface. var attr = await Instance.GetAttribute("Temperature"); await Instance.SetAttribute("Setpoint", "43"); var track = await Instance.Tracking.Status(op); var parentSpeed = Parent?.Attributes["Speed"]; var alarmName = Alarm?.Name; var p = Parameters; var ct = CancellationToken; var status = await Notify.Status("notif-id"); var cachedCall = await ExternalSystem.CachedCall("erp", "ping"); var resolved = Attributes.Resolve("Temperature"); var conn = await Database.Connection("hist"); var scope = Scope; var currentAlarms = await Alarms.CurrentAsync(); var viaInstance = await Instance.Alarms.CurrentAsync(); """; var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)); Assert.Empty(diagnostics); } [Fact] public void Compile_Empty_ForTheMesAlarmStatusScriptShape() { // MES alarm-status API §5.3: the CvdReactor.SimpleAlarmStatus body must bind at // design time. Every ScriptAlarm field the endpoint projects is read here, so a // rename on the record breaks this test rather than the deployed script. const string code = """ const int MesBandMin = 900; const int MesBandMax = 999; var raw = (Parameters["SAPID"] as string) ?? ""; var code = Parameters["MachineCode"]?.ToString() ?? ""; var core = raw.EndsWith("_LT") ? raw.Substring(0, raw.Length - 3) : raw; var side = core.EndsWith("_A") ? "Left" : core.EndsWith("_B") ? "Right" : null; System.Func InScope = src => string.IsNullOrEmpty(src) || side == null || src.StartsWith(side, System.StringComparison.OrdinalIgnoreCase) || src.StartsWith("Reactor", System.StringComparison.OrdinalIgnoreCase); var alarms = await Alarms.CurrentAsync(); var infos = alarms .Where(a => a.Active && !a.IsConfiguredPlaceholder) .Where(a => InScope(a.NativeSourceCanonicalName)) .Where(a => a.Severity >= MesBandMin && a.Severity <= MesBandMax) .Select(a => new { Name = a.Name, HierarchicalName = code + "." + a.Name, Description = string.IsNullOrEmpty(a.Message) ? a.AlarmTypeName : a.Message, IsFlaggedForMES = a.Severity >= MesBandMin && a.Severity <= MesBandMax, Severity = a.Severity, StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered", TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp), AckDT = a.AckTime, AckComment = a.OperatorComment, }).ToList(); return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos }; """; Assert.Empty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface))); } [Fact] public void Compile_Empty_ForTriggerExpression() { const string expr = "Attributes[\"Temp\"] != null && (int)(Children[\"P\"].Attributes[\"S\"] ?? 0) > 5"; var diagnostics = RoslynScriptCompiler.Compile(expr, typeof(TriggerCompileSurface)); Assert.Empty(diagnostics); } [Fact] public void Compile_Empty_ForWaitAsyncAndWaitForAsync() { // Covers all four overloads: value + predicate for both WaitAsync and // WaitForAsync, on both root scope and composed/child scope. const string code = """ // Root scope — value overload var matched = await Attributes.WaitAsync("Flag", true, System.TimeSpan.FromSeconds(5)); // Root scope — predicate overload with requireGoodQuality var matched2 = await Attributes.WaitAsync("Flag", v => v != null, System.TimeSpan.FromSeconds(5), true); // Root scope — WaitForAsync value overload var r = await Attributes.WaitForAsync("Flag", true, System.TimeSpan.FromSeconds(5)); // Root scope — WaitForAsync predicate overload with requireGoodQuality var r2 = await Attributes.WaitForAsync("Flag", v => v != null, System.TimeSpan.FromSeconds(5), true); // Composed/child scope — value overload var childMatched = await Children["LeftMESReceiver"].Attributes.WaitAsync("MoveInCompleteFlag", true, System.TimeSpan.FromSeconds(5)); """; var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)); Assert.Empty(diagnostics); } }