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
5 changes: 3 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<ItemGroup>
<PackageVersion Include="System.Net.Http" Version="4.3.4"/>
<PackageVersion Include="aweXpect" Version="2.30.0"/>
<PackageVersion Include="aweXpect.Chronology" Version="1.0.0"/>
<PackageVersion Include="aweXpect.Testably" Version="0.13.0"/>
<PackageVersion Include="aweXpect.Mockolate" Version="1.2.0"/>
<PackageVersion Include="Mockolate" Version="1.5.4"/>
Expand All @@ -49,8 +50,8 @@
<PackageVersion Include="AutoFixture" Version="4.18.1" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Testably.Abstractions.Interface" Version="10.1.0"/>
<PackageVersion Include="Testably.Abstractions" Version="10.1.0"/>
<PackageVersion Include="Testably.Abstractions.Interface" Version="10.2.0-pre.1"/>
<PackageVersion Include="Testably.Abstractions" Version="10.2.0-pre.1"/>
<PackageVersion Include="Testably.Abstractions.Testing" Version="5.0.3"/>
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Pipeline/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ partial class Build : NukeBuild
/// <para />
/// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag.
/// </summary>
readonly BuildScope BuildScope = BuildScope.CoreOnly;
readonly BuildScope BuildScope = BuildScope.Default;

[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ In addition, the following interfaces are defined:
- `Stopwatch` is a wrapper around [`System.Diagnostics.Stopwatch`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch)
- `Task` allows replacing [`Task.Delay`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay)
- `Thread` allows replacing [`Thread.Sleep`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.sleep)
- `PeriodicTimer` is a wrapper around [`System.Threading.PeriodicTimer`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer)
- `Timer` is a wrapper around [`System.Threading.Timer`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer)
- The `IRandomSystem` interface abstracts away functionality related to randomness:
`Random` methods implement a thread-safe Shared instance also under .NET Framework and `Guid` methods allow creating new GUIDs.
Expand Down
16 changes: 12 additions & 4 deletions Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ internal static IOException FileSharingViolation()
};

internal static IOException FileSharingViolation(string path)
=> new($"The process cannot access the file '{path}' because it is being used by another process.")
=> new(
$"The process cannot access the file '{path}' because it is being used by another process.")
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024864,
Expand Down Expand Up @@ -156,6 +157,11 @@ internal static NotSupportedException NotSupportedSafeFileHandle()
internal static NotSupportedException NotSupportedStopwatchWrapping()
=> new("You cannot wrap an existing Stopwatch in the MockTimeSystem instance!");

#if FEATURE_PERIODIC_TIMER
internal static NotSupportedException NotSupportedPeriodicTimerWrapping()
=> new("You cannot wrap an existing PeriodicTimer in the MockTimeSystem instance!");
#endif

internal static NotSupportedException NotSupportedTimerWrapping()
=> new("You cannot wrap an existing Timer in the MockTimeSystem instance!");

Expand Down Expand Up @@ -229,7 +235,8 @@ internal static IOException ProcessCannotAccessTheFile(string path, int hResult)

internal static IOException ReplaceSourceMustBeDifferentThanDestination(
string sourcePath, string destinationPath)
=> new($"The source '{sourcePath}' and destination '{destinationPath}' are the same file.", -2146232800);
=> new($"The source '{sourcePath}' and destination '{destinationPath}' are the same file.",
-2146232800);

#pragma warning disable MA0015 // Specify the parameter name
internal static ArgumentException SearchPatternCannotContainTwoDots()
Expand Down Expand Up @@ -334,7 +341,8 @@ internal static PlatformNotSupportedException UnixFileModeNotSupportedOnThisPlat
HResult = -2146233031,
#endif
};

internal static ArgumentException InvalidUnixCreateMode(string paramName)
=> new("UnixCreateMode can only be used with file modes that can create a new file.", paramName);
=> new("UnixCreateMode can only be used with file modes that can create a new file.",
paramName);
}
12 changes: 12 additions & 0 deletions Source/Testably.Abstractions.Testing/MockTimeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public INotificationHandler On
private readonly StopwatchFactoryMock _stopwatchFactoryMock;
private readonly TaskMock _taskMock;
private readonly ThreadMock _threadMock;
#if FEATURE_PERIODIC_TIMER
private readonly PeriodicTimerFactoryMock _periodicTimerFactoryMock;
#endif
private readonly TimerFactoryMock _timerFactoryMock;

