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
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Testably.Abstractions.Testing.TimeSystem;
using System.Threading;

namespace Testably.Abstractions.Testing.TimeSystem;

/// <summary>
/// The strategy how to handle mocked timers for testing.
Expand All @@ -9,4 +11,12 @@ public interface ITimerStrategy
/// The timer mode.
/// </summary>
TimerMode Mode { get; }

/// <summary>
/// Flag indicating, if exceptions in the <see cref="TimerCallback" /> should be swallowed or thrown.
/// <para />
/// The real <see cref="Timer" /> will crash the application in case of an exception in the
/// <see cref="TimerCallback" />.
/// </summary>
bool SwallowExceptions { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ public class TimerExecution
/// </summary>
public ITimerMock Timer { get; }

internal TimerExecution(DateTime time, ITimerMock timer)
/// <summary>
/// The exception thrown during this timer execution.
/// </summary>
public Exception? Exception { get; }

internal TimerExecution(DateTime time, ITimerMock timer, Exception? exception)
{
Time = time;
Timer = timer;
Exception = exception;
}
}
33 changes: 24 additions & 9 deletions Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal sealed class TimerMock : ITimerMock
private CountdownEvent? _countdownEvent;
private readonly ManualResetEventSlim _continueEvent = new();
private int _executionCount;
private Exception? _exception;

internal TimerMock(MockTimeSystem timeSystem,
NotificationHandler callbackHandler,
Expand Down Expand Up @@ -76,7 +77,6 @@ private void Start()
}

CancellationToken token = runningCancellationTokenSource.Token;
Exception? backgroundEx = null;
ManualResetEventSlim startCreateTimerThreads = new();
Thread t = new(() =>
{
Expand All @@ -87,8 +87,7 @@ private void Start()
}
catch (Exception ex)
{
backgroundEx = ex;
Interlocked.MemoryBarrier();
_exception = ex;
}
finally
{
Expand All @@ -108,10 +107,6 @@ private void Start()
};
t.Start();
startCreateTimerThreads.Wait(token);
if (backgroundEx != null)
{
throw new AggregateException(backgroundEx);
}
}

internal void RegisterOnDispose(Action? onDispose)
Expand All @@ -126,16 +121,32 @@ private void RunTimer(CancellationToken cancellationToken = default)
while (!cancellationToken.IsCancellationRequested)
{
nextPlannedExecution += _period;
_callback(_state);
Exception? exception = null;
try
{
_callback(_state);
}
catch (Exception swallowedException)
{
_exception = exception = swallowedException;
}
Interlocked.Increment(ref _executionCount);
_callbackHandler.InvokeTimerExecutedCallbacks(
new TimerExecution(_mockTimeSystem.DateTime.UtcNow, this));
new TimerExecution(
_mockTimeSystem.DateTime.UtcNow,
this,
exception));
if (_countdownEvent?.Signal() == true)
{
_continueEvent.Wait(cancellationToken);
_continueEvent.Reset();
}

if (_exception != null && !_timerStrategy.SwallowExceptions)
{
break;
}

