diff --git a/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs b/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs index 5d84e5b52eb..f454fe4eb9b 100644 --- a/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs +++ b/src/core/Akka.TestKit/EventFilter/IEventFilterApplier.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Threading.Tasks; namespace Akka.TestKit { @@ -24,6 +25,26 @@ public interface IEventFilterApplier /// /// The action. void ExpectOne(Action action); + + /// + /// Executes and + /// expects one event to be logged during the execution. + /// This method fails and throws an exception if more than one event is logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The action. + Task ExpectOneAsync(Action action); + + /// + /// Executes and + /// expects one event to be logged during the execution. + /// This method fails and throws an exception if more than one event is logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The action. + Task ExpectOneAsync(Func actionAsync); /// /// Executes and @@ -34,7 +55,17 @@ public interface IEventFilterApplier /// The time to wait for a log event after executing /// The action. void ExpectOne(TimeSpan timeout, Action action); - + + /// + /// Executes and + /// expects one event to be logged during the execution. + /// This method fails and throws an exception if more than one event is logged, + /// or if a timeout occurs. + /// + /// The time to wait for a log event after executing + /// The action. + Task ExpectOneAsync(TimeSpan timeout, Action action); + /// /// Executes and expects the specified number /// of events to be logged during the execution. @@ -45,6 +76,28 @@ public interface IEventFilterApplier /// The expected number of events /// The action. void Expect(int expectedCount, Action action); + + /// + /// Executes and expects the specified number + /// of events to be logged during the execution. + /// This method fails and throws an exception if more events than expected are logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The expected number of events + /// The action. + Task ExpectAsync(int expectedCount, Action action); + + /// + /// Executes task and expects the specified number + /// of events to be logged during the execution. + /// This method fails and throws an exception if more events than expected are logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The expected number of events + /// The async action. + Task ExpectAsync(int expectedCount, Func actionAsync); /// /// Executes and expects the specified number @@ -57,6 +110,18 @@ public interface IEventFilterApplier /// The expected number of events /// The action. void Expect(int expectedCount, TimeSpan timeout, Action action); + + /// + /// Executes and expects the specified number + /// of events to be logged during the execution. + /// This method fails and throws an exception if more events than expected are logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The time to wait for log events after executing + /// The expected number of events + /// The action. + Task ExpectAsync(int expectedCount, TimeSpan timeout, Action action); /// /// Executes and @@ -69,6 +134,18 @@ public interface IEventFilterApplier /// The function. /// The returned value from . T ExpectOne(Func func); + + /// + /// Executes and + /// expects one event to be logged during the execution. + /// This function fails and throws an exception if more than one event is logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The return value of the function + /// The function. + /// The returned value from . + Task ExpectOneAsync(Func func); /// /// Executes and @@ -81,6 +158,18 @@ public interface IEventFilterApplier /// The function. /// The returned value from . T ExpectOne(TimeSpan timeout, Func func); + + /// + /// Executes and + /// expects one event to be logged during the execution. + /// This function fails and throws an exception if more than one event is logged, + /// or if a timeout occurs. + /// + /// The return value of the function + /// The time to wait for a log event after executing + /// The function. + /// The returned value from . + Task ExpectOneAsync(TimeSpan timeout, Func func); /// /// Executes and expects the specified number @@ -94,6 +183,19 @@ public interface IEventFilterApplier /// The function. /// The returned value from . T Expect(int expectedCount, Func func); + + /// + /// Executes and expects the specified number + /// of events to be logged during the execution. + /// This function fails and throws an exception if more events than expected are logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The return value of the function + /// The expected number of events + /// The function. + /// The returned value from . + Task ExpectAsync(int expectedCount, Func func); /// /// Executes and expects the specified number @@ -108,6 +210,20 @@ public interface IEventFilterApplier /// The function. /// The returned value from . T Expect(int expectedCount, TimeSpan timeout, Func func); + + /// + /// Executes and expects the specified number + /// of events to be logged during the execution. + /// This function fails and throws an exception if more events than expected are logged, + /// or if a timeout occurs. The timeout is taken from the config value + /// "akka.test.filter-leeway", see . + /// + /// The return value of the function + /// The time to wait for log events after executing + /// The expected number of events + /// The function. + /// The returned value from . + Task ExpectAsync(int expectedCount, TimeSpan timeout, Func func); /// /// Executes and prevent events from being logged during the execution. @@ -116,6 +232,14 @@ public interface IEventFilterApplier /// The function. /// The returned value from . T Mute(Func func); + + /// + /// Executes and prevent events from being logged during the execution. + /// + /// The return value of the function + /// The function. + /// The returned value from . + Task MuteAsync(Func func); /// /// Executes and prevent events from being logged during the execution. @@ -123,6 +247,13 @@ public interface IEventFilterApplier /// The function. /// The returned value from . void Mute(Action action); + + /// + /// Executes and prevent events from being logged during the execution. + /// + /// The function. + /// The returned value from . + Task MuteAsync(Action action); /// /// Prevents events from being logged from now on. To allow events to be logged again, call diff --git a/src/core/Akka.TestKit/EventFilter/Internal/EventFilterApplier.cs b/src/core/Akka.TestKit/EventFilter/Internal/EventFilterApplier.cs index d11df487346..a4f96523071 100644 --- a/src/core/Akka.TestKit/EventFilter/Internal/EventFilterApplier.cs +++ b/src/core/Akka.TestKit/EventFilter/Internal/EventFilterApplier.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Event; using Akka.TestKit.TestEvent; @@ -45,6 +46,20 @@ public void ExpectOne(Action action) InternalExpect(action, _actorSystem, 1); } + public Task ExpectOneAsync(Func actionAsync) + { + return InternalExpectAsync(actionAsync, _actorSystem, 1); + } + + /// + /// Async version of + /// + /// + public async Task ExpectOneAsync(Action action) + { + await InternalExpectAsync(action, _actorSystem, 1); + } + /// /// TBD /// @@ -54,6 +69,15 @@ public void ExpectOne(TimeSpan timeout, Action action) { InternalExpect(action, _actorSystem, 1, timeout); } + + /// + /// Async version of + /// + /// + public async Task ExpectOneAsync(TimeSpan timeout, Action action) + { + await InternalExpectAsync(action, _actorSystem, 1, timeout); + } /// /// TBD @@ -65,6 +89,22 @@ public void Expect(int expectedCount, Action action) InternalExpect(action, _actorSystem, expectedCount, null); } + /// + /// Async version of Expect + /// + public Task ExpectAsync(int expectedCount, Func actionAsync) + { + return InternalExpectAsync(actionAsync, _actorSystem, expectedCount, null); + } + + /// + /// Async version of + /// + public async Task ExpectAsync(int expectedCount, Action action) + { + await InternalExpectAsync(action, _actorSystem, expectedCount, null); + } + /// /// TBD /// @@ -75,6 +115,14 @@ public void Expect(int expectedCount, TimeSpan timeout, Action action) { InternalExpect(action, _actorSystem, expectedCount, timeout); } + + /// + /// Async version of + /// + public async Task ExpectAsync(int expectedCount, TimeSpan timeout, Action action) + { + await InternalExpectAsync(action, _actorSystem, expectedCount, timeout); + } /// /// TBD @@ -86,6 +134,14 @@ public T ExpectOne(Func func) { return Intercept(func, _actorSystem, null, 1); } + + /// + /// Async version of ExpectOne + /// + public async Task ExpectOneAsync(Func func) + { + return await InterceptAsync(func, _actorSystem, null, 1); + } /// /// TBD @@ -98,6 +154,14 @@ public T ExpectOne(TimeSpan timeout, Func func) { return Intercept(func, _actorSystem, timeout, 1); } + + /// + /// Async version of ExpectOne + /// + public async Task ExpectOneAsync(TimeSpan timeout, Func func) + { + return await InterceptAsync(func, _actorSystem, timeout, 1); + } /// /// TBD @@ -111,6 +175,14 @@ public T Expect(int expectedCount, Func func) return Intercept(func, _actorSystem, null, expectedCount); } + /// + /// Async version of Expect + /// + public async Task ExpectAsync(int expectedCount, Func func) + { + return await InterceptAsync(func, _actorSystem, null, expectedCount); + } + /// /// TBD /// @@ -123,6 +195,14 @@ public T Expect(int expectedCount, TimeSpan timeout, Func func) { return Intercept(func, _actorSystem, timeout, expectedCount); } + + /// + /// Async version of Expect + /// + public async Task ExpectAsync(int expectedCount, TimeSpan timeout, Func func) + { + return await InterceptAsync(func, _actorSystem, timeout, expectedCount); + } /// /// TBD @@ -134,6 +214,14 @@ public T Mute(Func func) { return Intercept(func, _actorSystem, null, null); } + + /// + /// Async version of Mute + /// + public async Task MuteAsync(Func func) + { + return await InterceptAsync(func, _actorSystem, null, null); + } /// /// TBD @@ -143,6 +231,14 @@ public void Mute(Action action) { Intercept(() => { action(); return null; }, _actorSystem, null, null); } + + /// + /// Async version of Mute + /// + public async Task MuteAsync(Action action) + { + await InterceptAsync(() => { action(); return null; }, _actorSystem, null, null); + } /// /// TBD @@ -227,6 +323,69 @@ protected T Intercept(Func func, ActorSystem system, TimeSpan? timeout, in } } + /// + /// Async version of + /// + protected Task InterceptAsync(Func func, ActorSystem system, TimeSpan? timeout, int? expectedOccurrences, MatchedEventHandler matchedEventHandler = null) + { + return InterceptAsync(() => Task.FromResult(func()), system, timeout, expectedOccurrences, matchedEventHandler); + } + + /// + /// Async version of + /// + protected async Task InterceptAsync(Func> func, ActorSystem system, TimeSpan? timeout, int? expectedOccurrences, MatchedEventHandler matchedEventHandler = null) + { + var leeway = system.HasExtension() + ? TestKitExtension.For(system).TestEventFilterLeeway + : _testkit.TestKitSettings.TestEventFilterLeeway; + + var timeoutValue = timeout.HasValue ? _testkit.Dilated(timeout.Value) : leeway; + matchedEventHandler = matchedEventHandler ?? new MatchedEventHandler(); + system.EventStream.Publish(new Mute(_filters)); + try + { + foreach(var filter in _filters) + { + filter.EventMatched += matchedEventHandler.HandleEvent; + } + var result = await func(); + + if(!await AwaitDoneAsync(timeoutValue, expectedOccurrences, matchedEventHandler)) + { + var actualNumberOfEvents = matchedEventHandler.ReceivedCount; + string msg; + if(expectedOccurrences.HasValue) + { + var expectedNumberOfEvents = expectedOccurrences.Value; + if(actualNumberOfEvents < expectedNumberOfEvents) + msg = string.Format("Timeout ({0}) while waiting for messages. Only received {1}/{2} messages that matched filter [{3}]", timeoutValue, actualNumberOfEvents, expectedNumberOfEvents, string.Join(",", _filters)); + else + { + var tooMany = actualNumberOfEvents - expectedNumberOfEvents; + msg = string.Format("Received {0} {1} too many. Expected {2} {3} but received {4} that matched filter [{5}]", tooMany, GetMessageString(tooMany), expectedNumberOfEvents, GetMessageString(expectedNumberOfEvents), actualNumberOfEvents, string.Join(",", _filters)); + } + } + else + msg = string.Format("Timeout ({0}) while waiting for messages that matched filter [{1}]", timeoutValue, _filters); + + var assertionsProvider = system.HasExtension() + ? TestKitAssertionsExtension.For(system) + : TestKitAssertionsExtension.For(_testkit.Sys); + assertionsProvider.Assertions.Fail(msg); + } + return result; + } + finally + { + foreach(var filter in _filters) + { + filter.EventMatched -= matchedEventHandler.HandleEvent; + } + system.EventStream.Publish(new Unmute(_filters)); + } + } + /// /// TBD /// @@ -244,6 +403,20 @@ protected bool AwaitDone(TimeSpan timeout, int? expectedOccurrences, MatchedEven } return true; } + + /// + /// Async version of + /// + protected async Task AwaitDoneAsync(TimeSpan timeout, int? expectedOccurrences, MatchedEventHandler matchedEventHandler) + { + if(expectedOccurrences.HasValue) + { + var expected = expectedOccurrences.GetValueOrDefault(); + await _testkit.AwaitConditionNoThrowAsync(() => matchedEventHandler.ReceivedCount >= expected, timeout); + return matchedEventHandler.ReceivedCount == expected; + } + return true; + } /// /// TBD @@ -259,6 +432,22 @@ private void InternalExpect(Action action, ActorSystem actorSystem, int expected { Intercept(() => { action(); return null; }, actorSystem, timeout, expectedCount); } + + /// + /// Async version of + /// + private async Task InternalExpectAsync(Func actionAsync, ActorSystem actorSystem, int expectedCount, TimeSpan? timeout = null) + { + await InterceptAsync(() => { actionAsync(); return Task.FromResult(null); }, actorSystem, timeout, expectedCount); + } + + /// + /// Async version of + /// + private async Task InternalExpectAsync(Action action, ActorSystem actorSystem, int expectedCount, TimeSpan? timeout = null) + { + await InterceptAsync(() => { action(); return Task.FromResult(null); }, actorSystem, timeout, expectedCount); + } /// /// TBD diff --git a/src/core/Akka.TestKit/TestKitBase.cs b/src/core/Akka.TestKit/TestKitBase.cs index b1130f60a22..e0f96da23be 100644 --- a/src/core/Akka.TestKit/TestKitBase.cs +++ b/src/core/Akka.TestKit/TestKitBase.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------- +//----------------------------------------------------------------------- // // Copyright (C) 2009-2019 Lightbend Inc. // Copyright (C) 2013-2019 .NET Foundation @@ -7,6 +7,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Actor.Internal; using Akka.Configuration; @@ -134,6 +135,7 @@ protected void InitializeTest(ActorSystem system, Config config, string actorSys var testActor = CreateTestActor(system, testActorName); //Wait for the testactor to start + // Calling sync version here, since .Wait() causes deadlock AwaitCondition(() => { var repRef = testActor as IRepointableRef; diff --git a/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs b/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs index 6b202fa9d5d..55ad7693d8d 100644 --- a/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs +++ b/src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs @@ -7,6 +7,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Akka.TestKit.Internal; namespace Akka.TestKit @@ -54,5 +55,44 @@ public void AwaitAssert(Action assertion, TimeSpan? duration=null, TimeSpan? int t = (stop - Now).Min(intervalValue); } } + + /// + /// Await until the given assertion does not throw an exception or the timeout + /// expires, whichever comes first. If the timeout expires the last exception + /// is thrown. + /// The action is called, and if it throws an exception the thread sleeps + /// the specified interval before retrying. + /// If no timeout is given, take it from the innermost enclosing `within` + /// block. + /// Note that the timeout is scaled using , + /// which uses the configuration entry "akka.test.timefactor". + /// + /// The action. + /// The timeout. + /// The interval to wait between executing the assertion. + public async Task AwaitAssertAsync(Action assertion, TimeSpan? duration=null, TimeSpan? interval=null) + { + var intervalValue = interval.GetValueOrDefault(TimeSpan.FromMilliseconds(800)); + if(intervalValue == Timeout.InfiniteTimeSpan) intervalValue = TimeSpan.MaxValue; + intervalValue.EnsureIsPositiveFinite("interval"); + var max = RemainingOrDilated(duration); + var stop = Now + max; + var t = max.Min(intervalValue); + while(true) + { + try + { + assertion(); + return; + } + catch(Exception) + { + if(Now + t >= stop) + throw; + } + await Task.Delay(t); + t = (stop - Now).Min(intervalValue); + } + } } } diff --git a/src/core/Akka.TestKit/TestKitBase_AwaitConditions.cs b/src/core/Akka.TestKit/TestKitBase_AwaitConditions.cs index 7f1c128d51a..46dd40c2429 100644 --- a/src/core/Akka.TestKit/TestKitBase_AwaitConditions.cs +++ b/src/core/Akka.TestKit/TestKitBase_AwaitConditions.cs @@ -7,6 +7,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Akka.Event; using Akka.TestKit.Internal; @@ -37,6 +38,27 @@ public void AwaitCondition(Func conditionIsFulfilled) var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; InternalAwaitCondition(conditionIsFulfilled, maxDur, interval, (format, args) => _assertions.Fail(format, args), logger); } + + /// + /// Await until the given condition evaluates to true or until a timeout + /// The timeout is taken from the innermost enclosing `within` + /// block (if inside a `within` block) or the value specified in config value "akka.test.single-expect-default". + /// The value is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor".. + /// A call to is done immediately, then the threads sleep + /// for about a tenth of the timeout value, before it checks the condition again. This is repeated until + /// timeout or the condition evaluates to true. To specify another interval, use the overload + /// + /// + /// + /// The condition that must be fulfilled within the duration. + public async Task AwaitConditionAsync(Func conditionIsFulfilled) + { + var maxDur = RemainingOrDefault; + var interval = new TimeSpan(maxDur.Ticks / 10); + var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; + await InternalAwaitConditionAsync(conditionIsFulfilled, maxDur, interval, (format, args) => _assertions.Fail(format, args), logger); + } /// /// Await until the given condition evaluates to true or the timeout @@ -63,6 +85,32 @@ public void AwaitCondition(Func conditionIsFulfilled, TimeSpan? max) var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; InternalAwaitCondition(conditionIsFulfilled, maxDur, interval, (format, args) => _assertions.Fail(format, args), logger); } + + /// + /// Await until the given condition evaluates to true or the timeout + /// expires, whichever comes first. + /// If no timeout is given, take it from the innermost enclosing `within` + /// block (if inside a `within` block) or the value specified in config value "akka.test.single-expect-default". + /// The value is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor".. + /// A call to is done immediately, then the threads sleep + /// for about a tenth of the timeout value, before it checks the condition again. This is repeated until + /// timeout or the condition evaluates to true. To specify another interval, use the overload + /// + /// + /// + /// The condition that must be fulfilled within the duration. + /// The maximum duration. If undefined, uses the remaining time + /// (if inside a `within` block) or the value specified in config value "akka.test.single-expect-default". + /// The value is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor". + public async Task AwaitConditionAsync(Func conditionIsFulfilled, TimeSpan? max) + { + var maxDur = RemainingOrDilated(max); + var interval = new TimeSpan(maxDur.Ticks / 10); + var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; + await InternalAwaitConditionAsync(conditionIsFulfilled, maxDur, interval, (format, args) => _assertions.Fail(format, args), logger); + } /// /// Await until the given condition evaluates to true or the timeout @@ -90,6 +138,33 @@ public void AwaitCondition(Func conditionIsFulfilled, TimeSpan? max, strin var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; InternalAwaitCondition(conditionIsFulfilled, maxDur, interval, (format, args) => AssertionsFail(format, args, message), logger); } + + /// + /// Await until the given condition evaluates to true or the timeout + /// expires, whichever comes first. + /// If no timeout is given, take it from the innermost enclosing `within` + /// block (if inside a `within` block) or the value specified in config value "akka.test.single-expect-default". + /// The value is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor".. + /// A call to is done immediately, then the threads sleep + /// for about a tenth of the timeout value, before it checks the condition again. This is repeated until + /// timeout or the condition evaluates to true. To specify another interval, use the overload + /// + /// + /// + /// The condition that must be fulfilled within the duration. + /// The maximum duration. If undefined, uses the remaining time + /// (if inside a `within` block) or the value specified in config value "akka.test.single-expect-default". + /// The value is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor". + /// The message used if the timeout expires. + public async Task AwaitConditionAsync(Func conditionIsFulfilled, TimeSpan? max, string message) + { + var maxDur = RemainingOrDilated(max); + var interval = new TimeSpan(maxDur.Ticks / 10); + var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; + await InternalAwaitConditionAsync(conditionIsFulfilled, maxDur, interval, (format, args) => AssertionsFail(format, args, message), logger); + } /// /// Await until the given condition evaluates to true or the timeout @@ -124,13 +199,46 @@ public void AwaitCondition(Func conditionIsFulfilled, TimeSpan? max, TimeS var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; InternalAwaitCondition(conditionIsFulfilled, maxDur, interval, (format, args) => AssertionsFail(format, args, message), logger); } + + /// + /// Await until the given condition evaluates to true or the timeout + /// expires, whichever comes first. + /// If no timeout is given, take it from the innermost enclosing `within` + /// block. + /// Note that the timeout is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor". + /// The parameter specifies the time between calls to + /// Between calls the thread sleeps. If is undefined the thread only sleeps + /// one time, using the as duration, and then rechecks the condition and ultimately + /// succeeds or fails. + /// To make sure that tests run as fast as possible, make sure you do not leave this value as undefined, + /// instead set it to a relatively small value. + /// + /// The condition that must be fulfilled within the duration. + /// The maximum duration. If undefined, uses the remaining time + /// (if inside a `within` block) or the value specified in config value "akka.test.single-expect-default". + /// The value is dilated, i.e. scaled by the factor + /// specified in config value "akka.test.timefactor". + /// The time between calls to to check + /// if the condition is fulfilled. Between calls the thread sleeps. If undefined, negative or + /// the thread only sleeps one time, using the , + /// and then rechecks the condition and ultimately succeeds or fails. + /// To make sure that tests run as fast as possible, make sure you do not set this value as undefined, + /// instead set it to a relatively small value. + /// + /// The message used if the timeout expires. + public async Task AwaitConditionAsync(Func conditionIsFulfilled, TimeSpan? max, TimeSpan? interval, string message = null) + { + var maxDur = RemainingOrDilated(max); + var logger = _testState.TestKitSettings.LogTestKitCalls ? _testState.Log : null; + await InternalAwaitConditionAsync(conditionIsFulfilled, maxDur, interval, (format, args) => AssertionsFail(format, args, message), logger); + } private void AssertionsFail(string format, object[] args, string message = null) { _assertions.Fail(format + (message ?? ""), args); } - /// /// Await until the given condition evaluates to true or the timeout /// expires, whichever comes first. Returns true if the condition was fulfilled. @@ -148,7 +256,24 @@ public bool AwaitConditionNoThrow(Func conditionIsFulfilled, TimeSpan max, var intervalDur = interval.GetValueOrDefault(TimeSpan.FromMilliseconds(100)); return InternalAwaitCondition(conditionIsFulfilled, max, intervalDur, (f, a) => { }); } - + + /// + /// Await until the given condition evaluates to true or the timeout + /// expires, whichever comes first. Returns true if the condition was fulfilled. + /// The parameter specifies the time between calls to + /// Between calls the thread sleeps. If is not specified or null 100 ms is used. + /// + /// The condition that must be fulfilled within the duration. + /// The maximum duration. + /// Optional. The time between calls to to check + /// if the condition is fulfilled. Between calls the thread sleeps. If undefined, 100 ms is used + /// + /// TBD + public Task AwaitConditionNoThrowAsync(Func conditionIsFulfilled, TimeSpan max, TimeSpan? interval = null) + { + var intervalDur = interval.GetValueOrDefault(TimeSpan.FromMilliseconds(100)); + return InternalAwaitConditionAsync(conditionIsFulfilled, max, intervalDur, (f, a) => { }); + } /// /// Await until the given condition evaluates to true or the timeout @@ -181,6 +306,19 @@ protected static bool InternalAwaitCondition(Func conditionIsFulfilled, Ti { return InternalAwaitCondition(conditionIsFulfilled, max, interval, fail, null); } + + /// + /// Async version of + /// + /// + /// + /// + /// + /// + protected static Task InternalAwaitConditionAsync(Func conditionIsFulfilled, TimeSpan max, TimeSpan? interval, Action fail) + { + return InternalAwaitConditionAsync(conditionIsFulfilled, max, interval, fail, null); + } /// /// Await until the given condition evaluates to true or the timeout @@ -234,6 +372,34 @@ protected static bool InternalAwaitCondition(Func conditionIsFulfilled, Ti ConditionalLog(logger, "Condition fulfilled after {0}", Now-start); return true; } + + /// + /// Async version of + /// + protected static async Task InternalAwaitConditionAsync(Func conditionIsFulfilled, TimeSpan max, TimeSpan? interval, Action fail, ILoggingAdapter logger) + { + max.EnsureIsPositiveFinite("max"); + var start = Now; + var stop = start + max; + ConditionalLog(logger, "Awaiting condition for {0}.{1}", max, interval.HasValue ? " Will sleep " + interval.Value + " between checks" : ""); + + while (!conditionIsFulfilled()) + { + var now = Now; + + if (now > stop) + { + const string message = "Timeout {0} expired while waiting for condition."; + ConditionalLog(logger, message, max); + fail(message, new object[] { max }); + return false; + } + var sleepDuration = (stop - now).Min(interval); + await Task.Delay(sleepDuration); + } + ConditionalLog(logger, "Condition fulfilled after {0}", Now-start); + return true; + } private static void ConditionalLog(ILoggingAdapter logger, string format, params object[] args) { diff --git a/src/core/Akka.TestKit/TestKitBase_Within.cs b/src/core/Akka.TestKit/TestKitBase_Within.cs index 0556d525dd0..6ed21924456 100644 --- a/src/core/Akka.TestKit/TestKitBase_Within.cs +++ b/src/core/Akka.TestKit/TestKitBase_Within.cs @@ -7,6 +7,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Akka.TestKit.Internal; namespace Akka.TestKit @@ -30,6 +31,14 @@ public void Within(TimeSpan max, Action action, TimeSpan? epsilonValue = null) { Within(TimeSpan.Zero, max, action, epsilonValue: epsilonValue); } + + /// + /// Async version of Within + /// + public Task WithinAsync(TimeSpan max, Func actionAsync, TimeSpan? epsilonValue = null) + { + return WithinAsync(TimeSpan.Zero, max, actionAsync, epsilonValue: epsilonValue); + } /// /// Execute code block while bounding its execution time between and . @@ -47,7 +56,14 @@ public void Within(TimeSpan min, TimeSpan max, Action action, string hint = null { Within(min, max, () => { action(); return null; }, hint, epsilonValue); } - + + /// + /// Async version of + /// + public Task WithinAsync(TimeSpan min, TimeSpan max, Func actionAsync, string hint = null, TimeSpan? epsilonValue = null) + { + return WithinAsync(min, max, async () => { await actionAsync(); return null; }, hint, epsilonValue); + } /// /// Execute code block while bounding its execution time between 0 seconds and . @@ -65,6 +81,23 @@ public T Within(TimeSpan max, Func function, TimeSpan? epsilonValue = null { return Within(TimeSpan.Zero, max, function, epsilonValue: epsilonValue); } + + /// + /// 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 + /// are available in a version which implicitly uses the remaining time governed by + /// the innermost enclosing `within` block. + /// Note that the max duration is scaled using which uses the config value "akka.test.timefactor" + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + public Task WithinAsync(TimeSpan max, Func> function, TimeSpan? epsilonValue = null) + { + return WithinAsync(TimeSpan.Zero, max, function, epsilonValue: epsilonValue); + } /// /// Execute code block while bounding its execution time between and . @@ -128,5 +161,66 @@ public T Within(TimeSpan min, TimeSpan max, Func function, string hint = n return ret; } + /// + /// 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 + /// are available in a version which implicitly uses the remaining time governed by + /// the innermost enclosing `within` block. + /// Note that the max duration is scaled using which uses the config value "akka.test.timefactor" + /// + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + /// TBD + public async Task WithinAsync(TimeSpan min, TimeSpan max, Func> function, string hint = null, TimeSpan? epsilonValue = null) + { + 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 = await 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; + } } } diff --git a/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs b/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs index 3df9e710c32..f19ff092269 100644 --- a/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs +++ b/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.TestKit.Internal.StringMatcher; @@ -150,6 +151,19 @@ protected void Intercept(Action actionThatThrows) } throw new ThrowsException(typeof(Exception)); } + + protected async Task InterceptAsync(Func asyncActionThatThrows) + { + try + { + await asyncActionThatThrows(); + } + catch(Exception) + { + return; + } + throw new ThrowsException(typeof(Exception)); + } [Obsolete("Use Intercept instead. This member will be removed.")] protected void intercept(Action actionThatThrows) where T : Exception