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]