Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ConnectionBackoffTests.cs
T

99 lines
4.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
/// <summary>
/// Covers <see cref="ConnectionBackoff"/> — the shared capped-exponential backoff extracted
/// from the S7 poll fork (05/STAB-8; the seam plan R2-01 wires into the S7 connect throttle).
/// Two surfaces: the static <see cref="ConnectionBackoff.ComputeDelay"/> schedule and the
/// per-device attempt-throttle instance.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ConnectionBackoffTests
{
private static readonly TimeSpan Base = TimeSpan.FromSeconds(1);
private static readonly TimeSpan Cap = TimeSpan.FromSeconds(30);
/// <summary>Zero (or negative) consecutive failures returns the base interval unchanged.</summary>
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void ComputeDelay_NoFailures_ReturnsBaseInterval(int failures)
=> ConnectionBackoff.ComputeDelay(Base, failures, Cap).ShouldBe(Base);
/// <summary>The delay doubles per consecutive failure (1×, 2×, 4×, 8×) until it saturates the cap.</summary>
[Theory]
[InlineData(1, 1)]
[InlineData(2, 2)]
[InlineData(3, 4)]
[InlineData(4, 8)]
[InlineData(5, 16)]
public void ComputeDelay_DoublesPerFailure(int failures, int expectedSeconds)
=> ConnectionBackoff.ComputeDelay(Base, failures, Cap)
.ShouldBe(TimeSpan.FromSeconds(expectedSeconds));
/// <summary>Growth saturates at the cap and never exceeds it, even at a large failure count (overflow guard).</summary>
[Theory]
[InlineData(6)] // 32s would exceed 30s cap
[InlineData(30)]
[InlineData(1000)] // shift saturates; ticks overflow guard returns cap
public void ComputeDelay_SaturatesAtCap(int failures)
=> ConnectionBackoff.ComputeDelay(Base, failures, Cap).ShouldBe(Cap);
/// <summary>A fresh throttle permits the first attempt immediately.</summary>
[Fact]
public void ShouldAttempt_FreshInstance_AllowsImmediately()
{
var backoff = new ConnectionBackoff(Base, Cap);
backoff.ShouldAttempt(DateTime.UtcNow).ShouldBeTrue();
}
/// <summary>After a failure the throttle blocks inside the backoff window and reopens once it elapses.</summary>
[Fact]
public void RecordFailure_BlocksWithinWindow_ReopensAfter()
{
var backoff = new ConnectionBackoff(Base, Cap);
var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
backoff.RecordFailure(t0); // window = 1s (first failure)
backoff.ShouldAttempt(t0).ShouldBeFalse(); // still inside window
backoff.ShouldAttempt(t0.AddMilliseconds(500)).ShouldBeFalse();
backoff.ShouldAttempt(t0.AddSeconds(1)).ShouldBeTrue(); // window elapsed
}
/// <summary>Consecutive failures widen the window (1s then 2s).</summary>
[Fact]
public void RecordFailure_ConsecutiveFailures_WidenWindow()
{
var backoff = new ConnectionBackoff(Base, Cap);
var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
backoff.RecordFailure(t0); // 1s
backoff.RecordFailure(t0.AddSeconds(1)); // 2nd failure → 2s window
backoff.ShouldAttempt(t0.AddSeconds(2)).ShouldBeFalse(); // within the 2s window
backoff.ShouldAttempt(t0.AddSeconds(3)).ShouldBeTrue();
}
/// <summary>Success resets immediately — recovery is never delayed by a residual window.</summary>
[Fact]
public void RecordSuccess_ResetsWindowImmediately()
{
var backoff = new ConnectionBackoff(Base, Cap);
var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
backoff.RecordFailure(t0);
backoff.RecordFailure(t0); // deep in a widened window
backoff.ShouldAttempt(t0).ShouldBeFalse();
backoff.RecordSuccess();
backoff.ShouldAttempt(t0).ShouldBeTrue(); // reset, no residual delay
// And the schedule restarts from the base window on the next failure.
backoff.RecordFailure(t0);
backoff.ShouldAttempt(t0.AddMilliseconds(999)).ShouldBeFalse();
backoff.ShouldAttempt(t0.AddSeconds(1)).ShouldBeTrue();
}
}