fix(scriptanalysis): warn on scripts blocking on a Task (.Result/.Wait()/GetAwaiter().GetResult()) (review 2026-10-21 01/SR-U2)
Advisory, not a trust-policy violation: every policy entry blocks deploy, and an error here would fail the next redeploy of already-deployed scripts. ValidationService surfaces it as a non-blocking ScriptCompilation warning.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 01/SR-U2: synchronously blocking on a Task inside a site
|
||||
/// script (<c>.Result</c>, <c>.Wait()</c>, <c>.GetAwaiter().GetResult()</c>) can
|
||||
/// starve the dedicated script-execution pool — the awaited helper's continuation
|
||||
/// is queued to the same pool the blocked worker occupies. The analyzer reports an
|
||||
/// advisory for each such use, resolved semantically so a non-Task <c>.Result</c>
|
||||
/// is not flagged.
|
||||
/// </summary>
|
||||
public class BlockingWaitAnalyzerTests
|
||||
{
|
||||
/// <summary>Verifies that each blocking-wait shape on a Task is reported.</summary>
|
||||
/// <param name="code">A script blocking on a Task.</param>
|
||||
[Theory]
|
||||
[InlineData("var r = Instance.CallScript(\"Calc\").Result;")]
|
||||
[InlineData("var s = Notify.Status(\"id\").Result;")]
|
||||
[InlineData("Instance.CallScript(\"Calc\").Wait();")]
|
||||
[InlineData("var r = Instance.CallScript(\"Calc\").GetAwaiter().GetResult();")]
|
||||
[InlineData("var a = Alarms.CurrentAsync().Result;")]
|
||||
[InlineData("var t = System.Threading.Tasks.Task.Delay(1); t.Wait(100);")]
|
||||
[InlineData("var r = Instance.CallScript(\"Calc\").ConfigureAwait(false).GetAwaiter().GetResult();")]
|
||||
public void Find_ReportsBlockingWaitOnTask(string code)
|
||||
{
|
||||
Assert.NotEmpty(BlockingWaitAnalyzer.Find(code, typeof(ScriptCompileSurface)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that awaited calls and non-Task members named Result/Wait are not reported.</summary>
|
||||
/// <param name="code">A script that does not block on a Task.</param>
|
||||
[Theory]
|
||||
[InlineData("var r = await Instance.CallScript(\"Calc\");")]
|
||||
[InlineData("var a = await Alarms.CurrentAsync();")]
|
||||
[InlineData("var o = new { Result = 1 }; var x = o.Result;")]
|
||||
[InlineData("var t = Instance.CallScript(\"Calc\"); return t.IsCompleted;")]
|
||||
[InlineData("")]
|
||||
public void Find_IgnoresNonBlockingCode(string code)
|
||||
{
|
||||
Assert.Empty(BlockingWaitAnalyzer.Find(code, typeof(ScriptCompileSurface)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the advisory names the blocking member so the author can find it.</summary>
|
||||
[Fact]
|
||||
public void Find_MessageNamesTheMember()
|
||||
{
|
||||
var advisories = BlockingWaitAnalyzer.Find("var r = Instance.CallScript(\"Calc\").Result;", typeof(ScriptCompileSurface));
|
||||
Assert.Contains(advisories, a => a.Contains(".Result") && a.Contains("await"));
|
||||
}
|
||||
}
|
||||
@@ -504,6 +504,44 @@ public class ValidationServiceTests
|
||||
Assert.Contains(gated.Errors, e => e.Message.Contains("Ghost"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 01/SR-U2: a script that blocks on a Task gets a non-blocking
|
||||
/// warning (it still deploys — existing scripts are not broken), naming the script.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_ScriptBlockingOnTask_ReturnsWarningNotError()
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Scripts = [new ResolvedScript { CanonicalName = "Blocker", Code = "var a = Alarms.CurrentAsync().Result;" }]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Contains(result.Warnings, w =>
|
||||
w.Category == ValidationCategory.ScriptCompilation
|
||||
&& w.EntityName == "Blocker"
|
||||
&& w.Message.Contains(".Result"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an awaiting script produces no blocking-wait warning.</summary>
|
||||
[Fact]
|
||||
public void Validate_ScriptAwaitingTask_NoBlockingWaitWarning()
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Scripts = [new ResolvedScript { CanonicalName = "Good", Code = "var a = await Alarms.CurrentAsync();" }]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.DoesNotContain(result.Warnings, w => w.EntityName == "Good");
|
||||
}
|
||||
|
||||
private static FlattenedConfiguration ConfigWithExpressionTriggerScript(string expression)
|
||||
{
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(new { expression });
|
||||
|
||||
Reference in New Issue
Block a user