Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Source/Testably.Abstractions.Testing/MockTimeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public MockTimeSystem(ITimeProviderFactory timeProvider, Func<MockTimeSystemOpti
#if FEATURE_PERIODIC_TIMER
_periodicTimerFactoryMock = new PeriodicTimerFactoryMock(this, _callbackHandler, initialization.AutoAdvance);
#endif
_timerFactoryMock = new TimerFactoryMock(this);
_timerFactoryMock = new TimerFactoryMock(this, initialization.AutoAdvance);
}

#region ITimeSystem Members
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ namespace Testably.Abstractions.Testing.TimeSystem;
internal sealed class TimerFactoryMock : ITimerFactory, ITimerHandler
{
private readonly MockTimeSystem _mockTimeSystem;
private readonly bool _autoAdvance;
private ITimerStrategy _timerStrategy;
private readonly ConcurrentDictionary<int, TimerMock> _timers = new();
private int _nextIndex = -1;

internal TimerFactoryMock(MockTimeSystem timeSystem)
internal TimerFactoryMock(MockTimeSystem timeSystem, bool autoAdvance)
{
_mockTimeSystem = timeSystem;
_autoAdvance = autoAdvance;
_timerStrategy = TimerStrategy.Default;
}

Expand All @@ -38,31 +40,31 @@ public ITimeSystem TimeSystem
public ITimer New(TimerCallback callback)
{
TimerMock timerMock = new(_mockTimeSystem, _timerStrategy,
callback, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
callback, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, _autoAdvance);
return RegisterTimerMock(timerMock);
}

/// <inheritdoc cref="ITimerFactory.New(TimerCallback, object?, int, int)" />
public ITimer New(TimerCallback callback, object? state, int dueTime, int period)
{
TimerMock timerMock = new(_mockTimeSystem, _timerStrategy,
callback, state, TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period));
callback, state, TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period), _autoAdvance);
return RegisterTimerMock(timerMock);
}

/// <inheritdoc cref="ITimerFactory.New(TimerCallback, object?, long, long)" />
public ITimer New(TimerCallback callback, object? state, long dueTime, long period)
{
TimerMock timerMock = new(_mockTimeSystem, _timerStrategy,
callback, state, TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period));
callback, state, TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period), _autoAdvance);
return RegisterTimerMock(timerMock);
}

/// <inheritdoc cref="ITimerFactory.New(TimerCallback, object?, TimeSpan, TimeSpan)" />
public ITimer New(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
TimerMock timerMock = new(_mockTimeSystem, _timerStrategy,
callback, state, dueTime, period);
callback, state, dueTime, period, _autoAdvance);
return RegisterTimerMock(timerMock);
}

Expand Down
56 changes: 47 additions & 9 deletions Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal sealed class TimerMock : ITimerMock
private readonly MockTimeSystem _mockTimeSystem;
private Action? _onDispose;
private TimeSpan _period;
private readonly bool _autoAdvance;
private readonly object? _state;
private readonly ITimerStrategy _timerStrategy;