/// <summary>
Expand Down Expand Up @@ -59,6 +62,9 @@ public MockTimeSystem(ITimeProvider timeProvider)
_stopwatchFactoryMock = new StopwatchFactoryMock(this);
_threadMock = new ThreadMock(this, _callbackHandler);
_taskMock = new TaskMock(this, _callbackHandler);
#if FEATURE_PERIODIC_TIMER
_periodicTimerFactoryMock = new PeriodicTimerFactoryMock(this);
#endif
_timerFactoryMock = new TimerFactoryMock(this);
}

Expand All @@ -68,6 +74,12 @@ public MockTimeSystem(ITimeProvider timeProvider)
public IDateTime DateTime
=> _dateTimeMock;

#if FEATURE_PERIODIC_TIMER
/// <inheritdoc cref="ITimeSystem.PeriodicTimer" />
public IPeriodicTimerFactory PeriodicTimer
=> _periodicTimerFactoryMock;
#endif

/// <inheritdoc cref="ITimeSystem.Stopwatch" />
public IStopwatchFactory Stopwatch
=> _stopwatchFactoryMock;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#if FEATURE_PERIODIC_TIMER
using System;
using System.Threading;
using Testably.Abstractions.Testing.Helpers;
using Testably.Abstractions.TimeSystem;

namespace Testably.Abstractions.Testing.TimeSystem;

internal sealed class PeriodicTimerFactoryMock : IPeriodicTimerFactory
{
private readonly MockTimeSystem _mockTimeSystem;

internal PeriodicTimerFactoryMock(MockTimeSystem timeSystem)
{
_mockTimeSystem = timeSystem;
}

#region IPeriodicTimerFactory Members

/// <inheritdoc cref="ITimeSystemEntity.TimeSystem" />
public ITimeSystem TimeSystem => _mockTimeSystem;

/// <inheritdoc cref="IPeriodicTimerFactory.New(TimeSpan)" />
public IPeriodicTimer New(TimeSpan period)
=> new PeriodicTimerMock(_mockTimeSystem, period);

/// <inheritdoc cref="IPeriodicTimerFactory.Wrap(PeriodicTimer)" />
public IPeriodicTimer Wrap(PeriodicTimer timer)
=> throw ExceptionFactory.NotSupportedPeriodicTimerWrapping();

#endregion
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#if FEATURE_PERIODIC_TIMER
using System;
using System.Threading;
using System.Threading.Tasks;
using Testably.Abstractions.Testing.Helpers;
using Testably.Abstractions.TimeSystem;

namespace Testably.Abstractions.Testing.TimeSystem;

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

internal PeriodicTimerMock(MockTimeSystem timeSystem,
TimeSpan period)
{
ThrowIfPeriodIsInvalid(period, nameof(period));

_timeSystem = timeSystem;
_lastTime = _timeSystem.DateTime.UtcNow;
Period = period;
}

#region IPeriodicTimer Members

/// <inheritdoc cref="IPeriodicTimer.Period" />
public TimeSpan Period
{
get;
set
{
ThrowIfPeriodIsInvalid(value, nameof(value));
field = value;
}
}

/// <inheritdoc cref="ITimeSystemEntity.TimeSystem" />
public ITimeSystem TimeSystem => _timeSystem;

/// <inheritdoc cref="IDisposable.Dispose()" />
public void Dispose()
=> _isDisposed = true;

/// <inheritdoc cref="IPeriodicTimer.WaitForNextTickAsync(CancellationToken)" />
public async ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken = new())
{
if (cancellationToken.IsCancellationRequested)
{
throw ExceptionFactory.TaskWasCanceled();
}

if (_isDisposed)
{
return false;
}

DateTime now = _timeSystem.DateTime.UtcNow;
DateTime nextTime = _lastTime + Period;
if (nextTime > now)
{
_timeSystem.TimeProvider.AdvanceBy(nextTime - now);
_lastTime = nextTime;
}
else
{
_lastTime = now;
}

await Task.Yield();
return true;
}

