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() {