Expand All @@ -32,7 +33,8 @@ internal TimerMock(MockTimeSystem timeSystem,
TimerCallback callback,
object? state,
TimeSpan dueTime,
TimeSpan period)
TimeSpan period,
bool autoAdvance)
{
if (dueTime.TotalMilliseconds < -1)
{
Expand All @@ -50,6 +52,7 @@ internal TimerMock(MockTimeSystem timeSystem,
_state = state;
_dueTime = dueTime;
_period = period;
_autoAdvance = autoAdvance;
if (_timerStrategy.Mode == TimerMode.StartImmediately)
{
Start();
Expand Down Expand Up @@ -222,18 +225,22 @@ internal void RegisterOnDispose(Action? onDispose)
_onDispose = onDispose;
}

#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
private async Task RunTimer(CancellationToken cancellationToken = default)
{
await _mockTimeSystem.Task.Delay(_dueTime, cancellationToken).ConfigureAwait(false);
long nextPlannedExecution = _mockTimeSystem.TimeProvider.ElapsedTicks + _dueTime.Ticks;

if (_dueTime.TotalMilliseconds < 0)
{
cancellationToken.WaitHandle.WaitOne(_dueTime);
}
else
{
await WaitUntil(nextPlannedExecution).ConfigureAwait(false);
}

long nextPlannedExecution = _mockTimeSystem.TimeProvider.ElapsedTicks;
while (!cancellationToken.IsCancellationRequested)
{
nextPlannedExecution += _period.Ticks;
try
{
_callback(_state);
Expand All @@ -260,15 +267,46 @@ private async Task RunTimer(CancellationToken cancellationToken = default)
return;
}

long nowTicks = _mockTimeSystem.TimeProvider.ElapsedTicks;
if (nextPlannedExecution > nowTicks)
nextPlannedExecution += _period.Ticks;
await WaitUntil(nextPlannedExecution).ConfigureAwait(false);
}

async Task WaitUntil(long targetTicks)
{
if (_autoAdvance)
{
await _mockTimeSystem.Task
.Delay(TimeSpan.FromTicks(nextPlannedExecution - nowTicks), cancellationToken)
.ConfigureAwait(false);
long nowTicks = _mockTimeSystem.TimeProvider.ElapsedTicks;
if (targetTicks > nowTicks)
{
await _mockTimeSystem.Task
.Delay(TimeSpan.FromTicks(targetTicks - nowTicks),
cancellationToken)
.ConfigureAwait(false);
}
}
else
{
while (true)
{
long executeAfter = targetTicks;
using IAwaitableCallback<DateTime> onTimeChanged = _mockTimeSystem.On
.TimeChanged(predicate: _
=> _mockTimeSystem.TimeProvider.ElapsedTicks >= executeAfter);
// Check AFTER registering the callback to avoid missing a time change
// that occurred between reading ElapsedTicks and subscribing.
if (_mockTimeSystem.TimeProvider.ElapsedTicks >= targetTicks)
{
break;
}

await onTimeChanged.WaitAsync(
timeout: null,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
}
#pragma warning restore S3776 // Cognitive Complexity of methods should not be too high

private void Start()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,92 @@ public async Task Change_ValidPeriodValue_ShouldNotThrowException(int period)
await That(exception).IsNull();
}

[Test]
public async Task DisableAutoAdvance_ShouldExecuteTimerLimitedNumberOfTimes()
{
int callbackCount = 0;
MockTimeSystem timeSystem = new(o => o.DisableAutoAdvance());
using CancellationTokenSource cts = CancellationTokenSource
.CreateLinkedTokenSource(TestContext.Current!.Execution.CancellationToken);
cts.CancelAfter(30.Seconds());
CancellationToken token = cts.Token;
using SemaphoreSlim callbackExecuted = new(0);
using ITimer timer = timeSystem.Timer.New(_ =>
{
// ReSharper disable once AccessToModifiedClosure
Interlocked.Increment(ref callbackCount);
// ReSharper disable once AccessToDisposedClosure
callbackExecuted.Release();
}, null, 1.Seconds(), 2.Seconds());

await Task.Delay(50.Milliseconds(), token);
await That(Volatile.Read(ref callbackCount)).IsEqualTo(0);

// Advance past dueTime (1s): should trigger first callback
timeSystem.TimeProvider.AdvanceBy(2.Seconds());
await callbackExecuted.WaitAsync(token);
await That(Volatile.Read(ref callbackCount)).IsEqualTo(1);

// Advance past one period (2s): should trigger second callback
await Task.Delay(50.Milliseconds(), token);
timeSystem.TimeProvider.AdvanceBy(2.Seconds());
await callbackExecuted.WaitAsync(token);
await That(Volatile.Read(ref callbackCount)).IsEqualTo(2);

// Advance past two periods (4s): should trigger two more callbacks
await Task.Delay(50.Milliseconds(), token);
timeSystem.TimeProvider.AdvanceBy(4.Seconds());
await callbackExecuted.WaitAsync(token);
await Task.Delay(50.Milliseconds(), token);
timeSystem.TimeProvider.AdvanceBy(0.Seconds());
await callbackExecuted.WaitAsync(token);
await That(Volatile.Read(ref callbackCount)).IsEqualTo(4);
}

[Test]
public async Task DisableAutoAdvance_ShouldNotExecuteTimerBeforeTimeElapsed()
{
int callbackCount = 0;
MockTimeSystem timeSystem = new(o => o.DisableAutoAdvance());
using CancellationTokenSource cts = CancellationTokenSource
.CreateLinkedTokenSource(TestContext.Current!.Execution.CancellationToken);
cts.CancelAfter(30.Seconds());
CancellationToken token = cts.Token;
using ITimer timer = timeSystem.Timer.New(_ =>
{
// ReSharper disable once AccessToModifiedClosure
Interlocked.Increment(ref callbackCount);
}, null, 1.Seconds(), 2.Seconds());

await Task.Delay(50.Milliseconds(), token);
await That(Volatile.Read(ref callbackCount)).IsEqualTo(0);
}

[Test]
public async Task DisableAutoAdvance_ShouldStartTimerWhenTimeElapsed()
{
DateTime callbackTime = DateTime.MinValue;
MockTimeSystem timeSystem = new(o => o.DisableAutoAdvance());
using CancellationTokenSource cts = CancellationTokenSource
.CreateLinkedTokenSource(TestContext.Current!.Execution.CancellationToken);
cts.CancelAfter(30.Seconds());
CancellationToken token = cts.Token;
using SemaphoreSlim callbackExecuted = new(0);
using ITimer timer = timeSystem.Timer.New(_ =>
{
callbackTime = timeSystem.DateTime.UtcNow;
// ReSharper disable once AccessToDisposedClosure
callbackExecuted.Release();
}, null, 1.Seconds(), Timeout.InfiniteTimeSpan);

await Task.Delay(50.Milliseconds(), token);
timeSystem.TimeProvider.AdvanceBy(2.Seconds());
await callbackExecuted.WaitAsync(token);
DateTime after = timeSystem.DateTime.UtcNow;

await That(callbackTime).IsEqualTo(after);
}

[Test]
public async Task Dispose_ShouldDisposeTimer()
{
Expand Down Expand Up @@ -242,7 +328,7 @@ public async Task ShouldNotBeAffectedByTimeChange()
ticks.Release();
advanced.Wait(token);
// ReSharper restore AccessToDisposedClosure
}, null, TimeSpan.Zero, 2.Seconds());
}, null, TimeSpan.Zero, 1.Seconds());

await Task.Delay(Timeout.InfiniteTimeSpan, token);
}
Expand Down
Loading