All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatest.concurrent.Eventually.scala Maven / Gradle / Ivy

There is a newer version: 2.0.M6-SNAP27
Show newest version
/*
 * Copyright 2001-2012 Artima, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.scalatest.concurrent

import org.scalatest._
import exceptions.{TestFailedDueToTimeoutException, TestFailedException, TestPendingException}
import org.scalatest.exceptions.StackDepthExceptionHelper.getStackDepthFun
import org.scalatest.Suite.anErrorThatShouldCauseAnAbort
import scala.annotation.tailrec
import time.{Nanosecond, Span, Nanoseconds}

// TODO describe backoff algo

/**
 * Trait that provides the eventually construct, which periodically retries executing
 * a passed by-name parameter, until it either succeeds or the configured timeout has been surpassed.
 *
 * 

* The by-name parameter "succeeds" if it returns a result. It "fails" if it throws any exception that * would normally cause a test to fail. (These are any exceptions except TestPendingException and * Errors listed in the * Treatment of java.lang.Errors section of the * documentation of trait Suite.) *

* *

* For example, the following invocation of eventually would succeed (not throw an exception): *

* *
 * val xs = 1 to 125
 * val it = xs.iterator
 * eventually { it.next should be (3) }
 * 
* *

* However, because the default timeout is 150 milliseconds, the following invocation of * eventually would ultimately produce a TestFailedDueToTimeoutException: *

* * *
 * val xs = 1 to 125
 * val it = xs.iterator
 * eventually { Thread.sleep(50); it.next should be (110) }
 * 
* *

* Assuming the default configuration parameters, a timeout of 150 milliseconds and an interval of 15 milliseconds, * were passed implicitly to eventually, the detail message of the thrown * TestFailedDueToTimeoutException would look like: *

* *

* The code passed to eventually never returned normally. Attempted 2 times over 166.682 milliseconds. Last failure message: 2 was not equal to 110. *

* *

* The cause of the thrown TestFailedDueToTimeoutException will be the exception most recently thrown by the block of code passed to eventually. (In * the previous example, the cause would be the TestFailedException with the detail message 2 was not equal to 100.) *

* *

Configuration of eventually

* *

* The eventually methods of this trait can be flexibly configured. * The two configuration parameters for eventually along with their * default values and meanings are described in the following table: *

* * * * * * * * * * * * * * * * * *
* Configuration Parameter * * Default Value * * Meaning *
* timeout * * scaled(150 milliseconds) * * the maximum amount of time to allow unsuccessful attempts before giving up and throwing TestFailedException *
* interval * * scaled(15 milliseconds) * * the amount of time to sleep between each attempt *
* *

* The default values of both timeout and interval are passed to the scaled method, inherited * from ScaledTimeSpans, so that the defaults can be scaled up * or down together with other scaled time spans. See the documentation for trait ScaledTimeSpans * for more information. *

* *

* The eventually methods of trait Eventually each take an PatienceConfig * object as an implicit parameter. This object provides values for the two configuration parameters. (These configuration parameters * are called "patience" because they determine how patient tests will be with asynchronous operations: how long * they will tolerate failures before giving up and how long they will wait before checking again after a failure.) Trait * Eventually provides an implicit val named patienceConfig with each * configuration parameter set to its default value. * If you want to set one or more configuration parameters to a different value for all invocations of * eventually in a suite you can override this * val (or hide it, for example, if you are importing the members of the Eventually companion object rather * than mixing in the trait). For example, if * you always want the default timeout to be 2 seconds and the default interval to be 5 milliseconds, you * can override patienceConfig, like this: * *

 * implicit override val patienceConfig =
 *   PatienceConfig(timeout = scaled(Span(2, Seconds)), interval = scaled(Span(5, Millis)))
 * 
* *

* Or, hide it by declaring a variable of the same name in whatever scope you want the changed values to be in effect: *

* *
 * implicit val patienceConfig =
 *   PatienceConfig(timeout = scaled(Span(2, Seconds)), interval = scaled(Span(5, Millis)))
 * 
* *

* Passing your new default values to scaled is optional, but a good idea because it allows them to * be easily scaled if run on a slower or faster system. *

* *

* In addition to taking a PatienceConfig object as an implicit parameter, the eventually methods of trait * Eventually include overloaded forms that take one or two PatienceConfigParam * objects that you can use to override the values provided by the implicit PatienceConfig for a single eventually * invocation. For example, if you want to set timeout to 5000 for just one particular eventually invocation, * you can do so like this: *

* *
 * eventually (timeout(Span(5, Seconds))) { Thread.sleep(10); it.next should be (110) }
 * 
* *

* This invocation of eventually will use 5 seconds for the timeout and whatever value is specified by the * implicitly passed PatienceConfig object for the interval configuration parameter. * If you want to set both configuration parameters in this way, just list them separated by commas: *

* *
 * eventually (timeout(Span(5, Seconds)), interval(Span(5, Millis))) { it.next should be (110) }
 * 
* *

* You can also import or mix in the members of SpanSugar if * you want a more concise DSL for expressing time spans: *

* *
 * eventually (timeout(5 seconds), interval(5 millis)) { it.next should be (110) }
 * 
* *

* Note that ScalaTest will not scale any time span that is not explicitly passed to scaled to make * the meaning of the code as obvious as possible. Thus * if you ask for "timeout(5 seconds)" you will get exactly that: a timeout of five seconds. If you want such explicitly * given values to be scaled, you must say pass them to scale explicitly like this: *

* *
 * eventually (timeout(scaled(5 seconds))) { it.next should be (110) }
 * 
* *

* The previous code says more clearly that the timeout will be five seconds, unless scaled higher or lower by the scaled method. *

* *

Simple backoff algorithm

* *

* The eventually methods employ a very simple backoff algorithm to try and maximize the speed of tests. If an asynchronous operation * completes quickly, a smaller interval will yield a faster test. But if an asynchronous operation takes a while, a small interval will keep the CPU * busy repeatedly checking and rechecking a not-ready operation, to some extent taking CPU cycles away from other processes that could proceed. To * strike the right balance between these design tradeoffs, the eventually methods will check more frequently during the initial interval. *

* *

* Rather than sleeping an entire interval if the initial attempt fails, eventually will only sleep 1/10 of the configured interval. It * will continue sleeping only 1/10 of the configured interval until the configured interval has passed, after which it sleeps the configured interval * between attempts. Here's an example in which the timeout is set equal to the interval: *

* *
 * val xs = 1 to 125
 * val it = xs.iterator
 * eventually(timeout(100 milliseconds), interval(100 milliseconds)) { it.next should be (110) }
 * 
* *

* Even though this call to eventually will time out after only one interval, approximately, the error message will likely report that more * than one (and less than ten) attempts were made: *

* *

* The code passed to eventually never returned normally. Attempted 6 times over 100.485 milliseconds. Last failure message: 6 was not equal to 110. *

* *

* Note that if the initial attempt takes longer than the configured interval to complete, eventually will never sleep for * a 1/10 interval. You can observe this behavior in the second example above in which the first statement in the block of code passed to eventually * was Thread.sleep(50). *

* *

Usage note: Eventually intended primarily for integration testing

* *

* Although the default timeouts of trait Eventually are tuned for unit testing, the use of Eventually in unit tests is * a choice you should question. Usually during unit testing you'll want to mock out subsystems that would require Eventually, such as * network services with varying and unpredictable response times. This will allow your unit tests to run as fast as possible while still testing * the focused bits of behavior they are designed to test. * *

* Nevertheless, because sometimes it will make sense to use Eventually in unit tests (and * because it is destined to happen anyway even when it isn't the best choice), Eventually by default uses * timeouts tuned for unit tests: Calls to eventually are more likely to succeed on fast development machines, and if a call does time out, * it will do so quickly so the unit tests can move on. *

* *

* When you are using Eventually for integration testing, therefore, the default timeout and interval may be too small. A * good way to override them is by mixing in trait IntegrationPatience or a similar trait of your * own making. Here's an example: *

* *
 * class ExampleSpec extends FeatureSpec with Eventually with IntegrationPatience {
 *   // Your integration tests here...
 * }
 * 
* *

* Trait IntegrationPatience increases the default timeout from 150 milliseconds to 15 seconds, the default * interval from 15 milliseconds to 150 milliseconds. If need be, you can do fine tuning of the timeout and interval by * specifying a time span scale factor when you * run your tests. *

* * @author Bill Venners * @author Chua Chee Seng */ trait Eventually extends PatienceConfiguration { /** * Invokes the passed by-name parameter repeatedly until it either succeeds, or a configured maximum * amount of time has passed, sleeping a configured interval between attempts. * *

* The by-name parameter "succeeds" if it returns a result. It "fails" if it throws any exception that * would normally cause a test to fail. (These are any exceptions except TestPendingException and * Errors listed in the * Treatment of java.lang.Errors section of the * documentation of trait Suite.) *

* *

* The maximum amount of time in milliseconds to tolerate unsuccessful attempts before giving up and throwing * TestFailedException is configured by the value contained in the passed * timeout parameter. * The interval to sleep between attempts is configured by the value contained in the passed * interval parameter. *

* * @param timeout the Timeout configuration parameter * @param interval the Interval configuration parameter * @param fun the by-name parameter to repeatedly invoke * @return the result of invoking the fun by-name parameter, the first time it succeeds */ def eventually[T](timeout: Timeout, interval: Interval)(fun: => T): T = eventually(fun)(PatienceConfig(timeout.value, interval.value)) /** * Invokes the passed by-name parameter repeatedly until it either succeeds, or a configured maximum * amount of time has passed, sleeping a configured interval between attempts. * *

* The by-name parameter "succeeds" if it returns a result. It "fails" if it throws any exception that * would normally cause a test to fail. (These are any exceptions except TestPendingException and * Errors listed in the * Treatment of java.lang.Errors section of the * documentation of trait Suite.) *

* *

* The maximum amount of time in milliseconds to tolerate unsuccessful attempts before giving up and throwing * TestFailedException is configured by the value contained in the passed * timeout parameter. * The interval to sleep between attempts is configured by the interval field of * the PatienceConfig passed implicitly as the last parameter. *

* * @param timeout the Timeout configuration parameter * @param fun the by-name parameter to repeatedly invoke * @param config the PatienceConfig object containing the (unused) timeout and * (used) interval parameters * @return the result of invoking the fun by-name parameter, the first time it succeeds */ def eventually[T](timeout: Timeout)(fun: => T)(implicit config: PatienceConfig): T = eventually(fun)(PatienceConfig(timeout.value, config.interval)) /** * Invokes the passed by-name parameter repeatedly until it either succeeds, or a configured maximum * amount of time has passed, sleeping a configured interval between attempts. * *

* The by-name parameter "succeeds" if it returns a result. It "fails" if it throws any exception that * would normally cause a test to fail. (These are any exceptions except TestPendingException and * Errors listed in the * Treatment of java.lang.Errors section of the * documentation of trait Suite.) *

* *

* The maximum amount of time in milliseconds to tolerate unsuccessful attempts before giving up is configured by the timeout field of * the PatienceConfig passed implicitly as the last parameter. * The interval to sleep between attempts is configured by the value contained in the passed * interval parameter. *

* * @param interval the Interval configuration parameter * @param fun the by-name parameter to repeatedly invoke * @param config the PatienceConfig object containing the (used) timeout and * (unused) interval parameters * @return the result of invoking the fun by-name parameter, the first time it succeeds */ def eventually[T](interval: Interval)(fun: => T)(implicit config: PatienceConfig): T = eventually(fun)(PatienceConfig(config.timeout, interval.value)) /** * Invokes the passed by-name parameter repeatedly until it either succeeds, or a configured maximum * amount of time has passed, sleeping a configured interval between attempts. * *

* The by-name parameter "succeeds" if it returns a result. It "fails" if it throws any exception that * would normally cause a test to fail. (These are any exceptions except TestPendingException and * Errors listed in the * Treatment of java.lang.Errors section of the * documentation of trait Suite.) *

* *

* The maximum amount of time in milliseconds to tolerate unsuccessful attempts before giving up is configured by the timeout field of * the PatienceConfig passed implicitly as the last parameter. * The interval to sleep between attempts is configured by the interval field of * the PatienceConfig passed implicitly as the last parameter. *

* * @param fun the by-name parameter to repeatedly invoke * @param config the PatienceConfig object containing the timeout and * interval parameters * @return the result of invoking the fun by-name parameter, the first time it succeeds */ def eventually[T](fun: => T)(implicit config: PatienceConfig): T = { val startNanos = System.nanoTime def makeAValiantAttempt(): Either[Throwable, T] = { try { Right(fun) } catch { case tpe: TestPendingException => throw tpe case e: Throwable if !anErrorThatShouldCauseAnAbort(e) => Left(e) } } val initialInterval = Span(config.interval.totalNanos * 0.1, Nanoseconds) // config.interval scaledBy 0.1 @tailrec def tryTryAgain(attempt: Int): T = { val timeout = config.timeout val interval = config.interval makeAValiantAttempt() match { case Right(result) => result case Left(e) => val duration = System.nanoTime - startNanos if (duration < timeout.totalNanos) { if (duration < interval.totalNanos) // For first interval, we wake up every 1/10 of the interval. This is mainly for optimization purpose. Thread.sleep(initialInterval.millisPart, initialInterval.nanosPart) else Thread.sleep(interval.millisPart, interval.nanosPart) } else { val durationSpan = Span(1, Nanosecond) scaledBy duration // Use scaledBy to get pretty units def msg = if (e.getMessage == null) Resources("didNotEventuallySucceed", attempt.toString, durationSpan.prettyString) else Resources("didNotEventuallySucceedBecause", attempt.toString, durationSpan.prettyString, e.getMessage) throw new TestFailedDueToTimeoutException( sde => Some(msg), Some(e), getStackDepthFun("Eventually.scala", "eventually"), None, config.timeout ) } tryTryAgain(attempt + 1) } } tryTryAgain(1) } } /** * Companion object that facilitates the importing of Eventually members as * an alternative to mixing in the trait. One use case is to import Eventually's members so you can use * them in the Scala interpreter: * *
 * $ scala -cp scalatest-1.8.jar
 * Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_29).
 * Type in expressions to have them evaluated.
 * Type :help for more information.
 *
 * scala> import org.scalatest._
 * import org.scalatest._
 *
 * scala> import matchers.ShouldMatchers._
 * import matchers.ShouldMatchers._
 *
 * scala> import concurrent.Eventually._
 * import concurrent.Eventually._
 *
 * scala> val xs = 1 to 125
 * xs: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 125)
 * 
 * scala> val it = xs.iterator
 * it: Iterator[Int] = non-empty iterator
 *
 * scala> eventually { it.next should be (3) }
 *
 * scala> eventually { Thread.sleep(999); it.next should be (3) }
 * org.scalatest.TestFailedException: The code passed to eventually never returned normally.
 *     Attempted 2 times, sleeping 10 milliseconds between each attempt.
 *   at org.scalatest.Eventually$class.tryTryAgain$1(Eventually.scala:313)
 *   at org.scalatest.Eventually$class.eventually(Eventually.scala:322)
 *   ...
 * 
*/ object Eventually extends Eventually




© 2015 - 2024 Weber Informatics LLC | Privacy Policy