test(scripting): pin the Scripting section bind behind one helper
Review m-1. The bind moves from Program.cs into Host.Configuration.ScriptingOptionsBinding.Bind, which binds the section and clamps it. Program.cs calls it, and ScriptingOptionsBindingTests drives it with in-memory configuration. Every documented key takes a non-default value, so a misspelled property or key goes red instead of silently falling back to the defaults. The tests also cover a clamped bind (1e300 / 1024 / 0, three adjustments), an absent section, and the sibling LogFilePath / LogTopicMinLevel keys leaving the bind untouched. Refs #731 #734 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the node-wide <c>Scripting</c> section into <see cref="ScriptingOptions"/> and clamps it
|
||||
/// (<see cref="ScriptingOptions.Sanitized"/>). One place, so <c>Program.cs</c> and its unit test bind the same
|
||||
/// way and a misspelled key cannot silently fall back to the compiled defaults unnoticed.
|
||||
/// </summary>
|
||||
public static class ScriptingOptionsBinding
|
||||
{
|
||||
/// <summary>Binds and clamps the <c>Scripting</c> section.</summary>
|
||||
/// <param name="configuration">The host configuration.</param>
|
||||
/// <param name="adjustments">One line per clamped value, for the caller to log as a Warning.</param>
|
||||
/// <returns>The clamped options; the compiled defaults when the section is absent.</returns>
|
||||
public static ScriptingOptions Bind(IConfiguration configuration, out IReadOnlyList<string> adjustments)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
var bound = configuration.GetSection(ScriptingOptions.SectionName).Get<ScriptingOptions>() ?? new ScriptingOptions();
|
||||
return bound.Sanitized(out adjustments);
|
||||
}
|
||||
}
|
||||
@@ -370,8 +370,8 @@ if (hasDriver)
|
||||
// Warning rather than an OverflowException inside a script run. The one instance is shared by every script
|
||||
// seam on the node: the virtual-tag evaluator below, the scripted-alarm engine (DriverHostActor, via
|
||||
// WithOtOpcUaRuntimeActors) and every Calculation driver (DriverFactoryBootstrap).
|
||||
var scriptingOptions = (builder.Configuration.GetSection(ScriptingOptions.SectionName).Get<ScriptingOptions>()
|
||||
?? new ScriptingOptions()).Sanitized(out var scriptingAdjustments);
|
||||
var scriptingOptions = ZB.MOM.WW.OtOpcUa.Host.Configuration.ScriptingOptionsBinding.Bind(
|
||||
builder.Configuration, out var scriptingAdjustments);
|
||||
foreach (var adjustment in scriptingAdjustments)
|
||||
Log.Warning("Scripting misconfiguration detected at startup: {Adjustment}", adjustment);
|
||||
builder.Services.AddSingleton(scriptingOptions);
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
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",
|
||||
// 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);
|
||||
}
|
||||
|
||||
[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",
|
||||
}), out var adjustments);
|
||||
|
||||
bound.StuckOrphanQuarantineFactor.ShouldBe(ScriptingOptions.MaxStuckOrphanQuarantineFactor);
|
||||
bound.MaxAllocBytesPerRun.ShouldBe(ScriptingOptions.MinMaxAllocBytesPerRun);
|
||||
bound.QuarantineAfterConsecutiveTimeouts.ShouldBe(1);
|
||||
adjustments.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user