using Microsoft.Extensions.Configuration; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Resilience; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration; /// /// lmxopcua#777: DriverWrites:Timeout is the one value every write bound derives from, and it is checked /// against the resilience tiers' Write timeouts at startup. /// public sealed class DriverWriteBoundsBindingTests { private static IConfiguration Config(Dictionary values) => new ConfigurationBuilder().AddInMemoryCollection(values).Build(); /// The shipped default is consistent with every tier — no tier's Write policy is capped below itself. [Fact] public void An_absent_key_yields_the_default_which_fits_every_tier() { var bounds = DriverWriteBoundsBinding.Bind(Config(new()), out var warnings); warnings.ShouldBeEmpty(); bounds.ShouldBe(DriverWriteBounds.Default); foreach (var tier in Enum.GetValues()) { TimeSpan.FromSeconds(DriverResilienceOptions.GetTierDefaults(tier)[DriverCapability.Write].TimeoutSeconds) .ShouldBeLessThanOrEqualTo(bounds.Timeout, $"tier {tier}"); } } [Fact] public void The_configured_timeout_binds_and_every_bound_derives_from_it() { var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "00:00:12" }), out var warnings); warnings.ShouldBeEmpty(); bounds.Timeout.ShouldBe(TimeSpan.FromSeconds(12)); bounds.HostAsk.ShouldBe(TimeSpan.FromSeconds(12 + 2 + 12 + 2 + 1)); bounds.GatewayAsk.ShouldBe(bounds.HostAsk + DriverWriteBounds.ReplyMargin); } /// /// An inconsistent config is caught at startup: a write timeout below a tier's Write timeout means that tier's /// policy never takes effect, and each such tier is named. Tier A (2 s) fits under 3 s and is not. /// [Fact] public void A_timeout_below_a_tiers_Write_timeout_is_reported_per_tier() { var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "00:00:03" }), out var warnings); bounds.Timeout.ShouldBe(TimeSpan.FromSeconds(3), "an inconsistency is reported, not overridden"); warnings.Count.ShouldBe(2); warnings.ShouldContain(w => w.StartsWith("Tier B ", StringComparison.Ordinal) && w.Contains("(4s)")); warnings.ShouldContain(w => w.StartsWith("Tier C ", StringComparison.Ordinal) && w.Contains("(10s)")); warnings.ShouldNotContain(w => w.StartsWith("Tier A ", StringComparison.Ordinal)); } [Theory] [InlineData("not-a-timespan")] [InlineData("00:00:00")] [InlineData("-00:00:05")] public void An_unusable_value_falls_back_to_the_default_with_a_warning(string raw) { var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = raw }), out var warnings); bounds.ShouldBe(DriverWriteBounds.Default); warnings.Count.ShouldBe(1); warnings[0].ShouldContain("DriverWrites:Timeout"); } [Fact] public void A_value_above_the_max_is_clamped_with_a_warning() { var bounds = DriverWriteBoundsBinding.Bind(Config(new() { ["DriverWrites:Timeout"] = "01:00:00" }), out var warnings); bounds.Timeout.ShouldBe(DriverWriteBounds.MaxTimeout); warnings.Count.ShouldBe(1); warnings[0].ShouldContain("clamped"); } }