#endregion

private static void ThrowIfPeriodIsInvalid(TimeSpan period, string paramName)
{
if (period.TotalMilliseconds < 1 && period != Timeout.InfiniteTimeSpan)
{
throw new ArgumentOutOfRangeException(paramName);
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ namespace Testably.Abstractions.Testing
public MockTimeSystem(Testably.Abstractions.Testing.TimeSystem.ITimeProvider timeProvider) { }
public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; }
public Testably.Abstractions.Testing.TimeSystem.INotificationHandler On { get; }
public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; }
public Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; }
public Testably.Abstractions.TimeSystem.ITask Task { get; }
public Testably.Abstractions.TimeSystem.IThread Thread { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ namespace Testably.Abstractions.Testing
public MockTimeSystem(Testably.Abstractions.Testing.TimeSystem.ITimeProvider timeProvider) { }
public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; }
public Testably.Abstractions.Testing.TimeSystem.INotificationHandler On { get; }
public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; }
public Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; }
public Testably.Abstractions.TimeSystem.ITask Task { get; }
public Testably.Abstractions.TimeSystem.IThread Thread { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ namespace Testably.Abstractions.Testing
public MockTimeSystem(Testably.Abstractions.Testing.TimeSystem.ITimeProvider timeProvider) { }
public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; }
public Testably.Abstractions.Testing.TimeSystem.INotificationHandler On { get; }
public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; }
public Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; }
public Testably.Abstractions.TimeSystem.ITask Task { get; }
public Testably.Abstractions.TimeSystem.IThread Thread { get; }
Expand Down
1 change: 1 addition & 0 deletions Tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<PackageReference Include="System.Net.Http"/>
<PackageReference Include="aweXpect"/>
<PackageReference Include="aweXpect.Chronology"/>
<PackageReference Include="aweXpect.Testably"/>
<PackageReference Include="aweXpect.Mockolate"/>
<PackageReference Include="Mockolate"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#if FEATURE_PERIODIC_TIMER
using System.Threading;

namespace Testably.Abstractions.Testing.Tests.TimeSystem;

public class PeriodicTimerFactoryMockTests
{
[Test]
public async Task Wrap_ShouldThrowNotSupportedException()
{
MockTimeSystem timeSystem = new();
using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(5));

void Act()
{
// ReSharper disable once AccessToDisposedClosure
_ = timeSystem.PeriodicTimer.Wrap(periodicTimer);
}

await That(Act).Throws<NotSupportedException>().WithMessage(
"You cannot wrap an existing PeriodicTimer in the MockTimeSystem instance!");
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#if FEATURE_PERIODIC_TIMER
using Testably.Abstractions.TimeSystem;

namespace Testably.Abstractions.Tests.TimeSystem;

[TimeSystemTests]
public class PeriodicTimerFactoryTests(TimeSystemTestData testData) : TimeSystemTestBase(testData)
{
[Test]
public async Task New_PeriodIsInfinite_ShouldNotThrow()
{
TimeSpan period = TimeSpan.FromMilliseconds(-1);

void Act()
{
_ = TimeSystem.PeriodicTimer.New(period);
}

await That(Act).DoesNotThrow();
}

[Test]
[Arguments(0)]
[Arguments(-2)]
public async Task New_PeriodIsZeroOrNegative_ShouldThrowArgumentOutOfRangeException(
int milliseconds)
{
TimeSpan period = TimeSpan.FromMilliseconds(milliseconds);

void Act()
{
_ = TimeSystem.PeriodicTimer.New(period);
}

await That(Act).Throws<ArgumentOutOfRangeException>().WithParamName("period");
}

[Test]
public async Task New_ShouldCreatePeriodicTimerWithGivenPeriod()
{
TimeSpan period = TimeSpan.FromSeconds(1);
using IPeriodicTimer timer = TimeSystem.PeriodicTimer.New(period);

await That(timer.Period).IsEqualTo(period);
}
}
#endif
Loading
Loading