diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/ITimerStrategy.cs b/Source/Testably.Abstractions.Testing/TimeSystem/ITimerStrategy.cs
index 8fb45dec6..6ead4badc 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/ITimerStrategy.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/ITimerStrategy.cs
@@ -1,4 +1,6 @@
-namespace Testably.Abstractions.Testing.TimeSystem;
+using System.Threading;
+
+namespace Testably.Abstractions.Testing.TimeSystem;
///
/// The strategy how to handle mocked timers for testing.
@@ -9,4 +11,12 @@ public interface ITimerStrategy
/// The timer mode.
///
TimerMode Mode { get; }
+
+ ///
+ /// Flag indicating, if exceptions in the should be swallowed or thrown.
+ ///
+ /// The real will crash the application in case of an exception in the
+ /// .
+ ///
+ bool SwallowExceptions { get; }
}
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/TimerExecution.cs b/Source/Testably.Abstractions.Testing/TimeSystem/TimerExecution.cs
index 16d32f584..6dffcad13 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/TimerExecution.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/TimerExecution.cs
@@ -17,9 +17,15 @@ public class TimerExecution
///
public ITimerMock Timer { get; }
- internal TimerExecution(DateTime time, ITimerMock timer)
+ ///
+ /// The exception thrown during this timer execution.
+ ///
+ public Exception? Exception { get; }
+
+ internal TimerExecution(DateTime time, ITimerMock timer, Exception? exception)
{
Time = time;
Timer = timer;
+ Exception = exception;
}
}
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
index 3f7da598b..34d5b8d22 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/TimerMock.cs
@@ -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,
@@ -76,7 +77,6 @@ private void Start()
}
CancellationToken token = runningCancellationTokenSource.Token;
- Exception? backgroundEx = null;
ManualResetEventSlim startCreateTimerThreads = new();
Thread t = new(() =>
{
@@ -87,8 +87,7 @@ private void Start()
}
catch (Exception ex)
{
- backgroundEx = ex;
- Interlocked.MemoryBarrier();
+ _exception = ex;
}
finally
{
@@ -108,10 +107,6 @@ private void Start()
};
t.Start();
startCreateTimerThreads.Wait(token);
- if (backgroundEx != null)
- {
- throw new AggregateException(backgroundEx);
- }
}
internal void RegisterOnDispose(Action? onDispose)
@@ -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;
@@ -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();
diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/TimerStrategy.cs b/Source/Testably.Abstractions.Testing/TimeSystem/TimerStrategy.cs
index 589a3bba6..4af50d0c9 100644
--- a/Source/Testably.Abstractions.Testing/TimeSystem/TimerStrategy.cs
+++ b/Source/Testably.Abstractions.Testing/TimeSystem/TimerStrategy.cs
@@ -11,17 +11,22 @@ public class TimerStrategy : ITimerStrategy
public static ITimerStrategy Default { get; }
= new TimerStrategy(TimerMode.StartImmediately);
- ///
- /// The timer mode.
- ///
+ ///
public TimerMode Mode { get; }
+ ///
+ public bool SwallowExceptions { get; }
+
///
/// Initializes a new instance of .
///
/// The timer mode.
- public TimerStrategy(TimerMode mode)
+ /// Flag, indicating if exceptions should be swallowed.
+ public TimerStrategy(
+ TimerMode mode = TimerMode.StartImmediately,
+ bool swallowExceptions = false)
{
Mode = mode;
+ SwallowExceptions = swallowExceptions;
}
}
diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs
index 7839a9497..b50c34a49 100644
--- a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs
+++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerMockTests.cs
@@ -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;
@@ -24,6 +26,77 @@ public void Dispose_WithUnknownWaitHandle_ShouldThrowNotSupportedException()
exception.Should().BeOfType();
}
+ [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()
{
diff --git a/Tests/Testably.Abstractions.Tests/TimeSystem/TimerTests.cs b/Tests/Testably.Abstractions.Tests/TimeSystem/TimerTests.cs
index 0dbef2d81..b7254873d 100644
--- a/Tests/Testably.Abstractions.Tests/TimeSystem/TimerTests.cs
+++ b/Tests/Testably.Abstractions.Tests/TimeSystem/TimerTests.cs
@@ -42,7 +42,8 @@ public void Change_InvalidDueTime_ShouldThrowArgumentOutOfRangeException(int due
});
exception.Should()
- .BeException(hResult: -2146233086, paramName: nameof(dueTime));
+ .BeException(hResult: -2146233086,
+ paramName: nameof(dueTime));
}
[SkippableTheory]
@@ -60,7 +61,8 @@ public void Change_InvalidPeriod_ShouldThrowArgumentOutOfRangeException(int peri
});
exception.Should()
- .BeException(hResult: -2146233086, paramName: nameof(period));
+ .BeException(hResult: -2146233086,
+ paramName: nameof(period));
}
[SkippableFact]