Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Resilience/InFlightCounterTests.cs
T
Joseph Doherty 64e3fbe035
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
docs: backfill XML documentation across 756 files
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.
2026-05-28 08:10:17 -04:00

139 lines
5.0 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 InFlightCounterTests
{
/// <summary>Verifies that starting and completing a call nets to zero.</summary>
[Fact]
public void StartThenComplete_NetsToZero()
{
var tracker = new DriverResilienceStatusTracker();
tracker.RecordCallStart("drv", "host-a");
tracker.RecordCallComplete("drv", "host-a");
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(0);
}
/// <summary>Verifies that nested starts sum the depth.</summary>
[Fact]
public void NestedStarts_SumDepth()
{
var tracker = new DriverResilienceStatusTracker();
tracker.RecordCallStart("drv", "host-a");
tracker.RecordCallStart("drv", "host-a");
tracker.RecordCallStart("drv", "host-a");
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(3);
tracker.RecordCallComplete("drv", "host-a");
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(2);
}
/// <summary>Verifies that completing before start is clamped to zero.</summary>
[Fact]
public void CompleteBeforeStart_ClampedToZero()
{
var tracker = new DriverResilienceStatusTracker();
tracker.RecordCallComplete("drv", "host-a");
// A stray Complete without a matching Start shouldn't drive the counter negative.
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(0);
}
/// <summary>Verifies that different hosts track independently.</summary>
[Fact]
public void DifferentHosts_TrackIndependently()
{
var tracker = new DriverResilienceStatusTracker();
tracker.RecordCallStart("drv", "host-a");
tracker.RecordCallStart("drv", "host-a");
tracker.RecordCallStart("drv", "host-b");
tracker.TryGet("drv", "host-a")!.CurrentInFlight.ShouldBe(2);
tracker.TryGet("drv", "host-b")!.CurrentInFlight.ShouldBe(1);
}
/// <summary>Verifies that concurrent starts do not lose count.</summary>
[Fact]
public void ConcurrentStarts_DoNotLose_Count()
{
var tracker = new DriverResilienceStatusTracker();
Parallel.For(0, 500, _ => tracker.RecordCallStart("drv", "host-a"));
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()
{
var tracker = new DriverResilienceStatusTracker();
var invoker = new CapabilityInvoker(
new DriverResiliencePipelineBuilder(),
"drv-live",
() => new DriverResilienceOptions { Tier = DriverTier.A },
driverType: "Modbus",
statusTracker: tracker);
var observedMidCall = -1;
await invoker.ExecuteAsync(
DriverCapability.Read,
"plc-1",
async _ =>
{
observedMidCall = tracker.TryGet("drv-live", "plc-1")?.CurrentInFlight ?? -1;
await Task.Yield();
return 42;
},
CancellationToken.None);
observedMidCall.ShouldBe(1, "during call, in-flight == 1");
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()
{
var tracker = new DriverResilienceStatusTracker();
var invoker = new CapabilityInvoker(
new DriverResiliencePipelineBuilder(),
"drv-live",
() => new DriverResilienceOptions { Tier = DriverTier.A },
statusTracker: tracker);
await Should.ThrowAsync<InvalidOperationException>(async () =>
await invoker.ExecuteAsync<int>(
DriverCapability.Write,
"plc-1",
_ => throw new InvalidOperationException("boom"),
CancellationToken.None));
tracker.TryGet("drv-live", "plc-1")!.CurrentInFlight.ShouldBe(0,
"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()
{
var invoker = new CapabilityInvoker(
new DriverResiliencePipelineBuilder(),
"drv-live",
() => new DriverResilienceOptions { Tier = DriverTier.A },
statusTracker: null);
var result = await invoker.ExecuteAsync(
DriverCapability.Read, "host-1",
_ => ValueTask.FromResult(7),
CancellationToken.None);
result.ShouldBe(7);
}
}