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
19 changes: 19 additions & 0 deletions Source/Testably.Abstractions.Testing/TimeSystem/ITimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@ namespace Testably.Abstractions.Testing.TimeSystem;
/// </summary>
public interface ITimeProvider
{
#if FEATURE_PERIODIC_TIMER
/// <summary>
/// The elapsed ticks represent a monotonic clock that is not affected by changes to the system time.
/// </summary>
/// <remarks>
/// It is used internally for the <see cref="IPeriodicTimer" />, the <see cref="IStopwatch" /> and the <see cref="ITimer" />.<br />
/// The value is not affected by changes to the system time (see <see cref="SetTo(DateTime)" />), but it is affected by the <see cref="AdvanceBy(TimeSpan)" /> method.<br />
/// </remarks>
#else
/// <summary>
/// The elapsed ticks represent a monotonic clock that is not affected by changes to the system time.
/// </summary>
/// <remarks>
/// It is used internally for the <see cref="IStopwatch" /> and the <see cref="ITimer" />.<br />
/// The value is not affected by changes to the system time (see <see cref="SetTo(DateTime)" />), but it is affected by the <see cref="AdvanceBy(TimeSpan)" /> method.<br />
/// </remarks>
#endif
long ElapsedTicks { get; }

/// <summary>
/// Gets or sets the <see cref="IDateTime.MaxValue" />
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ namespace Testably.Abstractions.Testing.TimeSystem;

internal sealed class PeriodicTimerMock : IPeriodicTimer
{
private readonly bool _autoAdvance;
private bool _isDisposed;
private DateTime _lastTime;
private long _lastTime;
private readonly MockTimeSystem _timeSystem;
private readonly bool _autoAdvance;

internal PeriodicTimerMock(MockTimeSystem timeSystem,
TimeSpan period, bool autoAdvance)
Expand All @@ -21,7 +21,7 @@ internal PeriodicTimerMock(MockTimeSystem timeSystem,

_timeSystem = timeSystem;
_autoAdvance = autoAdvance;
_lastTime = _timeSystem.DateTime.UtcNow;
_lastTime = _timeSystem.TimeProvider.ElapsedTicks;
Period = period;
}

Expand Down Expand Up @@ -58,23 +58,23 @@ public void Dispose()
return false;
}

DateTime now = _timeSystem.DateTime.UtcNow;
DateTime nextTime = _lastTime + Period;
long now = _timeSystem.TimeProvider.ElapsedTicks;
long nextTime = _lastTime + Period.Ticks;
if (nextTime > now)
{
if (_autoAdvance)
{
_timeSystem.TimeProvider.AdvanceBy(nextTime - now);
_timeSystem.TimeProvider.AdvanceBy(TimeSpan.FromTicks(nextTime - now));
_lastTime = nextTime;
}
else
{
using var onTimeChanged = _timeSystem.On
.TimeChanged(predicate: t => t >= nextTime);
using IAwaitableCallback<DateTime> onTimeChanged = _timeSystem.On
.TimeChanged(predicate: _ => _timeSystem.TimeProvider.ElapsedTicks >= nextTime);
await onTimeChanged.WaitAsync(
timeout: Timeout.InfiniteTimeSpan,
cancellationToken: cancellationToken).ConfigureAwait(false);
_lastTime = _timeSystem.DateTime.UtcNow;
_lastTime = _timeSystem.TimeProvider.ElapsedTicks;
}
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)

/// <inheritdoc cref="IStopwatchFactory.GetTimestamp()" />
public long GetTimestamp()
=> _mockTimeSystem.TimeProvider.Read().Ticks * _tickPeriod;
=> _mockTimeSystem.TimeProvider.ElapsedTicks * _tickPeriod;

/// <inheritdoc cref="IStopwatchFactory.New()" />
public IStopwatch New()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal sealed class StopwatchMock : IStopwatch
{
private long _elapsedTicks;
private readonly MockTimeSystem _mockTimeSystem;
private DateTime? _start;
private long? _start;
private readonly long _tickPeriod;

internal StopwatchMock(MockTimeSystem timeSystem, long tickPeriod)
Expand Down Expand Up @@ -36,7 +36,7 @@ public long ElapsedTicks
// If the Stopwatch is running, add elapsed time since the Stopwatch is started last time.
if (_start is not null)
{
timeElapsed += (_mockTimeSystem.TimeProvider.Read() - _start.Value).Ticks
timeElapsed += (_mockTimeSystem.TimeProvider.ElapsedTicks - _start.Value)
* _tickPeriod;
}

Expand Down Expand Up @@ -70,7 +70,7 @@ public void Start()
{
if (_start is null)
{
_start = _mockTimeSystem.TimeProvider.Read();
_start = _mockTimeSystem.TimeProvider.ElapsedTicks;
}
}

Expand All @@ -79,7 +79,7 @@ public void Stop()
{
if (_start.HasValue)
{
_elapsedTicks += (_mockTimeSystem.TimeProvider.Read() - _start.Value).Ticks *
_elapsedTicks += (_mockTimeSystem.TimeProvider.ElapsedTicks - _start.Value) *
_tickPeriod;
_start = null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
#if NETSTANDARD2_0
using Testably.Abstractions.TimeSystem;
#endif
Expand All @@ -8,6 +8,7 @@ namespace Testably.Abstractions.Testing.TimeSystem;
internal sealed class TimeProviderMock : ITimeProvider
{
private DateTime _now;
private long _elapsedTicks;
private readonly Action<DateTime> _onTimeChanged;
private readonly string _description;
#if NET9_0_OR_GREATER
Expand All @@ -21,6 +22,7 @@ public TimeProviderMock(Action<DateTime> onTimeChanged, DateTime now, string des
_now = now.Kind == DateTimeKind.Unspecified
? DateTime.SpecifyKind(now, DateTimeKind.Utc)
: now;
_elapsedTicks = _now.Ticks;
StartTime = _now;
_onTimeChanged = onTimeChanged;
_description = description;
Expand All @@ -46,20 +48,33 @@ public TimeProviderMock(Action<DateTime> onTimeChanged, DateTime now, string des
/// <inheritdoc cref="ITimeProvider.StartTime" />
public DateTime StartTime { get; }

/// <inheritdoc cref="ITimeProvider.ElapsedTicks" />
public long ElapsedTicks
{
get
{
lock (_lock) { return _elapsedTicks; }
}
}

/// <inheritdoc cref="ITimeProvider.AdvanceBy(TimeSpan)" />
public void AdvanceBy(TimeSpan interval)
{
lock (_lock)
{
_now = _now.Add(interval);
_elapsedTicks += interval.Ticks;
_onTimeChanged.Invoke(_now);
}
}

/// <inheritdoc cref="ITimeProvider.Read()" />
public DateTime Read()
{
return _now;
lock (_lock)
{
return _now;
}
}

/// <inheritdoc cref="ITimeProvider.SetTo(DateTime)" />
Expand All @@ -68,6 +83,7 @@ public void SetTo(DateTime value)
lock (_lock)
{
_now = value;
_onTimeChanged.Invoke(_now);
}
}

Expand Down
12 changes: 7 additions & 5 deletions Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,10 @@ private async Task RunTimer(CancellationToken cancellationToken = default)
cancellationToken.WaitHandle.WaitOne(_dueTime);
}

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

TimeSpan delay = nextPlannedExecution - _mockTimeSystem.DateTime.UtcNow;
if (delay > TimeSpan.Zero)
long nowTicks = _mockTimeSystem.TimeProvider.ElapsedTicks;
if (nextPlannedExecution > nowTicks)
{
await _mockTimeSystem.Task.Delay(delay, cancellationToken).ConfigureAwait(false);
await _mockTimeSystem.Task
.Delay(TimeSpan.FromTicks(nextPlannedExecution - nowTicks), cancellationToken)
.ConfigureAwait(false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ namespace Testably.Abstractions.Testing.TimeSystem
}
public interface ITimeProvider
{
long ElapsedTicks { get; }
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ namespace Testably.Abstractions.Testing.TimeSystem
}
public interface ITimeProvider
{
long ElapsedTicks { get; }
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ namespace Testably.Abstractions.Testing.TimeSystem
}
public interface ITimeProvider
{
long ElapsedTicks { get; }
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ namespace Testably.Abstractions.Testing.TimeSystem
}
public interface ITimeProvider
{
long ElapsedTicks { get; }
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ namespace Testably.Abstractions.Testing.TimeSystem
}
public interface ITimeProvider
{
long ElapsedTicks { get; }
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ namespace Testably.Abstractions.Testing.TimeSystem
}
public interface ITimeProvider
{
long ElapsedTicks { get; }
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#if FEATURE_PERIODIC_TIMER
using aweXpect.Chronology;
using System.Threading;
using Testably.Abstractions.TimeSystem;

namespace Testably.Abstractions.Testing.Tests.TimeSystem;

public class PeriodicTimerMockTests
{
[Test]
public async Task ShouldNotBeAffectedByTimeChange()
{
int timerCount = 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 ticks = new(0);
using SemaphoreSlim advanced = new(0);
Task timerTask = Task.Run(async () =>
{
try
{
using IPeriodicTimer timer = timeSystem.PeriodicTimer.New(1.Seconds());
timeSystem.TimeProvider.AdvanceBy(1.Seconds());
while (await timer.WaitForNextTickAsync(token))
{
// ReSharper disable once AccessToModifiedClosure
Interlocked.Increment(ref timerCount);
// ReSharper disable AccessToDisposedClosure
ticks.Release();
await advanced.WaitAsync(token);
// ReSharper restore AccessToDisposedClosure
}
}
catch (OperationCanceledException)
{
// Ignore cancellation
}
}, token);

await ticks.WaitAsync(token);

timeSystem.TimeProvider.AdvanceBy(1.Seconds());
advanced.Release();
await ticks.WaitAsync(token);

timeSystem.TimeProvider.AdvanceBy(1.Seconds());
advanced.Release();
await ticks.WaitAsync(token);

await That(Volatile.Read(ref timerCount)).IsEqualTo(3);

// Changing the wall clock time should not affect the periodic timer
timeSystem.TimeProvider.SetTo(timeSystem.DateTime.Now - 10.Seconds());

for (int i = 0; i < 10; i++)
{
timeSystem.TimeProvider.AdvanceBy(1.Seconds());
advanced.Release();
await ticks.WaitAsync(token);
}

await That(Volatile.Read(ref timerCount)).IsEqualTo(13);

timeSystem.TimeProvider.AdvanceBy(1.Seconds());
advanced.Release();
await ticks.WaitAsync(token);

await That(Volatile.Read(ref timerCount)).IsEqualTo(14);
cts.Cancel();
await timerTask;
}
}
#endif
Loading
Loading