Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/DriverResilienceOptionsTests.cs
T
Joseph Doherty cbb882293b refactor(drivers): delete the inert driver-tier recycle machinery (#522)
The tier system was documented, operator-authorable, and inert. Deleted rather than activated,
because its premise is gone rather than merely unused.

"Tier C" meant a driver running out-of-process behind an IDriverSupervisor that could restart its
Host without tearing down the OPC UA session. No such process exists anywhere: Galaxy reaches
MXAccess over gRPC to the external mxaccessgw sidecar (PR 7.2 retired the in-process
Galaxy.Host/Proxy/Shared projects) and FOCAS has run in-process since its managed wire client
landed 2026-04-24. Consistently, IDriverSupervisor had ZERO implementations and there was nothing
for one to implement against.

The issue understated the inertness. It says the Tier-C-only protections never engaged, implying
the Tier A/B parts did. They did not: nothing constructs MemoryTracking, MemoryRecycle or
ScheduledRecycleScheduler outside their own unit tests, so the whole Core/Stability recycle layer
was dead — meaning option (a), "pass real tiers", was never a flag flip. It would have meant
writing the wiring that never existed AND arming it.

Deleted: MemoryTracking, MemoryRecycle, ScheduledRecycleScheduler, IDriverSupervisor, the
vestigial DriverTypeRegistry (referenced only by its own tests), and the RecycleIntervalSeconds
knob — which the AdminUI let an operator author and the parser validated while it configured
nothing.

DriverTier itself SURVIVES and is load-bearing: DriverResilienceOptions.GetTierDefaults supplies
the real per-capability timeout/retry/breaker policies via DriverFactoryRegistry.GetTier and
DriverCapabilityInvokerFactory. Only the isolation-and-recycle layer above it is gone.

Deliberately NOT deleted:

- WedgeDetector came along in the same directory and is equally dead in production, but it is
  tier-agnostic and is not recycle machinery — it only shares the folder. Restored rather than
  swept up in a decision that was not about it.
- IDriver.GetMemoryFootprint() and FlushOptionalCachesAsync() lose their only consumer here.
  Removing them touches all 12 drivers and every test stub, so they are documented as
  consumerless and filed as #525 instead of buried in this diff.

Compatibility: a deployed ResilienceConfig blob still carrying "recycleIntervalSeconds" parses
cleanly (unknown keys are ignored — guarded by a new test, because a blob that suddenly failed to
parse would fall back to tier defaults and silently discard the operator's real overrides), and
the AdminUI's preserve-unknown-keys bag keeps the key rather than rewriting stored config on an
unrelated edit.

The 01/U-6 knob-inertness guard carried an explicit carve-out admitting RecycleIntervalSeconds was
dormant and out of scope; that carve-out is now gone, so the expected set is literally what the
test's name claims.

Note: Host.IntegrationTests has 2 failures (DriverProbeRegistrationTests.is_idempotent,
PrimaryGateFailoverTests) — verified pre-existing by reproducing both on clean master dc9d947b.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-07-30 04:44:04 -04:00

195 lines
8.8 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
[Trait("Category", "Unit")]
public sealed class DriverResilienceOptionsTests
{
/// <summary>
/// 01/U-6 knob-inertness guard (OVERALL theme #1): the public property set of
/// <see cref="DriverResilienceOptions"/> must be EXACTLY the pipeline-wired knobs. Every option a
/// parser can populate must map to a strategy the builder actually composes — a parsed-but-unapplied
/// knob (the bulkhead genre this pass deleted) is inertness the interface-forwarding and
/// unwrapped-dispatch guards can't catch. To add a knob: wire it in <c>DriverResiliencePipelineBuilder</c>,
/// add a behavior test that proves it engages, THEN add it to the expected set below.
/// <para><c>RecycleIntervalSeconds</c> used to sit in the expected set under an explicit carve-out —
/// Tier-C-dormant per 01/U-2, "a documented open question, out of this pass's scope", justified by
/// the recycle scheduler rather than the Polly pipeline being its consumer. Gitea #522 closed that
/// question by deleting the scheduler (it was constructed only in tests, and the
/// <c>IDriverSupervisor</c> that would perform the recycle had no implementations), so the knob went
/// with it and the carve-out is gone. The expected set below is now literally what the name of this
/// test claims.</para>
/// </summary>
[Fact]
public void Options_properties_are_exactly_the_pipeline_wired_set()
{
var expected = new[] { "Tier", "CapabilityPolicies" };
var actual = typeof(DriverResilienceOptions)
.GetProperties()
.Select(p => p.Name)
.OrderBy(n => n)
.ToArray();
actual.ShouldBe(expected.OrderBy(n => n).ToArray(),
"DriverResilienceOptions must expose only pipeline-wired knobs — a new option needs a builder " +
"strategy + a behavior test before it is added to the expected set (guards against the deleted " +
"bulkhead 'parsed-but-unapplied' genre; see 01/U-6).");
}
/// <summary>Verifies that tier defaults cover every capability.</summary>
/// <param name="tier">The driver tier to test.</param>
[Theory]
[InlineData(DriverTier.A)]
[InlineData(DriverTier.B)]
[InlineData(DriverTier.C)]
public void TierDefaults_Cover_EveryCapability(DriverTier tier)
{
var defaults = DriverResilienceOptions.GetTierDefaults(tier);
foreach (var capability in Enum.GetValues<DriverCapability>())
defaults.ShouldContainKey(capability);
}
/// <summary>Verifies that write never retries by default.</summary>
/// <param name="tier">The driver tier to test.</param>
[Theory]
[InlineData(DriverTier.A)]
[InlineData(DriverTier.B)]
[InlineData(DriverTier.C)]
public void Write_NeverRetries_ByDefault(DriverTier tier)
{
var defaults = DriverResilienceOptions.GetTierDefaults(tier);
defaults[DriverCapability.Write].RetryCount.ShouldBe(0);
}
/// <summary>Verifies that alarm acknowledge never retries by default.</summary>
/// <param name="tier">The driver tier to test.</param>
[Theory]
[InlineData(DriverTier.A)]
[InlineData(DriverTier.B)]
[InlineData(DriverTier.C)]
public void AlarmAcknowledge_NeverRetries_ByDefault(DriverTier tier)
{
var defaults = DriverResilienceOptions.GetTierDefaults(tier);
defaults[DriverCapability.AlarmAcknowledge].RetryCount.ShouldBe(0);
}
/// <summary>Verifies that idempotent capabilities retry by default.</summary>
/// <param name="tier">The driver tier to test.</param>
/// <param name="capability">The driver capability to test.</param>
[Theory]
[InlineData(DriverTier.A, DriverCapability.Read)]
[InlineData(DriverTier.A, DriverCapability.HistoryRead)]
[InlineData(DriverTier.B, DriverCapability.Discover)]
[InlineData(DriverTier.B, DriverCapability.Probe)]
[InlineData(DriverTier.C, DriverCapability.AlarmSubscribe)]
public void IdempotentCapabilities_Retry_ByDefault(DriverTier tier, DriverCapability capability)
{
var defaults = DriverResilienceOptions.GetTierDefaults(tier);
defaults[capability].RetryCount.ShouldBeGreaterThan(0);
}
/// <summary>Verifies that TierC disables circuit breaker deferring to supervisor.</summary>
[Fact]
public void TierC_DisablesCircuitBreaker_DeferringToSupervisor()
{
var defaults = DriverResilienceOptions.GetTierDefaults(DriverTier.C);
foreach (var (_, policy) in defaults)
policy.BreakerFailureThreshold.ShouldBe(0, "Tier C breaker is handled by the Proxy supervisor (decision #68)");
}
/// <summary>Verifies that TierA and TierB enable circuit breaker.</summary>
/// <param name="tier">The driver tier to test.</param>
[Theory]
[InlineData(DriverTier.A)]
[InlineData(DriverTier.B)]
public void TierAAndB_EnableCircuitBreaker(DriverTier tier)
{
var defaults = DriverResilienceOptions.GetTierDefaults(tier);
foreach (var (_, policy) in defaults)
policy.BreakerFailureThreshold.ShouldBeGreaterThan(0);
}
/// <summary>Verifies that resolve uses tier defaults when no override is set.</summary>
[Fact]
public void Resolve_Uses_TierDefaults_When_NoOverride()
{
var options = new DriverResilienceOptions { Tier = DriverTier.A };
var resolved = options.Resolve(DriverCapability.Read);
resolved.ShouldBe(DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
}
/// <summary>Verifies that resolve uses override when configured.</summary>
[Fact]
public void Resolve_Uses_Override_When_Configured()
{
var custom = new CapabilityPolicy(TimeoutSeconds: 42, RetryCount: 7, BreakerFailureThreshold: 9);
var options = new DriverResilienceOptions
{
Tier = DriverTier.A,
CapabilityPolicies = new Dictionary<DriverCapability, CapabilityPolicy>
{
[DriverCapability.Read] = custom,
},
};
options.Resolve(DriverCapability.Read).ShouldBe(custom);
options.Resolve(DriverCapability.Write).ShouldBe(
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Write]);
}
/// <summary>
/// Core-010 regression: every <see cref="DriverCapability"/> value must successfully resolve
/// under every tier with a default <see cref="DriverResilienceOptions"/>. A future
/// enum-only addition that forgets to update <c>GetTierDefaults</c> would otherwise blow up
/// on the hot path with <see cref="KeyNotFoundException"/>.
/// </summary>
/// <param name="tier">The driver tier to test.</param>
[Theory]
[InlineData(DriverTier.A)]
[InlineData(DriverTier.B)]
[InlineData(DriverTier.C)]
public void Resolve_Returns_NonNull_Policy_For_Every_Capability(DriverTier tier)
{
var options = new DriverResilienceOptions { Tier = tier };
foreach (var capability in Enum.GetValues<DriverCapability>())
{
var policy = options.Resolve(capability);
policy.ShouldNotBeNull(
$"every DriverCapability must resolve to a non-null policy for tier {tier} — {capability} did not");
}
}
/// <summary>
/// Core-010 regression: when a capability is somehow missing from BOTH the override
/// map and the tier defaults (defensive — should be impossible thanks to the
/// <c>TierDefaults_Cover_EveryCapability</c> invariant, but is the failure mode the
/// finding flagged), <c>Resolve</c> must throw a diagnostic <see cref="KeyNotFoundException"/>
/// that names the missing capability and tier — not a bare lookup failure.
/// </summary>
[Fact]
public void Resolve_Throws_Diagnostic_When_Capability_Missing_From_Tier_Defaults()
{
// Use a CapabilityPolicies dict that purposely omits one capability and use reflection
// to confirm the message names the capability when the tier defaults also omit it.
// We can't easily mutate GetTierDefaults so we exercise the documented behavior on a
// synthetic non-tier-known capability (we cast an out-of-range enum value).
var options = new DriverResilienceOptions { Tier = DriverTier.A };
var bogus = (DriverCapability)int.MaxValue;
var ex = Should.Throw<KeyNotFoundException>(() => options.Resolve(bogus));
ex.Message.ShouldContain(bogus.ToString());
ex.Message.ShouldContain(DriverTier.A.ToString());
ex.Message.ShouldContain(nameof(DriverResilienceOptions.GetTierDefaults));
}
}