d683b83156
DriverHostActor built ScriptedAlarmEngine without a scriptTimeout, so every
production alarm predicate fell back to TimedScriptEvaluator.DefaultTimeout
(250 ms) while virtual tags and Calculation tags ran on 2 s. History shows no
decision behind it: 250 ms is the generic Phase-7 A.4 default, the VT
evaluator moved to 2 s in F8b (219d10a2) and the alarm engine was simply never
given a value.
- New node-wide Scripting:RunTimeout (default 2 s, clamped to (0, 60 s],
non-positive -> default, each clamp a startup Warning) on ScriptingOptions.
- The VT evaluator and the alarm engine default to it; DriverHostActor now
builds the engine through CreateScriptedAlarmEngine, which passes it
explicitly. Calculation keeps its per-driver runTimeoutMs.
- Docs: ScriptEditor (config table), ScriptedAlarms, VirtualTags.
Tests: ScriptedAlarmEngineBudgetTests (a 600 ms predicate now completes and
activates; default and configured budgets), ScriptedAlarmEngineBudgetWiringTests
(the host-built engine), ScriptingOptionsBindingTests (bind + clamps). The
engine and wiring tests fail with the old 250 ms default restored.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
83 lines
3.5 KiB
C#
83 lines
3.5 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
|
|
|
|
/// <summary>
|
|
/// Review m-1: the <c>Scripting</c> section binds by the documented key names and is clamped at bind. Each
|
|
/// configured value differs from the compiled default, so a misspelled key (which the binder would silently
|
|
/// ignore, falling back to the default) goes red.
|
|
/// </summary>
|
|
public sealed class ScriptingOptionsBindingTests
|
|
{
|
|
private static IConfiguration Config(Dictionary<string, string?> values) =>
|
|
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
|
|
|
|
[Fact]
|
|
public void Every_documented_key_binds_to_its_property()
|
|
{
|
|
var bound = ScriptingOptionsBinding.Bind(Config(new()
|
|
{
|
|
["Scripting:MaxAllocBytesPerRun"] = "8388608",
|
|
["Scripting:QuarantineAfterConsecutiveTimeouts"] = "7",
|
|
["Scripting:StuckOrphanQuarantineFactor"] = "4.5",
|
|
["Scripting:RunTimeout"] = "00:00:05",
|
|
// Sibling keys owned by the script-log pipeline must not disturb the bind.
|
|
["Scripting:LogFilePath"] = "logs/x-.log",
|
|
["Scripting:LogTopicMinLevel"] = "Warning",
|
|
}), out var adjustments);
|
|
|
|
adjustments.ShouldBeEmpty();
|
|
bound.MaxAllocBytesPerRun.ShouldBe(8L * 1024 * 1024);
|
|
bound.QuarantineAfterConsecutiveTimeouts.ShouldBe(7);
|
|
bound.StuckOrphanQuarantineFactor.ShouldBe(4.5);
|
|
bound.RunTimeout.ShouldBe(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void Out_of_range_values_are_clamped_at_bind_and_reported()
|
|
{
|
|
var bound = ScriptingOptionsBinding.Bind(Config(new()
|
|
{
|
|
["Scripting:StuckOrphanQuarantineFactor"] = "1e300",
|
|
["Scripting:MaxAllocBytesPerRun"] = "1024",
|
|
["Scripting:QuarantineAfterConsecutiveTimeouts"] = "0",
|
|
["Scripting:RunTimeout"] = "00:10:00",
|
|
}), out var adjustments);
|
|
|
|
bound.StuckOrphanQuarantineFactor.ShouldBe(ScriptingOptions.MaxStuckOrphanQuarantineFactor);
|
|
bound.MaxAllocBytesPerRun.ShouldBe(ScriptingOptions.MinMaxAllocBytesPerRun);
|
|
bound.QuarantineAfterConsecutiveTimeouts.ShouldBe(1);
|
|
bound.RunTimeout.ShouldBe(ScriptingOptions.MaxRunTimeout);
|
|
adjustments.Count.ShouldBe(4);
|
|
}
|
|
|
|
/// <summary>lmxopcua#779: a non-positive budget is not "no timeout"; it falls back to the default.</summary>
|
|
[Theory]
|
|
[InlineData("00:00:00")]
|
|
[InlineData("-00:00:01")]
|
|
public void A_non_positive_RunTimeout_becomes_the_default_and_is_reported(string value)
|
|
{
|
|
var bound = ScriptingOptionsBinding.Bind(Config(new() { ["Scripting:RunTimeout"] = value }), out var adjustments);
|
|
|
|
bound.RunTimeout.ShouldBe(ScriptingOptions.DefaultRunTimeout);
|
|
adjustments.Count.ShouldBe(1);
|
|
adjustments[0].ShouldContain("RunTimeout");
|
|
}
|
|
|
|
[Fact]
|
|
public void An_absent_section_yields_the_compiled_defaults()
|
|
{
|
|
var bound = ScriptingOptionsBinding.Bind(Config(new()), out var adjustments);
|
|
|
|
adjustments.ShouldBeEmpty();
|
|
bound.MaxAllocBytesPerRun.ShouldBe(ScriptingOptions.DefaultMaxAllocBytesPerRun);
|
|
bound.QuarantineAfterConsecutiveTimeouts.ShouldBe(ScriptingOptions.DefaultQuarantineAfterConsecutiveTimeouts);
|
|
bound.StuckOrphanQuarantineFactor.ShouldBe(ScriptingOptions.DefaultStuckOrphanQuarantineFactor);
|
|
bound.RunTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
|
}
|
|
}
|