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
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.
119 lines
5.1 KiB
C#
119 lines
5.1 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Stability;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Stability;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ScheduledRecycleSchedulerTests
|
|
{
|
|
private static readonly DateTime T0 = new(2026, 4, 19, 0, 0, 0, DateTimeKind.Utc);
|
|
private static readonly TimeSpan Weekly = TimeSpan.FromDays(7);
|
|
|
|
/// <summary>Verifies constructor throws for Tier A or B.</summary>
|
|
/// <param name="tier">The driver tier to test.</param>
|
|
[Theory]
|
|
[InlineData(DriverTier.A)]
|
|
[InlineData(DriverTier.B)]
|
|
public void TierAOrB_Ctor_Throws(DriverTier tier)
|
|
{
|
|
var supervisor = new FakeSupervisor();
|
|
Should.Throw<ArgumentException>(() => new ScheduledRecycleScheduler(
|
|
tier, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance));
|
|
}
|
|
|
|
/// <summary>Verifies constructor throws for zero or negative intervals.</summary>
|
|
[Fact]
|
|
public void ZeroOrNegativeInterval_Throws()
|
|
{
|
|
var supervisor = new FakeSupervisor();
|
|
Should.Throw<ArgumentException>(() => new ScheduledRecycleScheduler(
|
|
DriverTier.C, TimeSpan.Zero, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance));
|
|
Should.Throw<ArgumentException>(() => new ScheduledRecycleScheduler(
|
|
DriverTier.C, TimeSpan.FromSeconds(-1), T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance));
|
|
}
|
|
|
|
/// <summary>Verifies Tick before the next recycle time is a no-op.</summary>
|
|
[Fact]
|
|
public async Task Tick_BeforeNextRecycle_NoOp()
|
|
{
|
|
var supervisor = new FakeSupervisor();
|
|
var sch = new ScheduledRecycleScheduler(DriverTier.C, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);
|
|
|
|
var fired = await sch.TickAsync(T0 + TimeSpan.FromDays(6), CancellationToken.None);
|
|
|
|
fired.ShouldBeFalse();
|
|
supervisor.RecycleCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies Tick at or after the next recycle time fires once and advances.</summary>
|
|
[Fact]
|
|
public async Task Tick_AtOrAfterNextRecycle_FiresOnce_AndAdvances()
|
|
{
|
|
var supervisor = new FakeSupervisor();
|
|
var sch = new ScheduledRecycleScheduler(DriverTier.C, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);
|
|
|
|
var fired = await sch.TickAsync(T0 + Weekly + TimeSpan.FromMinutes(1), CancellationToken.None);
|
|
|
|
fired.ShouldBeTrue();
|
|
supervisor.RecycleCount.ShouldBe(1);
|
|
sch.NextRecycleUtc.ShouldBe(T0 + Weekly + Weekly);
|
|
}
|
|
|
|
/// <summary>Verifies RequestRecycleNow fires immediately without advancing the schedule.</summary>
|
|
[Fact]
|
|
public async Task RequestRecycleNow_Fires_Immediately_WithoutAdvancingSchedule()
|
|
{
|
|
var supervisor = new FakeSupervisor();
|
|
var sch = new ScheduledRecycleScheduler(DriverTier.C, Weekly, T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);
|
|
var nextBefore = sch.NextRecycleUtc;
|
|
|
|
await sch.RequestRecycleNowAsync("memory hard-breach", CancellationToken.None);
|
|
|
|
supervisor.RecycleCount.ShouldBe(1);
|
|
supervisor.LastReason.ShouldBe("memory hard-breach");
|
|
sch.NextRecycleUtc.ShouldBe(nextBefore, "ad-hoc recycle doesn't shift the cron schedule");
|
|
}
|
|
|
|
/// <summary>Verifies multiple ticks across the recycle interval each advance by one interval.</summary>
|
|
[Fact]
|
|
public async Task MultipleFires_AcrossTicks_AdvanceOneIntervalEach()
|
|
{
|
|
var supervisor = new FakeSupervisor();
|
|
var sch = new ScheduledRecycleScheduler(DriverTier.C, TimeSpan.FromDays(1), T0, supervisor, NullLogger<ScheduledRecycleScheduler>.Instance);
|
|
|
|
await sch.TickAsync(T0 + TimeSpan.FromDays(1) + TimeSpan.FromHours(1), CancellationToken.None);
|
|
await sch.TickAsync(T0 + TimeSpan.FromDays(2) + TimeSpan.FromHours(1), CancellationToken.None);
|
|
await sch.TickAsync(T0 + TimeSpan.FromDays(3) + TimeSpan.FromHours(1), CancellationToken.None);
|
|
|
|
supervisor.RecycleCount.ShouldBe(3);
|
|
sch.NextRecycleUtc.ShouldBe(T0 + TimeSpan.FromDays(4));
|
|
}
|
|
|
|
/// <summary>Fake driver supervisor for testing.</summary>
|
|
private sealed class FakeSupervisor : IDriverSupervisor
|
|
{
|
|
/// <summary>Gets the driver instance ID.</summary>
|
|
public string DriverInstanceId => "tier-c-fake";
|
|
|
|
/// <summary>Gets the number of times RecycleAsync was called.</summary>
|
|
public int RecycleCount { get; private set; }
|
|
|
|
/// <summary>Gets the reason from the most recent recycle call.</summary>
|
|
public string? LastReason { get; private set; }
|
|
|
|
/// <summary>Simulates a driver recycle operation.</summary>
|
|
/// <param name="reason">The reason for the recycle.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>A completed task.</returns>
|
|
public Task RecycleAsync(string reason, CancellationToken cancellationToken)
|
|
{
|
|
RecycleCount++;
|
|
LastReason = reason;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
}
|