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; /// /// Review m-1: the Scripting 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. /// public sealed class ScriptingOptionsBindingTests { private static IConfiguration Config(Dictionary 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); } /// lmxopcua#779: a non-positive budget is not "no timeout"; it falls back to the default. [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)); } }