diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/ITimeProvider.cs b/Source/Testably.Abstractions.Testing/TimeSystem/ITimeProvider.cs
index 01c9de0c..763eed76 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/ITimeProvider.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/ITimeProvider.cs
@@ -8,6 +8,25 @@ namespace Testably.Abstractions.Testing.TimeSystem;
///
public interface ITimeProvider
{
+#if FEATURE_PERIODIC_TIMER
+ ///
+ /// The elapsed ticks represent a monotonic clock that is not affected by changes to the system time.
+ ///
+ ///
+ /// It is used internally for the , the and the .
+ /// The value is not affected by changes to the system time (see ), but it is affected by the method.
+ ///
+#else
+ ///
+ /// The elapsed ticks represent a monotonic clock that is not affected by changes to the system time.
+ ///
+ ///
+ /// It is used internally for the and the .
+ /// The value is not affected by changes to the system time (see ), but it is affected by the method.
+ ///
+#endif
+ long ElapsedTicks { get; }
+
///
/// Gets or sets the
///
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/PeriodicTimerMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/PeriodicTimerMock.cs
index 37cecd03..047077ef 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/PeriodicTimerMock.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/PeriodicTimerMock.cs
@@ -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)
@@ -21,7 +21,7 @@ internal PeriodicTimerMock(MockTimeSystem timeSystem,
_timeSystem = timeSystem;
_autoAdvance = autoAdvance;
- _lastTime = _timeSystem.DateTime.UtcNow;
+ _lastTime = _timeSystem.TimeProvider.ElapsedTicks;
Period = period;
}
@@ -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 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
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs
index 349b552a..540b0bbe 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs
@@ -42,7 +42,7 @@ public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
///
public long GetTimestamp()
- => _mockTimeSystem.TimeProvider.Read().Ticks * _tickPeriod;
+ => _mockTimeSystem.TimeProvider.ElapsedTicks * _tickPeriod;
///
public IStopwatch New()
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs
index 29909133..cbc7ce03 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs
@@ -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)
@@ -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;
}
@@ -70,7 +70,7 @@ public void Start()
{
if (_start is null)
{
- _start = _mockTimeSystem.TimeProvider.Read();
+ _start = _mockTimeSystem.TimeProvider.ElapsedTicks;
}
}
@@ -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;
}
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/TimeProviderMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/TimeProviderMock.cs
index ab4cbb82..09d3158c 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/TimeProviderMock.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/TimeProviderMock.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
#if NETSTANDARD2_0
using Testably.Abstractions.TimeSystem;
#endif
@@ -8,6 +8,7 @@ namespace Testably.Abstractions.Testing.TimeSystem;
internal sealed class TimeProviderMock : ITimeProvider
{
private DateTime _now;
+ private long _elapsedTicks;
private readonly Action _onTimeChanged;
private readonly string _description;
#if NET9_0_OR_GREATER
@@ -21,6 +22,7 @@ public TimeProviderMock(Action 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;
@@ -46,12 +48,22 @@ public TimeProviderMock(Action onTimeChanged, DateTime now, string des
///
public DateTime StartTime { get; }
+ ///
+ public long ElapsedTicks
+ {
+ get
+ {
+ lock (_lock) { return _elapsedTicks; }
+ }
+ }
+
///
public void AdvanceBy(TimeSpan interval)
{
lock (_lock)
{
_now = _now.Add(interval);
+ _elapsedTicks += interval.Ticks;
_onTimeChanged.Invoke(_now);
}
}
@@ -59,7 +71,10 @@ public void AdvanceBy(TimeSpan interval)
///
public DateTime Read()
{
- return _now;
+ lock (_lock)
+ {
+ return _now;
+ }
}
///
@@ -68,6 +83,7 @@ public void SetTo(DateTime value)
lock (_lock)
{
_now = value;
+ _onTimeChanged.Invoke(_now);
}
}
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
index c2ef9029..f5b63b0a 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
@@ -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);
@@ -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);
}
}
}
diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt
index 6d5ab422..8bb8efd3 100644
--- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt
+++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net10.0.txt
@@ -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; }
diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt
index 6b458a72..88e89a87 100644
--- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt
+++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt
@@ -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; }
diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt
index ceb66fed..072ad37d 100644
--- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt
+++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt
@@ -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; }
diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt
index 1baf656e..26650fd1 100644
--- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt
+++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net9.0.txt
@@ -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; }
diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt
index b2351821..b039d0ac 100644
--- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt
+++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt
@@ -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; }
diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt
index 4c6427cc..674fde6c 100644
--- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt
+++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt
@@ -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; }
diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/PeriodicTimerMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/PeriodicTimerMockTests.cs
new file mode 100644
index 00000000..726724c7
--- /dev/null
+++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/PeriodicTimerMockTests.cs
@@ -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
diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs
index e015802e..c8bd02fb 100644
--- a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs
+++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs
@@ -1,4 +1,5 @@
-using System.Threading;
+using aweXpect.Chronology;
+using System.Threading;
using Testably.Abstractions.Testing.TimeSystem;
using ITimer = Testably.Abstractions.TimeSystem.ITimer;
@@ -218,6 +219,72 @@ public async Task New_WithStartOnMockWaitMode_ShouldOnlyStartWhenCallingWait()
await That(count).IsGreaterThan(0);
}
+ [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 ITimer timer = timeSystem.Timer.New(_ =>
+ {
+ // ReSharper disable once AccessToModifiedClosure
+ Interlocked.Increment(ref timerCount);
+ // ReSharper disable AccessToDisposedClosure
+ ticks.Release();
+ advanced.Wait(token);
+ // ReSharper restore AccessToDisposedClosure
+ }, null, TimeSpan.Zero, 2.Seconds());
+
+ await Task.Delay(Timeout.InfiniteTimeSpan, token);
+ }
+ 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 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;
+ }
+
[Test]
public async Task Wait_Infinite_ShouldBeValidTimeout()
{