Merge R2-02 ResilienceConfig hardening (arch-review round 2) [PR #431]
Findings 01/S-6 (High — clamp hostile ResilienceConfig, no ValidationException driver-brick), 01/U-6 (delete dead bulkhead knob), 01/S-8=03/S12 (Write/Ack non-idempotent + retry->0), 04/C-7 (JsonObject round-trip preserves unknown keys), 01/S-7=03/S13 (per-Create options-generation kills respawn cache race). T15/T18 deferred. STATUS.md conflict resolved additively. Build clean.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
|
||||
@@ -6,6 +7,59 @@ using ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
||||
|
||||
public class ResilienceFormModelTests
|
||||
{
|
||||
// ---- 04/C-7: non-lossy round-trip ----
|
||||
|
||||
[Fact]
|
||||
public void Unknown_top_level_key_survives_round_trip()
|
||||
{
|
||||
// bulkheadMaxConcurrent doubles as the 01/U-6 continuity proof: the deleted knob's key survives.
|
||||
var json = """{"bulkheadMaxConcurrent":16,"capabilityPolicies":{"Read":{"timeoutSeconds":5}}}""";
|
||||
|
||||
var back = ResilienceFormModel.FromJson(json).ToJson();
|
||||
|
||||
back.ShouldNotBeNull();
|
||||
var obj = JsonNode.Parse(back)!.AsObject();
|
||||
obj["bulkheadMaxConcurrent"]!.GetValue<int>().ShouldBe(16);
|
||||
obj["capabilityPolicies"]!["Read"]!["timeoutSeconds"]!.GetValue<int>().ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_capability_entry_survives_round_trip()
|
||||
{
|
||||
var json = """{"capabilityPolicies":{"Read":{"timeoutSeconds":5},"FutureCap":{"timeoutSeconds":9}}}""";
|
||||
|
||||
var back = ResilienceFormModel.FromJson(json).ToJson();
|
||||
|
||||
back.ShouldNotBeNull();
|
||||
var obj = JsonNode.Parse(back)!.AsObject();
|
||||
obj["capabilityPolicies"]!["FutureCap"]!["timeoutSeconds"]!.GetValue<int>().ShouldBe(9);
|
||||
obj["capabilityPolicies"]!["Read"]!["timeoutSeconds"]!.GetValue<int>().ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_per_policy_field_survives_round_trip()
|
||||
{
|
||||
var json = """{"capabilityPolicies":{"Read":{"timeoutSeconds":5,"futureField":7}}}""";
|
||||
|
||||
var back = ResilienceFormModel.FromJson(json).ToJson();
|
||||
|
||||
back.ShouldNotBeNull();
|
||||
var read = JsonNode.Parse(back)!.AsObject()["capabilityPolicies"]!["Read"]!.AsObject();
|
||||
read["timeoutSeconds"]!.GetValue<int>().ShouldBe(5);
|
||||
read["futureField"]!.GetValue<int>().ShouldBe(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Malformed_json_sets_ParseFailed_and_ToJson_returns_original_text()
|
||||
{
|
||||
const string json = "{ not json";
|
||||
|
||||
var m = ResilienceFormModel.FromJson(json);
|
||||
|
||||
m.ParseFailed.ShouldBeTrue();
|
||||
m.ToJson().ShouldBe(json); // never overwrite what it couldn't read
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blank_form_serializes_to_null()
|
||||
=> new ResilienceFormModel().ToJson().ShouldBeNull();
|
||||
@@ -13,7 +67,7 @@ public class ResilienceFormModelTests
|
||||
[Fact]
|
||||
public void Partial_override_round_trips()
|
||||
{
|
||||
var m = new ResilienceFormModel { BulkheadMaxConcurrent = 16 };
|
||||
var m = new ResilienceFormModel { RecycleIntervalSeconds = 3600 };
|
||||
m.Policies["Read"].TimeoutSeconds = 5;
|
||||
m.Policies["Read"].RetryCount = 5;
|
||||
|
||||
@@ -21,7 +75,7 @@ public class ResilienceFormModelTests
|
||||
json.ShouldNotBeNull();
|
||||
|
||||
var back = ResilienceFormModel.FromJson(json);
|
||||
back.BulkheadMaxConcurrent.ShouldBe(16);
|
||||
back.RecycleIntervalSeconds.ShouldBe(3600);
|
||||
back.Policies["Read"].TimeoutSeconds.ShouldBe(5);
|
||||
back.Policies["Write"].IsEmpty.ShouldBeTrue();
|
||||
}
|
||||
@@ -30,19 +84,48 @@ public class ResilienceFormModelTests
|
||||
public void Malformed_json_yields_empty_model()
|
||||
{
|
||||
var m = ResilienceFormModel.FromJson("{ not valid json");
|
||||
m.BulkheadMaxConcurrent.ShouldBeNull();
|
||||
m.RecycleIntervalSeconds.ShouldBeNull();
|
||||
m.Policies["Read"].IsEmpty.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_flags_out_of_range_values_with_capability_and_range()
|
||||
{
|
||||
var m = new ResilienceFormModel();
|
||||
m.Policies["Read"].TimeoutSeconds = 0; // < MinTimeoutSeconds
|
||||
m.Policies["Read"].RetryCount = -1; // < MinRetryCount
|
||||
m.Policies["Read"].BreakerFailureThreshold = 1; // below Polly MinimumThroughput floor
|
||||
|
||||
var msgs = m.Validate();
|
||||
|
||||
msgs.Count.ShouldBe(3);
|
||||
msgs.ShouldContain(s => s.Contains("Read") && s.Contains("timeout"));
|
||||
msgs.ShouldContain(s => s.Contains("Read") && s.Contains("retries"));
|
||||
msgs.ShouldContain(s => s.Contains("Read") && s.Contains("breaker"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_passes_in_range_and_blank()
|
||||
{
|
||||
var blank = new ResilienceFormModel();
|
||||
blank.Validate().ShouldBeEmpty();
|
||||
|
||||
var inRange = new ResilienceFormModel();
|
||||
inRange.Policies["Read"].TimeoutSeconds = 5;
|
||||
inRange.Policies["Read"].RetryCount = 3;
|
||||
inRange.Policies["Read"].BreakerFailureThreshold = 4;
|
||||
inRange.Policies["Write"].BreakerFailureThreshold = 0; // 0 = breaker off, legal
|
||||
inRange.Validate().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Emitted_json_is_consumable_by_the_runtime_parser()
|
||||
{
|
||||
var m = new ResilienceFormModel { BulkheadMaxConcurrent = 16 };
|
||||
var m = new ResilienceFormModel();
|
||||
m.Policies["Read"].TimeoutSeconds = 7;
|
||||
|
||||
var opts = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.B, m.ToJson(), out var diag);
|
||||
diag.ShouldBeNull();
|
||||
opts.BulkheadMaxConcurrent.ShouldBe(16);
|
||||
opts.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(7);
|
||||
opts.Resolve(DriverCapability.Write).RetryCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user