docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
This commit is contained in:
@@ -10,6 +10,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
{
|
||||
private static readonly DriverResilienceOptions TierAOptions = new() { Tier = DriverTier.A };
|
||||
|
||||
/// <summary>Verifies SubscribeAsync on an empty list returns empty without calling the driver.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_EmptyList_ReturnsEmpty_WithoutDriverCall()
|
||||
{
|
||||
@@ -22,6 +23,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
driver.SubscribeCallCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies SubscribeAsync with no resolver routes through the default host.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_SingleHost_RoutesThroughDefaultHost()
|
||||
{
|
||||
@@ -35,6 +37,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
driver.LastSubscribedIds.ShouldBe(["src-1", "src-2"]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies SubscribeAsync fans out correctly to multiple hosts based on resolver.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_MultiHost_FansOutByResolvedHost()
|
||||
{
|
||||
@@ -53,6 +56,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
driver.SubscribeCallCount.ShouldBe(2); // one driver call per host
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeAsync does not retry on failure.</summary>
|
||||
[Fact]
|
||||
public async Task AcknowledgeAsync_DoesNotRetry_OnFailure()
|
||||
{
|
||||
@@ -65,6 +69,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
driver.AcknowledgeCallCount.ShouldBe(1, "AlarmAcknowledge must not retry — decision #143");
|
||||
}
|
||||
|
||||
/// <summary>Verifies SubscribeAsync retries on transient failures.</summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_Retries_Transient_Failures()
|
||||
{
|
||||
@@ -106,6 +111,7 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
driver.UnsubscribeCallCount.ShouldBe(2, "one unsubscribe per subscription handle (per host)");
|
||||
}
|
||||
|
||||
/// <summary>Verifies UnsubscribeAsync with no resolver uses the default host.</summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_SingleHost_UsesDefaultHost()
|
||||
{
|
||||
@@ -131,15 +137,31 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
return new AlarmSurfaceInvoker(invoker, driver, defaultHost, resolver);
|
||||
}
|
||||
|
||||
/// <summary>Fake alarm source for testing.</summary>
|
||||
private sealed class FakeAlarmSource : IAlarmSource
|
||||
{
|
||||
/// <summary>Gets the number of times SubscribeAlarmsAsync was called.</summary>
|
||||
public int SubscribeCallCount { get; private set; }
|
||||
|
||||
/// <summary>Gets the number of times UnsubscribeAlarmsAsync was called.</summary>
|
||||
public int UnsubscribeCallCount { get; private set; }
|
||||
|
||||
/// <summary>Gets the number of times AcknowledgeAsync was called.</summary>
|
||||
public int AcknowledgeCallCount { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the number of failures before SubscribeAlarmsAsync succeeds.</summary>
|
||||
public int SubscribeFailuresBeforeSuccess { get; set; }
|
||||
|
||||
/// <summary>Gets or sets whether AcknowledgeAsync should throw.</summary>
|
||||
public bool AcknowledgeShouldThrow { get; set; }
|
||||
|
||||
/// <summary>Gets the source node IDs from the most recent SubscribeAlarmsAsync call.</summary>
|
||||
public IReadOnlyList<string> LastSubscribedIds { get; private set; } = [];
|
||||
|
||||
/// <summary>Subscribes to alarms.</summary>
|
||||
/// <param name="sourceNodeIds">The source node IDs to subscribe to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>An alarm subscription handle.</returns>
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -150,12 +172,20 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
return Task.FromResult<IAlarmSubscriptionHandle>(new StubHandle($"h-{SubscribeCallCount}"));
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from alarms.</summary>
|
||||
/// <param name="handle">The subscription handle to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
UnsubscribeCallCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Acknowledges alarms.</summary>
|
||||
/// <param name="acknowledgements">The alarm acknowledgements to process.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -164,13 +194,21 @@ public sealed class AlarmSurfaceInvokerTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Occurs when an alarm event is raised.</summary>
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent { add { } remove { } }
|
||||
}
|
||||
|
||||
/// <summary>Stub alarm subscription handle for testing.</summary>
|
||||
/// <param name="DiagnosticId">Diagnostic identifier for the handle.</param>
|
||||
private sealed record StubHandle(string DiagnosticId) : IAlarmSubscriptionHandle;
|
||||
|
||||
/// <summary>Stub host resolver for testing multi-host scenarios.</summary>
|
||||
/// <param name="map">The map of source node IDs to host names.</param>
|
||||
private sealed class StubResolver(Dictionary<string, string> map) : IPerCallHostResolver
|
||||
{
|
||||
/// <summary>Resolves the host for the given full reference.</summary>
|
||||
/// <param name="fullReference">The full reference to resolve.</param>
|
||||
/// <returns>The resolved host name.</returns>
|
||||
public string ResolveHost(string fullReference) => map[fullReference];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public sealed class CapabilityInvokerTests
|
||||
DriverResilienceOptions options) =>
|
||||
new(builder, "drv-test", () => options);
|
||||
|
||||
/// <summary>Verifies that the capability invoker returns the value from the call site.</summary>
|
||||
[Fact]
|
||||
public async Task Read_ReturnsValue_FromCallSite()
|
||||
{
|
||||
@@ -27,6 +28,7 @@ public sealed class CapabilityInvokerTests
|
||||
result.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the capability invoker retries on transient failures.</summary>
|
||||
[Fact]
|
||||
public async Task Read_Retries_OnTransientFailure()
|
||||
{
|
||||
@@ -49,6 +51,7 @@ public sealed class CapabilityInvokerTests
|
||||
attempts.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that non-idempotent writes do not retry even when the policy has retries configured.</summary>
|
||||
[Fact]
|
||||
public async Task Write_NonIdempotent_DoesNotRetry_EvenWhenPolicyHasRetries()
|
||||
{
|
||||
@@ -81,6 +84,7 @@ public sealed class CapabilityInvokerTests
|
||||
attempts.ShouldBe(1, "non-idempotent write must never replay");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that idempotent writes retry when the policy has retries configured.</summary>
|
||||
[Fact]
|
||||
public async Task Write_Idempotent_Retries_WhenPolicyHasRetries()
|
||||
{
|
||||
@@ -111,6 +115,7 @@ public sealed class CapabilityInvokerTests
|
||||
attempts.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that writes do not retry when the policy has zero retries configured.</summary>
|
||||
[Fact]
|
||||
public async Task Write_Default_DoesNotRetry_WhenPolicyHasZeroRetries()
|
||||
{
|
||||
@@ -137,6 +142,7 @@ public sealed class CapabilityInvokerTests
|
||||
attempts.ShouldBe(1, "tier-A default for Write is RetryCount=0");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different hosts are honored independently in the resilience pipeline.</summary>
|
||||
[Fact]
|
||||
public async Task Execute_HonorsDifferentHosts_Independently()
|
||||
{
|
||||
|
||||
+17
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DriverResilienceOptionsParserTests
|
||||
{
|
||||
/// <summary>Verifies that null JSON returns pure tier defaults.</summary>
|
||||
[Fact]
|
||||
public void NullJson_ReturnsPureTierDefaults()
|
||||
{
|
||||
@@ -19,6 +20,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that whitespace JSON returns defaults.</summary>
|
||||
[Fact]
|
||||
public void WhitespaceJson_ReturnsDefaults()
|
||||
{
|
||||
@@ -26,6 +28,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
diag.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that malformed JSON falls back with diagnostic.</summary>
|
||||
[Fact]
|
||||
public void MalformedJson_FallsBack_WithDiagnostic()
|
||||
{
|
||||
@@ -38,6 +41,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty object returns defaults.</summary>
|
||||
[Fact]
|
||||
public void EmptyObject_ReturnsDefaults()
|
||||
{
|
||||
@@ -48,6 +52,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Write]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Read override is merged into tier defaults.</summary>
|
||||
[Fact]
|
||||
public void ReadOverride_MergedIntoTierDefaults()
|
||||
{
|
||||
@@ -72,6 +77,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Write]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that partial policy fills missing fields from tier default.</summary>
|
||||
[Fact]
|
||||
public void PartialPolicy_FillsMissingFieldsFromTierDefault()
|
||||
{
|
||||
@@ -92,6 +98,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
read.BreakerFailureThreshold.ShouldBe(tierDefault.BreakerFailureThreshold);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that bulkhead overrides are honored.</summary>
|
||||
[Fact]
|
||||
public void BulkheadOverrides_AreHonored()
|
||||
{
|
||||
@@ -105,6 +112,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
options.BulkheadMaxQueue.ShouldBe(500);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that unknown capability surfaces in diagnostic but does not fail.</summary>
|
||||
[Fact]
|
||||
public void UnknownCapability_Surfaces_InDiagnostic_ButDoesNotFail()
|
||||
{
|
||||
@@ -125,6 +133,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
DriverResilienceOptions.GetTierDefaults(DriverTier.A)[DriverCapability.Read]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that property names are case insensitive.</summary>
|
||||
[Fact]
|
||||
public void PropertyNames_AreCaseInsensitive()
|
||||
{
|
||||
@@ -137,6 +146,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
options.BulkheadMaxConcurrent.ShouldBe(42);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that capability name is case insensitive.</summary>
|
||||
[Fact]
|
||||
public void CapabilityName_IsCaseInsensitive()
|
||||
{
|
||||
@@ -150,6 +160,8 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
options.Resolve(DriverCapability.Read).RetryCount.ShouldBe(99);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that every tier with empty JSON round-trips its defaults.</summary>
|
||||
/// <param name="tier">The driver tier to test.</param>
|
||||
[Theory]
|
||||
[InlineData(DriverTier.A)]
|
||||
[InlineData(DriverTier.B)]
|
||||
@@ -164,6 +176,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
options.Resolve(cap).ShouldBe(DriverResilienceOptions.GetTierDefaults(tier)[cap]);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecycleIntervalSeconds on Tier C with positive value parses and surfaces.</summary>
|
||||
[Fact]
|
||||
public void RecycleIntervalSeconds_TierC_PositiveValue_ParsesAndSurfaces()
|
||||
{
|
||||
@@ -174,6 +187,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
options.RecycleIntervalSeconds.ShouldBe(3600);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecycleIntervalSeconds when null defaults to null.</summary>
|
||||
[Fact]
|
||||
public void RecycleIntervalSeconds_Null_DefaultsToNull()
|
||||
{
|
||||
@@ -181,6 +195,8 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
options.RecycleIntervalSeconds.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecycleIntervalSeconds on Tier A or B is rejected with diagnostic.</summary>
|
||||
/// <param name="tier">The driver tier to test.</param>
|
||||
[Theory]
|
||||
[InlineData(DriverTier.A)]
|
||||
[InlineData(DriverTier.B)]
|
||||
@@ -196,6 +212,7 @@ public sealed class DriverResilienceOptionsParserTests
|
||||
diag.ShouldContain("Tier C only");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecycleIntervalSeconds with non-positive value is rejected with diagnostic.</summary>
|
||||
[Fact]
|
||||
public void RecycleIntervalSeconds_NonPositive_Rejected_With_Diagnostic()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DriverResilienceOptionsTests
|
||||
{
|
||||
/// <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)]
|
||||
@@ -20,6 +22,8 @@ public sealed class DriverResilienceOptionsTests
|
||||
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)]
|
||||
@@ -30,6 +34,8 @@ public sealed class DriverResilienceOptionsTests
|
||||
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)]
|
||||
@@ -40,6 +46,9 @@ public sealed class DriverResilienceOptionsTests
|
||||
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)]
|
||||
@@ -52,6 +61,7 @@ public sealed class DriverResilienceOptionsTests
|
||||
defaults[capability].RetryCount.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that TierC disables circuit breaker deferring to supervisor.</summary>
|
||||
[Fact]
|
||||
public void TierC_DisablesCircuitBreaker_DeferringToSupervisor()
|
||||
{
|
||||
@@ -61,6 +71,8 @@ public sealed class DriverResilienceOptionsTests
|
||||
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)]
|
||||
@@ -72,6 +84,7 @@ public sealed class DriverResilienceOptionsTests
|
||||
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()
|
||||
{
|
||||
@@ -82,6 +95,7 @@ public sealed class DriverResilienceOptionsTests
|
||||
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()
|
||||
{
|
||||
@@ -106,6 +120,7 @@ public sealed class DriverResilienceOptionsTests
|
||||
/// 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)]
|
||||
|
||||
+14
@@ -12,6 +12,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
{
|
||||
private static readonly DriverResilienceOptions TierAOptions = new() { Tier = DriverTier.A };
|
||||
|
||||
/// <summary>Verifies that read operations retry transient failures.</summary>
|
||||
[Fact]
|
||||
public async Task Read_Retries_Transient_Failures()
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
attempts.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that write operations do not retry on failure.</summary>
|
||||
[Fact]
|
||||
public async Task Write_DoesNotRetry_OnFailure()
|
||||
{
|
||||
@@ -50,6 +52,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
ex.Message.ShouldBe("boom");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that alarm acknowledge operations do not retry on failure.</summary>
|
||||
[Fact]
|
||||
public async Task AlarmAcknowledge_DoesNotRetry_OnFailure()
|
||||
{
|
||||
@@ -70,6 +73,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
attempts.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pipelines are isolated per host.</summary>
|
||||
[Fact]
|
||||
public void Pipeline_IsIsolated_PerHost()
|
||||
{
|
||||
@@ -83,6 +87,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
builder.CachedPipelineCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pipelines are reused for the same driver, host, and capability triple.</summary>
|
||||
[Fact]
|
||||
public void Pipeline_IsReused_ForSameTriple()
|
||||
{
|
||||
@@ -96,6 +101,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
builder.CachedPipelineCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pipelines are isolated per capability.</summary>
|
||||
[Fact]
|
||||
public void Pipeline_IsIsolated_PerCapability()
|
||||
{
|
||||
@@ -108,6 +114,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
read.ShouldNotBeSameAs(write);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dead host does not open the breaker for a sibling host.</summary>
|
||||
[Fact]
|
||||
public async Task DeadHost_DoesNotOpenBreaker_ForSiblingHost()
|
||||
{
|
||||
@@ -138,6 +145,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
liveAttempts.ShouldBe(1, "healthy sibling host must not be affected by dead peer");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the circuit breaker opens after the failure threshold on tier A.</summary>
|
||||
[Fact]
|
||||
public async Task CircuitBreaker_Opens_AfterFailureThreshold_OnTierA()
|
||||
{
|
||||
@@ -162,6 +170,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that timeout cancels slow operations.</summary>
|
||||
[Fact]
|
||||
public async Task Timeout_Cancels_SlowOperation()
|
||||
{
|
||||
@@ -183,6 +192,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that invalidate removes only the matching instance.</summary>
|
||||
[Fact]
|
||||
public void Invalidate_Removes_OnlyMatchingInstance()
|
||||
{
|
||||
@@ -200,6 +210,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
builder.CachedPipelineCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that cancellation is not retried.</summary>
|
||||
[Fact]
|
||||
public async Task Cancellation_IsNot_Retried()
|
||||
{
|
||||
@@ -220,6 +231,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
attempts.ShouldBeLessThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the tracker records failure on every retry.</summary>
|
||||
[Fact]
|
||||
public async Task Tracker_RecordsFailure_OnEveryRetry()
|
||||
{
|
||||
@@ -240,6 +252,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
snap!.ConsecutiveFailures.ShouldBe(retryCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the tracker stamps the breaker open when it trips.</summary>
|
||||
[Fact]
|
||||
public async Task Tracker_StampsBreakerOpen_WhenBreakerTrips()
|
||||
{
|
||||
@@ -263,6 +276,7 @@ public sealed class DriverResiliencePipelineBuilderTests
|
||||
snap!.LastBreakerOpenUtc.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the tracker isolates counters per host.</summary>
|
||||
[Fact]
|
||||
public async Task Tracker_IsolatesCounters_PerHost()
|
||||
{
|
||||
|
||||
+9
@@ -9,6 +9,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
{
|
||||
private static readonly DateTime Now = new(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>Verifies that TryGet returns null before any write operations.</summary>
|
||||
[Fact]
|
||||
public void TryGet_Returns_Null_Before_AnyWrite()
|
||||
{
|
||||
@@ -17,6 +18,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
tracker.TryGet("drv", "host").ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecordFailure accumulates consecutive failures.</summary>
|
||||
[Fact]
|
||||
public void RecordFailure_Accumulates_ConsecutiveFailures()
|
||||
{
|
||||
@@ -29,6 +31,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
tracker.TryGet("drv", "host")!.ConsecutiveFailures.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecordSuccess resets consecutive failures to zero.</summary>
|
||||
[Fact]
|
||||
public void RecordSuccess_Resets_ConsecutiveFailures()
|
||||
{
|
||||
@@ -41,6 +44,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
tracker.TryGet("drv", "host")!.ConsecutiveFailures.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecordBreakerOpen populates the LastBreakerOpenUtc timestamp.</summary>
|
||||
[Fact]
|
||||
public void RecordBreakerOpen_Populates_LastBreakerOpenUtc()
|
||||
{
|
||||
@@ -51,6 +55,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
tracker.TryGet("drv", "host")!.LastBreakerOpenUtc.ShouldBe(Now);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecordRecycle populates the LastRecycleUtc timestamp.</summary>
|
||||
[Fact]
|
||||
public void RecordRecycle_Populates_LastRecycleUtc()
|
||||
{
|
||||
@@ -61,6 +66,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
tracker.TryGet("drv", "host")!.LastRecycleUtc.ShouldBe(Now);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RecordFootprint captures baseline and current memory usage.</summary>
|
||||
[Fact]
|
||||
public void RecordFootprint_CapturesBaselineAndCurrent()
|
||||
{
|
||||
@@ -73,6 +79,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
snap.CurrentFootprintBytes.ShouldBe(150_000_000);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different hosts are tracked independently.</summary>
|
||||
[Fact]
|
||||
public void DifferentHosts_AreIndependent()
|
||||
{
|
||||
@@ -86,6 +93,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
tracker.TryGet("drv", "host-b")!.ConsecutiveFailures.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Snapshot returns all tracked driver-host pairs.</summary>
|
||||
[Fact]
|
||||
public void Snapshot_ReturnsAll_TrackedPairs()
|
||||
{
|
||||
@@ -99,6 +107,7 @@ public sealed class DriverResilienceStatusTrackerTests
|
||||
snapshot.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent writes do not lose failure records.</summary>
|
||||
[Fact]
|
||||
public void ConcurrentWrites_DoNotLose_Failures()
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class FlakeyDriverIntegrationTests
|
||||
{
|
||||
/// <summary>Verifies read succeeds after transient failures with retries.</summary>
|
||||
[Fact]
|
||||
public async Task Read_SurfacesSuccess_AfterTransientFailures()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
result[0].StatusCode.ShouldBe(0u);
|
||||
}
|
||||
|
||||
/// <summary>Verifies non-idempotent write fails on first failure without replay.</summary>
|
||||
[Fact]
|
||||
public async Task Write_NonIdempotent_FailsOnFirstFailure_NoReplay()
|
||||
{
|
||||
@@ -65,6 +67,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
flaky.WriteAttempts.ShouldBe(1, "non-idempotent write must never replay (decision #44)");
|
||||
}
|
||||
|
||||
/// <summary>Verifies idempotent write retries until success.</summary>
|
||||
[Fact]
|
||||
public async Task Write_Idempotent_RetriesUntilSuccess()
|
||||
{
|
||||
@@ -89,6 +92,7 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
results[0].StatusCode.ShouldBe(0u);
|
||||
}
|
||||
|
||||
/// <summary>Verifies multiple hosts have independent failure counts and circuit breakers.</summary>
|
||||
[Fact]
|
||||
public async Task MultipleHosts_OnOneDriver_HaveIndependentFailureCounts()
|
||||
{
|
||||
@@ -116,20 +120,31 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
liveAttempts.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Driver that fails reads/writes for a configurable number of attempts.</summary>
|
||||
private sealed class FlakeyDriver : IReadable, IWritable
|
||||
{
|
||||
private readonly int _failReadsBeforeIndex;
|
||||
private readonly int _failWritesBeforeIndex;
|
||||
|
||||
/// <summary>Gets the number of read attempts made.</summary>
|
||||
public int ReadAttempts { get; private set; }
|
||||
|
||||
/// <summary>Gets the number of write attempts made.</summary>
|
||||
public int WriteAttempts { get; private set; }
|
||||
|
||||
/// <summary>Initializes a flaky driver with configurable failure counts.</summary>
|
||||
/// <param name="failReadsBeforeIndex">Fail reads until this attempt number.</param>
|
||||
/// <param name="failWritesBeforeIndex">Fail writes until this attempt number.</param>
|
||||
public FlakeyDriver(int failReadsBeforeIndex = 0, int failWritesBeforeIndex = 0)
|
||||
{
|
||||
_failReadsBeforeIndex = failReadsBeforeIndex;
|
||||
_failWritesBeforeIndex = failWritesBeforeIndex;
|
||||
}
|
||||
|
||||
/// <summary>Reads values, failing transiently until the threshold.</summary>
|
||||
/// <param name="fullReferences">Full references to read.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Data value snapshots.</returns>
|
||||
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -145,6 +160,10 @@ public sealed class FlakeyDriverIntegrationTests
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>Writes values, failing transiently until the threshold.</summary>
|
||||
/// <param name="writes">The write requests.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Write results.</returns>
|
||||
public Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Resilience;
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class InFlightCounterTests
|
||||
{
|
||||
/// <summary>Verifies that starting and completing a call nets to zero.</summary>
|
||||
[Fact]
|
||||
public void StartThenComplete_NetsToZero()
|
||||
{
|
||||
@@ -18,6 +19,7 @@ public sealed class InFlightCounterTests
|
||||
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that nested starts sum the depth.</summary>
|
||||
[Fact]
|
||||
public void NestedStarts_SumDepth()
|
||||
{
|
||||
@@ -32,6 +34,7 @@ public sealed class InFlightCounterTests
|
||||
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that completing before start is clamped to zero.</summary>
|
||||
[Fact]
|
||||
public void CompleteBeforeStart_ClampedToZero()
|
||||
{
|
||||
@@ -42,6 +45,7 @@ public sealed class InFlightCounterTests
|
||||
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that different hosts track independently.</summary>
|
||||
[Fact]
|
||||
public void DifferentHosts_TrackIndependently()
|
||||
{
|
||||
@@ -54,6 +58,7 @@ public sealed class InFlightCounterTests
|
||||
tracker.TryGet("drv", "host-b")!.CurrentInFlight.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent starts do not lose count.</summary>
|
||||
[Fact]
|
||||
public void ConcurrentStarts_DoNotLose_Count()
|
||||
{
|
||||
@@ -63,6 +68,7 @@ public sealed class InFlightCounterTests
|
||||
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(500);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CapabilityInvoker increments the tracker during execution.</summary>
|
||||
[Fact]
|
||||
public async Task CapabilityInvoker_IncrementsTracker_DuringExecution()
|
||||
{
|
||||
@@ -90,6 +96,7 @@ public sealed class InFlightCounterTests
|
||||
tracker.TryGet("drv-live", "plc-1")!.CurrentInFlight.ShouldBe(0, "post-call, counter decremented");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CapabilityInvoker decrements the counter on exception.</summary>
|
||||
[Fact]
|
||||
public async Task CapabilityInvoker_ExceptionPath_DecrementsCounter()
|
||||
{
|
||||
@@ -111,6 +118,7 @@ public sealed class InFlightCounterTests
|
||||
"finally-block must decrement even when call-site throws");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CapabilityInvoker without a tracker does not throw.</summary>
|
||||
[Fact]
|
||||
public async Task CapabilityInvoker_WithoutTracker_DoesNotThrow()
|
||||
{
|
||||
|
||||
+10
@@ -17,11 +17,18 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
private sealed class StaticResolver : IPerCallHostResolver
|
||||
{
|
||||
private readonly Dictionary<string, string> _map;
|
||||
|
||||
/// <summary>Initializes a new instance of StaticResolver with a predefined mapping.</summary>
|
||||
/// <param name="map">The mapping of full references to host names.</param>
|
||||
public StaticResolver(Dictionary<string, string> map) => _map = map;
|
||||
|
||||
/// <summary>Resolves a host name from the static mapping.</summary>
|
||||
/// <param name="fullReference">The full reference to resolve.</param>
|
||||
public string ResolveHost(string fullReference) =>
|
||||
_map.TryGetValue(fullReference, out var host) ? host : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a dead PLC does not open the breaker for healthy PLCs when using a per-call resolver.</summary>
|
||||
[Fact]
|
||||
public async Task DeadPlc_DoesNotOpenBreaker_For_HealthyPlc_With_Resolver()
|
||||
{
|
||||
@@ -59,6 +66,7 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
aliveAttempts.ShouldBe(1, "decision #144 — per-PLC isolation keeps healthy PLCs serving");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that empty string from resolver is treated as single-host fallback.</summary>
|
||||
[Fact]
|
||||
public void Resolver_EmptyString_Treated_As_Single_Host_Fallback()
|
||||
{
|
||||
@@ -71,6 +79,7 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
resolver.ResolveHost("not-in-map").ShouldBe("", "unknown refs return empty so dispatch falls back to single-host");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that without a resolver, the same host shares one resilience pipeline.</summary>
|
||||
[Fact]
|
||||
public async Task WithoutResolver_SameHost_Shares_One_Pipeline()
|
||||
{
|
||||
@@ -88,6 +97,7 @@ public sealed class PerCallHostResolverDispatchTests
|
||||
builder.CachedPipelineCount.ShouldBe(1, "single-host drivers share one pipeline");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with a resolver, different hosts get separate resilience pipelines.</summary>
|
||||
[Fact]
|
||||
public async Task WithResolver_TwoHosts_Get_Two_Pipelines()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user