diff --git a/src/core/Akka.TestKit.Tests/TestKitBaseTests/WithinTests.cs b/src/core/Akka.TestKit.Tests/TestKitBaseTests/WithinTests.cs index 2592cf8ad9c..2dc1ceff526 100644 --- a/src/core/Akka.TestKit.Tests/TestKitBaseTests/WithinTests.cs +++ b/src/core/Akka.TestKit.Tests/TestKitBaseTests/WithinTests.cs @@ -6,7 +6,13 @@ //----------------------------------------------------------------------- using System; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Extensions; using Xunit; +using Xunit.Sdk; +using static FluentAssertions.FluentActions; namespace Akka.TestKit.Tests.TestKitBaseTests { @@ -17,5 +23,45 @@ public void Within_should_increase_max_timeout_by_the_provided_epsilon_value() { Within(TimeSpan.FromSeconds(1), () => ExpectNoMsg(), TimeSpan.FromMilliseconds(50)); } + + [Fact] + public void Within_should_respect_minimum_time() + { + Within(0.3.Seconds(), 1.Seconds(), () => ExpectNoMsg(0.4.Seconds()), "", 0.1.Seconds()); + } + + [Fact] + public async Task WithinAsync_should_respect_minimum_time() + { + await WithinAsync( + 0.3.Seconds(), + 1.Seconds(), + async () => await ExpectNoMsgAsync(0.4.Seconds()), + "", + 0.1.Seconds()); + } + + [Fact] + public void Within_should_throw_if_execution_is_shorter_than_minimum_time() + { + Invoking(() => + { + Within(0.5.Seconds(), 1.Seconds(), () => ExpectNoMsg(0.1.Seconds()), null, 0.1.Seconds()); + }).Should().Throw(); + } + + [Fact] + public async Task WithinAsync_should_throw_if_execution_is_shorter_than_minimum_time() + { + await Awaiting(async () => + { + await WithinAsync( + 0.5.Seconds(), + 1.Seconds(), + async () => await ExpectNoMsgAsync(0.1.Seconds()), + null, + 0.1.Seconds()); + }).Should().ThrowAsync(); + } } } diff --git a/src/core/Akka.TestKit/TestKitBase.cs b/src/core/Akka.TestKit/TestKitBase.cs index 0d0cd6f12fd..d4e2c4f8746 100644 --- a/src/core/Akka.TestKit/TestKitBase.cs +++ b/src/core/Akka.TestKit/TestKitBase.cs @@ -364,7 +364,7 @@ public void SetAutoPilot(AutoPilot pilot) /// /// /// Retrieves the time remaining for execution of the innermost enclosing - /// Within block. + /// Within block. /// If missing that, then it returns the properly dilated default for this /// case from settings (key: "akka.test.single-expect-default"). /// @@ -378,7 +378,7 @@ public TimeSpan RemainingOrDefault /// /// /// Retrieves the time remaining for execution of the innermost enclosing - /// Within block. + /// Within block. /// /// The returned value is always finite. /// diff --git a/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs b/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs index ccf2907add1..18ed904b19d 100644 --- a/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs +++ b/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs @@ -35,8 +35,8 @@ public abstract partial class TestKitBase /// public void AwaitAssert(Action assertion, TimeSpan? duration=null, TimeSpan? interval=null, CancellationToken cancellationToken = default) { - var task = AwaitAssertAsync(assertion, duration, interval, cancellationToken); - task.WaitAndUnwrapException(); + AwaitAssertAsync(assertion, duration, interval, cancellationToken) + .WaitAndUnwrapException(); } /// @@ -53,6 +53,7 @@ public async Task AwaitAssertAsync(Action assertion, TimeSpan? duration=null, Ti cancellationToken.ThrowIfCancellationRequested(); try { + // TODO: assertion can run forever, need a way to stop this if this happens. assertion(); return; } diff --git a/src/core/Akka.TestKit/TestKitBase_Within.cs b/src/core/Akka.TestKit/TestKitBase_Within.cs index 9475108b242..8bb348cffa1 100644 --- a/src/core/Akka.TestKit/TestKitBase_Within.cs +++ b/src/core/Akka.TestKit/TestKitBase_Within.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Akka.TestKit.Internal; +using Nito.AsyncEx.Synchronous; namespace Akka.TestKit { @@ -27,17 +28,72 @@ public abstract partial class TestKitBase /// TBD /// TBD /// TBD - public void Within(TimeSpan max, Action action, TimeSpan? epsilonValue = null) + /// + public void Within( + TimeSpan max, + Action action, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - Within(TimeSpan.Zero, max, action, epsilonValue: epsilonValue); + WithinAsync( + min: TimeSpan.Zero, + max: max, + function: () => + { + action(); + return Task.FromResult((object)null); + }, + hint: null, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); } /// - /// Async version of Within + /// Async version of /// - public Task WithinAsync(TimeSpan max, Func actionAsync, TimeSpan? epsilonValue = null) + public async Task WithinAsync( + TimeSpan max, + Action action, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - return WithinAsync(TimeSpan.Zero, max, actionAsync, epsilonValue: epsilonValue); + await WithinAsync( + min: TimeSpan.Zero, + max: max, + function: () => + { + action(); + return Task.FromResult((object)null); + }, + hint: null, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Async version of + /// that takes a instead of an + /// + public async Task WithinAsync( + TimeSpan max, + Func actionAsync, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) + { + await WithinAsync( + min: TimeSpan.Zero, + max: max, + function: async () => + { + await actionAsync().ConfigureAwait(false); + return Task.FromResult((object)null); + }, + hint: null, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false); } /// @@ -52,17 +108,78 @@ public Task WithinAsync(TimeSpan max, Func actionAsync, TimeSpan? epsilonV /// TBD /// TBD /// TBD - public void Within(TimeSpan min, TimeSpan max, Action action, string hint = null, TimeSpan? epsilonValue = null) + /// + public void Within( + TimeSpan min, + TimeSpan max, + Action action, + string hint = null, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - Within(min, max, () => { action(); return null; }, hint, epsilonValue); + WithinAsync( + min: min, + max: max, + function: () => + { + action(); + return Task.FromResult((object)null); + }, + hint: hint, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); } /// - /// Async version of + /// Async version of + /// + public async Task WithinAsync( + TimeSpan min, + TimeSpan max, + Action action, + string hint = null, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) + { + await WithinAsync( + min: min, + max: max, + function: () => + { + action(); + return Task.FromResult((object)null); + }, + hint: hint, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Async version of + /// that takes a instead of an /// - public Task WithinAsync(TimeSpan min, TimeSpan max, Func actionAsync, string hint = null, TimeSpan? epsilonValue = null) + public async Task WithinAsync( + TimeSpan min, + TimeSpan max, + Func actionAsync, + string hint = null, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - return WithinAsync(min, max, async () => { await actionAsync(); return null; }, hint, epsilonValue); + await WithinAsync( + min: min, + max: max, + function: async () => + { + await actionAsync().ConfigureAwait(false); + return (object)null; + }, + hint: hint, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false); } /// @@ -76,12 +193,24 @@ public Task WithinAsync(TimeSpan min, TimeSpan max, Func actionAsync, stri /// TBD /// TBD /// TBD + /// /// TBD - public T Within(TimeSpan max, Func function, TimeSpan? epsilonValue = null) + public T Within( + TimeSpan max, + Func function, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - return Within(TimeSpan.Zero, max, function, epsilonValue: epsilonValue); + return WithinAsync( + min: TimeSpan.Zero, + max: max, + function: () => Task.FromResult(function()), + hint: null, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false).GetAwaiter().GetResult(); } - + /// /// Execute code block while bounding its execution time between 0 seconds and . /// `within` blocks may be nested. All methods in this class which take maximum wait times @@ -93,10 +222,22 @@ public T Within(TimeSpan max, Func function, TimeSpan? epsilonValue = null /// TBD /// TBD /// TBD + /// /// TBD - public Task WithinAsync(TimeSpan max, Func> function, TimeSpan? epsilonValue = null) + public async Task WithinAsync( + TimeSpan max, + Func> function, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - return WithinAsync(TimeSpan.Zero, max, function, epsilonValue: epsilonValue); + return await WithinAsync( + min: TimeSpan.Zero, + max: max, + function: function, + hint: null, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false); } /// @@ -112,55 +253,47 @@ public Task WithinAsync(TimeSpan max, Func> function, TimeSpan? ep /// TBD /// TBD /// TBD + /// /// TBD - public T Within(TimeSpan min, TimeSpan max, Func function, string hint = null, TimeSpan? epsilonValue = null) + public T Within( + TimeSpan min, + TimeSpan max, + Func function, + string hint = null, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { - min.EnsureIsPositiveFinite("min"); - min.EnsureIsPositiveFinite("max"); - max = Dilated(max); - var start = Now; - var rem = _testState.End.HasValue ? _testState.End.Value - start : Timeout.InfiniteTimeSpan; - _assertions.AssertTrue(rem.IsInfiniteTimeout() || rem >= min, "Required min time {0} not possible, only {1} left. {2}", min, rem, hint ?? ""); - - _testState.LastWasNoMsg = false; - - var maxDiff = max.Min(rem); - var prevEnd = _testState.End; - _testState.End = start + maxDiff; - - T ret; - try - { - ret = function(); - } - finally - { - _testState.End = prevEnd; - } - - var elapsed = Now - start; - var wasTooFast = elapsed < min; - if(wasTooFast) - { - const string failMessage = "Failed: Block took {0}, should have at least been {1}. {2}"; - ConditionalLog(failMessage, elapsed, min, hint ?? ""); - _assertions.Fail(failMessage, elapsed, min, hint ?? ""); - } - if (!_testState.LastWasNoMsg) - { - epsilonValue = epsilonValue ?? TimeSpan.Zero; - var tookTooLong = elapsed > maxDiff + epsilonValue; - if(tookTooLong) - { - const string failMessage = "Failed: Block took {0}, exceeding {1}. {2}"; - ConditionalLog(failMessage, elapsed, maxDiff, hint ?? ""); - _assertions.Fail(failMessage, elapsed, maxDiff, hint ?? ""); - } - } - - return ret; + return WithinAsync( + min: min, + max: max, + function: () => Task.FromResult(function()), + hint: hint, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .WaitAndUnwrapException(); } + /// + /// Async version of + /// + public async Task WithinAsync( + TimeSpan min, + TimeSpan max, + Func function, + string hint = null, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) + { + return await WithinAsync( + min: min, + max: max, + function: () => Task.FromResult(function()), + hint: hint, + epsilonValue: epsilonValue, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + /// /// Execute code block while bounding its execution time between and . /// `within` blocks may be nested. All methods in this class which take maximum wait times @@ -174,8 +307,15 @@ public T Within(TimeSpan min, TimeSpan max, Func function, string hint = n /// TBD /// TBD /// TBD + /// /// TBD - public async Task WithinAsync(TimeSpan min, TimeSpan max, Func> function, string hint = null, TimeSpan? epsilonValue = null) + public async Task WithinAsync( + TimeSpan min, + TimeSpan max, + Func> function, + string hint = null, + TimeSpan? epsilonValue = null, + CancellationToken cancellationToken = default) { min.EnsureIsPositiveFinite("min"); min.EnsureIsPositiveFinite("max"); @@ -208,9 +348,10 @@ public async Task WithinAsync(TimeSpan min, TimeSpan max, Func> fu ConditionalLog(failMessage, elapsed, min, hint ?? ""); _assertions.Fail(failMessage, elapsed, min, hint ?? ""); } + if (!_testState.LastWasNoMsg) { - epsilonValue = epsilonValue ?? TimeSpan.Zero; + epsilonValue ??= TimeSpan.Zero; var tookTooLong = elapsed > maxDiff + epsilonValue; if(tookTooLong) {