if (_period.TotalMilliseconds <= 0)
{
return;
Expand Down Expand Up @@ -310,6 +321,10 @@ public ITimerMock Wait(
// In case of an ArgumentOutOfRangeException, the executionCount is already reached.
}

if (_exception != null)
{
throw _exception;
}
callback?.Invoke(this);
_continueEvent.Set();

Expand Down
13 changes: 9 additions & 4 deletions Source/Testably.Abstractions.Testing/TimeSystem/TimerStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ public class TimerStrategy : ITimerStrategy
public static ITimerStrategy Default { get; }
= new TimerStrategy(TimerMode.StartImmediately);

/// <summary>
/// The timer mode.
/// </summary>
/// <inheritdoc cref="ITimerStrategy.Mode"/>
public TimerMode Mode { get; }

/// <inheritdoc cref="ITimerStrategy.SwallowExceptions"/>
public bool SwallowExceptions { get; }

/// <summary>
/// Initializes a new instance of <see cref="TimerStrategy" />.
/// </summary>
/// <param name="mode">The timer mode.</param>
public TimerStrategy(TimerMode mode)
/// <param name="swallowExceptions">Flag, indicating if exceptions should be swallowed.</param>
public TimerStrategy(
TimerMode mode = TimerMode.StartImmediately,
bool swallowExceptions = false)
{
Mode = mode;
SwallowExceptions = swallowExceptions;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Threading;
using Testably.Abstractions.Testing.FileSystemInitializer;
using Testably.Abstractions.Testing.Tests.TestHelpers;
using Testably.Abstractions.Testing.TimeSystem;
using Testably.Abstractions.TimeSystem;

Expand All @@ -24,6 +26,77 @@ public void Dispose_WithUnknownWaitHandle_ShouldThrowNotSupportedException()
exception.Should().BeOfType<NotSupportedException>();
}

[Fact]
public void Exception_ShouldBeIncludedInTimerExecutedNotification()
{
TestingException exception = new("foo");
MockTimeSystem timeSystem = new MockTimeSystem()
.WithTimerStrategy(new TimerStrategy(swallowExceptions: true));
ITimerHandler timerHandler = timeSystem.TimerHandler;
TimerExecution? receivedTimeout = null;

using (timeSystem.On.TimerExecuted(d => receivedTimeout = d))
{
timeSystem.Timer.New(_ => throw exception, null,
TimeTestHelper.GetRandomInterval(),
TimeTestHelper.GetRandomInterval());
try
{
timerHandler[0].Wait();
}
catch (TestingException)
{
// Expect a TestingException to be thrown
}
}

receivedTimeout!.Exception.Should().Be(exception);
}

[Fact]
public void Exception_WhenSwallowExceptionsIsSet_ShouldContinueTimerExecution()
{
MockTimeSystem timeSystem = new();
timeSystem.WithTimerStrategy(
new TimerStrategy(swallowExceptions: true));
Exception exception = new("foo");
int count = 0;
ManualResetEventSlim ms = new();
using ITimer timer = timeSystem.Timer.New(_ =>
{
if (count++ == 1)
{
throw exception;
}

if (count == 3)
{
ms.Set();
}
}, null, 0, 20);

ms.Wait(10000).Should().BeTrue();

count.Should().BeGreaterThanOrEqualTo(3);
}

[Fact]
public void Exception_WhenSwallowExceptionsIsNotSet_ShouldThrowExceptionOnWait()
{
MockTimeSystem timeSystem = new MockTimeSystem()
.WithTimerStrategy(new TimerStrategy(swallowExceptions: false));
Exception expectedException = new("foo");
using ITimer timer = timeSystem.Timer.New(
_ => throw expectedException, null, 0, 20);

Exception? exception = Record.Exception(() =>
{
timeSystem.TimerHandler[0].Wait();
});

exception.Should().Be(expectedException);
}

[Fact]
public void New_WithStartOnMockWaitMode_ShouldOnlyStartWhenCallingWait()
{
Expand Down
6 changes: 4 additions & 2 deletions Tests/Testably.Abstractions.Tests/TimeSystem/TimerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public void Change_InvalidDueTime_ShouldThrowArgumentOutOfRangeException(int due
});

exception.Should()
.BeException<ArgumentOutOfRangeException>(hResult: -2146233086, paramName: nameof(dueTime));
.BeException<ArgumentOutOfRangeException>(hResult: -2146233086,
paramName: nameof(dueTime));
}

[SkippableTheory]
Expand All @@ -60,7 +61,8 @@ public void Change_InvalidPeriod_ShouldThrowArgumentOutOfRangeException(int peri
});

exception.Should()
.BeException<ArgumentOutOfRangeException>(hResult: -2146233086, paramName: nameof(period));
.BeException<ArgumentOutOfRangeException>(hResult: -2146233086,
paramName: nameof(period));
}

[SkippableFact]
Expand Down