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
17 changes: 14 additions & 3 deletions Source/Testably.Abstractions.Testing/MockTimeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,21 @@ public MockTimeSystem(DateTime time) : this(Testing.TimeProvider.Use(time))
/// <summary>
/// Initializes the <see cref="MockTimeSystem" /> with the specified <paramref name="timeProvider" />.
/// </summary>
public MockTimeSystem(ITimeProvider timeProvider)
#if MarkExecuteWhileWaitingNotificationObsolete
[Obsolete("Use the constructor with ITimeProviderFactory instead.")]
#endif
public MockTimeSystem(ITimeProvider timeProvider) : this(
new TimeProvider.Factory(_ => timeProvider))
{
}

/// <summary>
/// Initializes the <see cref="MockTimeSystem" /> with the specified <paramref name="timeProvider" />.
/// </summary>
public MockTimeSystem(ITimeProviderFactory timeProvider)
{
TimeProvider = timeProvider;
_callbackHandler = new NotificationHandler();
_callbackHandler = new NotificationHandler(this);
TimeProvider = timeProvider.Create(_callbackHandler.InvokeTimeChanged);
_dateTimeMock = new DateTimeMock(this, _callbackHandler);
_stopwatchFactoryMock = new StopwatchFactoryMock(this);
_threadMock = new ThreadMock(this, _callbackHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Testably.Abstractions.Testing;
/// <summary>
/// Extension methods for the <see cref="INotificationHandler" />
/// </summary>
public static class NotificationHandlerExtensions
public static partial class NotificationHandlerExtensions
{
/// <summary>
/// Callback executed when a <paramref name="fileSystemType" /> matching the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using Testably.Abstractions.Testing.TimeSystem;

namespace Testably.Abstractions.Testing;

/// <summary>
/// Extension methods for the <see cref="INotificationHandler" />
/// </summary>
public static partial class NotificationHandlerExtensions
{
/// <summary>
/// Callback executed when the mocked time changed after the specified <paramref name="interval" />.
/// </summary>
/// <param name="handler">The notification handler.</param>
/// <param name="interval">The time interval that should elapse in the mock time system.</param>
/// <param name="callback">
/// (optional) The callback to execute after the mocked time changed.
/// </param>
/// <returns>A <see cref="IAwaitableCallback{ChangeDescription}" /> to un-register the callback on dispose.</returns>
public static IAwaitableCallback<DateTime> Elapsed(
this INotificationHandler handler,
TimeSpan interval,
Action<DateTime>? callback = null)
=> handler.TimeChanged(callback, (s, d) => d - s >= interval);

/// <summary>
/// Callback executed when the mocked time changed.
/// </summary>
/// <param name="handler">The notification handler.</param>
/// <param name="callback">
/// (optional) The callback to execute after the mocked time changed.
/// </param>
/// <param name="predicate">
/// (optional) A predicate used to filter which changes should be notified.<br />
/// If set to <see langword="null" /> (default value) all changes are notified.
/// </param>
/// <returns>A <see cref="IAwaitableCallback{TimeSpan}" /> to un-register the callback on dispose.</returns>
public static IAwaitableCallback<DateTime> TimeChanged(
this INotificationHandler handler,
Action<DateTime>? callback = null,
Func<DateTime, bool>? predicate = null)
=> handler.TimeChanged(callback, predicate is null ? null : (_, d) => predicate(d));
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
<!-- https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/asyncenumerable -->
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' OR '$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0' OR '$(TargetFramework)' == 'netstandard2.1' OR '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0' OR '$(TargetFramework)' == 'netstandard2.1' OR '$(TargetFramework)' == 'netstandard2.0'">
<!-- https://github.com/advisories/GHSA-73j8-2gch-69rq -->
<PackageReference Include="Microsoft.Bcl.Memory" />
</ItemGroup>
Expand Down
29 changes: 22 additions & 7 deletions Source/Testably.Abstractions.Testing/TimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,48 @@ public static class TimeProvider
/// <summary>
/// Initializes the <see cref="MockTimeSystem.TimeProvider" /> with the current time.
/// </summary>
public static ITimeProvider Now()
public static ITimeProviderFactory Now()
{
return new TimeProviderMock(DateTime.UtcNow, "Now");
return new Factory(onTimeChanged
=> new TimeProviderMock(onTimeChanged, DateTime.UtcNow, "Now"));
}

/// <summary>
/// Initializes the <see cref="MockTimeSystem.TimeProvider" /> with a random time.
/// <para />
/// The random time increments the unix epoch by a random integer of seconds.
/// </summary>
public static ITimeProvider Random()
public static ITimeProviderFactory Random()
{
#pragma warning disable MA0113 // Use DateTime.UnixEpoch
DateTime randomTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
.AddSeconds(RandomFactory.Shared.Next());
#pragma warning restore MA0113
return new TimeProviderMock(randomTime, "Random");
return new Factory(onTimeChanged
=> new TimeProviderMock(onTimeChanged, randomTime, "Random"));
}

/// <summary>
/// Initializes the <see cref="MockTimeSystem.TimeProvider" /> with the specified <paramref name="time" />.
/// </summary>
/// <remarks>
/// If the <paramref name="time" /> has Kind DateTimeKind.Unspecified it will be treated as if it had Kind DateTimeKind.Utc.
/// If the <paramref name="time" /> has Kind DateTimeKind.Unspecified it will be treated as if it had Kind
/// DateTimeKind.Utc.
/// </remarks>
public static ITimeProvider Use(DateTime time)
public static ITimeProviderFactory Use(DateTime time)
{
return new TimeProviderMock(time, "Fixed");
return new Factory(onTimeChanged => new TimeProviderMock(onTimeChanged, time, "Fixed"));
}

internal sealed class Factory(Func<Action<DateTime>, ITimeProvider> createCallback)
: ITimeProviderFactory
{
#region ITimeProviderFactory Members

/// <inheritdoc cref="ITimeProviderFactory.Create(Action{DateTime})" />
public ITimeProvider Create(Action<DateTime> onTimeChanged)
=> createCallback(onTimeChanged);

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public interface INotificationHandler
/// - <see cref="IDateTime.UtcNow" /><br />
/// - <see cref="IDateTime.Today" />
/// </summary>
/// <param name="callback">The callback to execute after <c>DateTime</c> was read.</param>
/// <param name="callback">
/// (optional) The callback to execute after <c>DateTime</c> was read.
/// </param>
/// <param name="predicate">
/// (optional) A predicate used to filter which callbacks should be notified.<br />
/// If set to <see langword="null" /> (default value) all callbacks are notified.
Expand All @@ -32,7 +34,9 @@ IAwaitableCallback<DateTime> DateTimeRead(
/// - <see cref="ITask.Delay(int)" /><br />
/// - <see cref="ITask.Delay(int, CancellationToken)" />
/// </summary>
/// <param name="callback">The callback to execute after the <c>Task.Delay</c> was called.</param>
/// <param name="callback">
/// (optional) The callback to execute after the <c>Task.Delay</c> was called.
/// </param>
/// <param name="predicate">
/// (optional) A predicate used to filter which callbacks should be notified.<br />
/// If set to <see langword="null" /> (default value) all callbacks are notified.
Expand All @@ -47,7 +51,9 @@ IAwaitableCallback<TimeSpan> TaskDelay(
/// - <see cref="IThread.Sleep(TimeSpan)" /><br />
/// - <see cref="IThread.Sleep(int)" />
/// </summary>
/// <param name="callback">The callback to execute after the <c>Thread.Sleep</c> was called.</param>
/// <param name="callback">
/// (optional) The callback to execute after the <c>Thread.Sleep</c> was called.
/// </param>
/// <param name="predicate">
/// (optional) A predicate used to filter which callbacks should be notified.<br />
/// If set to <see langword="null" /> (default value) all callbacks are notified.
Expand All @@ -56,4 +62,20 @@ IAwaitableCallback<TimeSpan> TaskDelay(
IAwaitableCallback<TimeSpan> ThreadSleep(
Action<TimeSpan>? callback = null,
Func<TimeSpan, bool>? predicate = null);

/// <summary>
/// Callback executed when the mocked time changed.
/// </summary>
/// <param name="callback">
/// (optional) The callback to execute after the mocked time changed.
/// </param>
/// <param name="predicate">
/// (optional) A predicate used to filter which changes should be notified.<br />
/// The first parameter is the start time, the second parameter is the current time of the time provider.<br />
/// If set to <see langword="null" /> (default value) all changes are notified.
/// </param>
/// <returns>A <see cref="IAwaitableCallback{TimeSpan}" /> to un-register the callback on dispose.</returns>
IAwaitableCallback<DateTime> TimeChanged(
Action<DateTime>? callback = null,
Func<DateTime, DateTime, bool>? predicate = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public interface ITimeProvider
/// </summary>
DateTime MinValue { get; set; }

/// <summary>
/// The start time of the time provider.
/// </summary>
/// <remarks>
/// This is the time the time provider was initialized at the beginning of the test.
/// </remarks>
DateTime StartTime { get; }

/// <summary>
/// Gets or sets the <see cref="IDateTime.UnixEpoch" />
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace Testably.Abstractions.Testing.TimeSystem;

/// <summary>
/// The factory for creating the time provider for the <see cref="MockTimeSystem" />
/// </summary>
public interface ITimeProviderFactory
{
/// <summary>
/// Creates a time provider for the <see cref="MockTimeSystem" />.
/// </summary>
/// <remarks>
/// The <paramref name="onTimeChanged" /> callback is called whenever the time provider's time is changed. The callback
/// receives the new time as a parameter.
/// </remarks>
ITimeProvider Create(Action<DateTime> onTimeChanged);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Testably.Abstractions.Testing.TimeSystem;

internal sealed class NotificationHandler : INotificationHandler
internal sealed class NotificationHandler(MockTimeSystem mockTimeSystem) : INotificationHandler
{
private readonly Notification.INotificationFactory<DateTime>
_dateTimeReadCallbacks = Notification.CreateFactory<DateTime>();
Expand All @@ -13,6 +13,9 @@ private readonly Notification.INotificationFactory<TimeSpan>
private readonly Notification.INotificationFactory<TimeSpan>
_threadSleepCallbacks = Notification.CreateFactory<TimeSpan>();

private readonly Notification.INotificationFactory<DateTime>
_timeChangedCallbacks = Notification.CreateFactory<DateTime>();

#region INotificationHandler Members

/// <inheritdoc cref="INotificationHandler.DateTimeRead(Action{DateTime}?, Func{DateTime, bool}?)" />
Expand All @@ -33,6 +36,13 @@ public IAwaitableCallback<TimeSpan> ThreadSleep(
Func<TimeSpan, bool>? predicate = null)
=> _threadSleepCallbacks.RegisterCallback(callback, predicate);

/// <inheritdoc cref="INotificationHandler.TimeChanged(Action{DateTime}?, Func{DateTime, DateTime, bool}?)" />
public IAwaitableCallback<DateTime> TimeChanged(
Action<DateTime>? callback = null,
Func<DateTime, DateTime, bool>? predicate = null)
=> _timeChangedCallbacks.RegisterCallback(callback,
predicate is null ? null : d => predicate(mockTimeSystem.TimeProvider.StartTime, d));

#endregion

public void InvokeDateTimeReadCallbacks(DateTime now)
Expand All @@ -43,4 +53,7 @@ public void InvokeTaskDelayCallbacks(TimeSpan delay)

public void InvokeThreadSleepCallbacks(TimeSpan timeout)
=> _threadSleepCallbacks.InvokeCallbacks(timeout);

public void InvokeTimeChanged(DateTime now)
=> _timeChangedCallbacks.InvokeCallbacks(now);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ namespace Testably.Abstractions.Testing.TimeSystem;
internal sealed class TimeProviderMock : ITimeProvider
{
private DateTime _now;
private readonly Action<DateTime> _onTimeChanged;
private readonly string _description;
#if NET9_0_OR_GREATER
private readonly System.Threading.Lock _lock = new();
#else
private readonly object _lock = new();
#endif

public TimeProviderMock(DateTime now, string description)
public TimeProviderMock(Action<DateTime> onTimeChanged, DateTime now, string description)
{
_now = now.Kind == DateTimeKind.Unspecified
? DateTime.SpecifyKind(now, DateTimeKind.Utc)
: now;

StartTime = _now;
_onTimeChanged = onTimeChanged;
_description = description;
}

Expand All @@ -41,12 +43,16 @@ public TimeProviderMock(DateTime now, string description)
public DateTime UnixEpoch { get; set; } = DateTime.UnixEpoch;
#endif

/// <inheritdoc cref="ITimeProvider.StartTime" />
public DateTime StartTime { get; }

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ namespace Testably.Abstractions.Testing
public MockTimeSystem() { }
public MockTimeSystem(System.DateTime time) { }
public MockTimeSystem(Testably.Abstractions.Testing.TimeSystem.ITimeProvider timeProvider) { }
public MockTimeSystem(Testably.Abstractions.Testing.TimeSystem.ITimeProviderFactory timeProvider) { }
public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; }
public Testably.Abstractions.Testing.TimeSystem.INotificationHandler On { get; }
public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; }
Expand All @@ -126,9 +127,11 @@ namespace Testably.Abstractions.Testing
}
public static class NotificationHandlerExtensions
{
public static Testably.Abstractions.Testing.IAwaitableCallback<System.DateTime> Elapsed(this Testably.Abstractions.Testing.TimeSystem.INotificationHandler handler, System.TimeSpan interval, System.Action<System.DateTime>? callback = null) { }
public static Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnChanged(this Testably.Abstractions.Testing.FileSystem.INotificationHandler handler, Testably.Abstractions.Testing.FileSystemTypes fileSystemType, System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? notificationCallback = null, string globPattern = "*", System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null) { }
public static Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnCreated(this Testably.Abstractions.Testing.FileSystem.INotificationHandler handler, Testably.Abstractions.Testing.FileSystemTypes fileSystemType, System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? notificationCallback = null, string globPattern = "*", System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null) { }
public static Testably.Abstractions.Testing.IAwaitableCallback<Testably.Abstractions.Testing.FileSystem.ChangeDescription> OnDeleted(this Testably.Abstractions.Testing.FileSystem.INotificationHandler handler, Testably.Abstractions.Testing.FileSystemTypes fileSystemType, System.Action<Testably.Abstractions.Testing.FileSystem.ChangeDescription>? notificationCallback = null, string globPattern = "*", System.Func<Testably.Abstractions.Testing.FileSystem.ChangeDescription, bool>? predicate = null) { }
public static Testably.Abstractions.Testing.IAwaitableCallback<System.DateTime> TimeChanged(this Testably.Abstractions.Testing.TimeSystem.INotificationHandler handler, System.Action<System.DateTime>? callback = null, System.Func<System.DateTime, bool>? predicate = null) { }
}
public static class RandomProvider
{
Expand Down Expand Up @@ -160,9 +163,9 @@ namespace Testably.Abstractions.Testing
}
public static class TimeProvider
{
public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Now() { }
public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Random() { }
public static Testably.Abstractions.Testing.TimeSystem.ITimeProvider Use(System.DateTime time) { }
public static Testably.Abstractions.Testing.TimeSystem.ITimeProviderFactory Now() { }
public static Testably.Abstractions.Testing.TimeSystem.ITimeProviderFactory Random() { }
public static Testably.Abstractions.Testing.TimeSystem.ITimeProviderFactory Use(System.DateTime time) { }
}
}
namespace Testably.Abstractions.Testing.FileSystem
Expand Down Expand Up @@ -423,16 +426,22 @@ namespace Testably.Abstractions.Testing.TimeSystem
Testably.Abstractions.Testing.IAwaitableCallback<System.DateTime> DateTimeRead(System.Action<System.DateTime>? callback = null, System.Func<System.DateTime, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<System.TimeSpan> TaskDelay(System.Action<System.TimeSpan>? callback = null, System.Func<System.TimeSpan, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<System.TimeSpan> ThreadSleep(System.Action<System.TimeSpan>? callback = null, System.Func<System.TimeSpan, bool>? predicate = null);
Testably.Abstractions.Testing.IAwaitableCallback<System.DateTime> TimeChanged(System.Action<System.DateTime>? callback = null, System.Func<System.DateTime, System.DateTime, bool>? predicate = null);
}
public interface ITimeProvider
{
System.DateTime MaxValue { get; set; }
System.DateTime MinValue { get; set; }
System.DateTime StartTime { get; }
System.DateTime UnixEpoch { get; set; }
void AdvanceBy(System.TimeSpan interval);
System.DateTime Read();
void SetTo(System.DateTime value);
}
public interface ITimeProviderFactory
{
Testably.Abstractions.Testing.TimeSystem.ITimeProvider Create(System.Action<System.DateTime> onTimeChanged);
}
public interface ITimerHandler
{
Testably.Abstractions.Testing.TimeSystem.ITimerMock this[int index] { get; }
Expand Down
Loading
Loading