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

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

The newest version!
/*
 * Copyright 2001-2013 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 Assertions.fail
import org.scalatest.exceptions.NotAllowedException
import org.scalatest.exceptions.TestFailedException
import time.{Nanoseconds, Second, Span}
import PatienceConfiguration._

/**
 * Trait that facilitates performing assertions outside the main test thread, such as assertions in callback methods
 * that are invoked asynchronously.
 *
 * 

* Trait AsyncAssertions provides a Waiter class that you can use to orchestrate the inter-thread * communication required to perform assertions outside the main test thread, and a means to configure it. *

* *

* To use Waiter, create an instance of it in the main test thread: *

* *
 * val w = new Waiter // Do this in the main test thread
 * 
* *

* At some point later, call await on the waiter: *

* *
 * w.await() // Call await() from the main test thread
 * 
* *

* The await call will block until it either receives a report of a failed assertion from a different thread, at which * point it will complete abruptly with the same exception, or until it is dismissed by a different thread (or threads), at * which point it will return normally. You can optionally specify a timeout and/or a number * of dismissals to wait for. Here's an example: *

* *
 * import org.scalatest.time.SpanSugar._
 *
 * w.await(timeout(300 millis), dismissals(2))
 * 
* *

* The default value for timeout, provided via an implicit PatienceConfig parameter, is 150 milliseconds. The default value for * dismissals is 1. The await method will block until either it is dismissed a sufficient number of times by other threads or * an assertion fails in another thread. Thus if you just want to perform assertions in just one other thread, only that thread will be * performing a dismissal, so you can use the default value of 1 for dismissals. *

* *

* Waiter contains four overloaded forms of await, two of which take an implicit * PatienceConfig parameter. To change the default timeout configuration, override or hide * (if you imported the members of AsyncAssertions companion object instead of mixing in the * trait) patienceConfig with a new one that returns your desired configuration. *

* *

* To dismiss a waiter, you just invoke dismiss on it: *

* *
 * w.dismiss() // Call this from one or more other threads
 * 
* *

* You may want to put dismiss invocations in a finally clause to ensure they happen even if an exception is thrown. * Otherwise if a dismissal is missed because of a thrown exception, an await call will wait until it times out. *

* *

* Finally, to perform an assertion in a different thread, you just apply the Waiter to the assertion code. Here are * some examples: *

* *
 * w { assert(1 + 1 === 3) }    // Can use assertions
 * w { 1 + 1 should equal (3) } // Or matchers
 * w { "hi".charAt(-1) }        // Any exceptions will be forwarded to await
 * 
* *

* Here's a complete example: *

* *
 * import org.scalatest._
 * import concurrent.AsyncAssertions
 * import matchers.ShouldMatchers
 * import scala.actors.Actor
 *
 * class ExampleSuite extends FunSuite with ShouldMatchers with AsyncAssertions {
 *
 *   case class Message(text: String)
 *
 *   class Publisher extends Actor {
 *
 *     @volatile private var handle: Message => Unit = { (msg) => }
 *
 *     def registerHandler(f: Message => Unit) {
 *       handle = f
 *     }
 *
 *     def act() {
 *       var done = false
 *       while (!done) {
 *         react {
 *           case msg: Message => handle(msg)
 *           case "Exit" => done = true
 *         }
 *       }
 *     }
 *   }
 *
 *   test("example one") {
 *
 *     val publisher = new Publisher
 *     val message = new Message("hi")
 *     val w = new Waiter
 *
 *     publisher.start()
 *
 *     publisher.registerHandler { msg =>
 *       w { msg should equal (message) }
 *       w.dismiss()
 *     }
 *
 *     publisher ! message
 *     w.await()
 *     publisher ! "Exit"
 *   }
 * }
 * 
* * @author Bill Venners */ trait AsyncAssertions extends PatienceConfiguration { /** * A configuration parameter that specifies the number of dismissals to wait for before returning normally * from an await call on a Waiter. * * @param value the number of dismissals for which to wait * @throws IllegalArgumentException if specified value is less than or equal to zero. * * @author Bill Venners */ final case class Dismissals(value: Int) // TODO check for IAE if negative /** * Returns a Dismissals configuration parameter containing the passed value, which * specifies the number of dismissals to wait for before returning normally from an await * call on a Waiter. */ def dismissals(value: Int) = Dismissals(value) /** * Class that facilitates performing assertions outside the main test thread, such as assertions in callback methods * that are invoked asynchronously. * *

* To use Waiter, create an instance of it in the main test thread: *

* *
   * val w = new Waiter // Do this in the main test thread
   * 
* *

* At some point later, call await on the waiter: *

* *
   * w.await() // Call await() from the main test thread
   * 
* *

* The await call will block until it either receives a report of a failed assertion from a different thread, at which * point it will complete abruptly with the same exception, or until it is dismissed by a different thread (or threads), at * which point it will return normally. You can optionally specify a timeout and/or a number * of dismissals to wait for. Here's an example: *

* *
   * import org.scalatest.time.SpanSugar._
   *
   * w.await(timeout(300 millis), dismissals(2))
   * 
* *

* The default value for timeout, provided via an implicit PatienceConfig parameter, is 150 milliseconds. The default value for * dismissals is 1. The await method will block until either it is dismissed a sufficient number of times by other threads or * an assertion fails in another thread. Thus if you just want to perform assertions in just one other thread, only that thread will be * performing a dismissal, so you can use the default value of 1 for dismissals. *

* *

* Waiter contains four overloaded forms of await, two of which take an implicit * PatienceConfig parameter. To change the default timeout configuration, override or hide * (if you imported the members of AsyncAssertions companion object instead of mixing in the * trait) patienceConfig with a new one that returns your desired configuration. *

* *

* To dismiss a waiter, you just invoke dismiss on it: *

* *
   * w.dismiss() // Call this from one or more other threads
   * 
* *

* You may want to put dismiss invocations in a finally clause to ensure they happen even if an exception is thrown. * Otherwise if a dismissal is missed because of a thrown exception, an await call will wait until it times out. *

* *

* Finally, to perform an assertion in a different thread, you just apply the Waiter to the assertion code. Here are * some examples: *

* *
   * w { assert(1 + 1 === 3) }    // Can use assertions
   * w { 1 + 1 should equal (3) } // Or matchers
   * w { "hi".charAt(-1) }        // Any exceptions will be forwarded to await
   * 
* *

* Here's a complete example: *

* *
   * import org.scalatest._
   * import concurrent.AsyncAssertions
   * import matchers.ShouldMatchers
   * import scala.actors.Actor
   *
   * class ExampleSuite extends FunSuite with ShouldMatchers with AsyncAssertions {
   *
   *   case class Message(text: String)
   *
   *   class Publisher extends Actor {
   *
   *     @volatile private var handle: Message => Unit = { (msg) => }
   *
   *     def registerHandler(f: Message => Unit) {
   *       handle = f
   *     }
   *
   *     def act() {
   *       var done = false
   *       while (!done) {
   *         react {
   *           case msg: Message => handle(msg)
   *           case "Exit" => done = true
   *         }
   *       }
   *     }
   *   }
   *
   *   test("example one") {
   *
   *     val publisher = new Publisher
   *     val message = new Message("hi")
   *     val w = new Waiter
   *
   *     publisher.start()
   *
   *     publisher.registerHandler { msg =>
   *       w { msg should equal (message) }
   *       w.dismiss()
   *     }
   *
   *     publisher ! message
   *     w.await()
   *     publisher ! "Exit"
   *   }
   * }
   * 
* * @author Bill Venners */ class Waiter { private final val creatingThread = Thread.currentThread /* * @volatile is not sufficient to ensure atomic compare and set, or increment. * Given that the code which modifies these variables should be synchronized anyway * (because these variables are used in the wait condition predicate), * the @volatile is superfluous. */ /* @volatile */ private var dismissedCount = 0 /* @volatile */ private var thrown: Option[Throwable] = None private def setThrownIfEmpty(t: Throwable) { /* * synchronized to serialize access to `thrown` which is used in the wait condition, * and to ensure the compare and set is atomic. * (Arguably, a race condition on setting `thrown` is harmless, but synchronization makes * the class easier to reason about.) */ synchronized { if (thrown.isEmpty) thrown = Some(t) } } /** * Executes the passed by-name, and if it throws an exception, forwards it to the thread that calls await, unless * a by-name passed during a previous invocation of this method threw an exception. * *

* This method returns normally whether or not the passed function completes abruptly. If called multiple times, only the * first invocation that yields an exception will "win" and have its exception forwarded to the thread that calls await. * Any subsequent exceptions will be "swallowed." This method may be invoked by multiple threads concurrently, in which case it is a race * to see who wins and has their exception forwarded to await. The await call will eventually complete * abruptly with the winning exception, or return normally if that instance of Waiter is dismissed. Any exception thrown by * a by-name passed to apply after the Waiter has been dismissed will also be "swallowed." *

* * @param fun the by-name function to execute */ def apply(fun: => Unit) { try { fun } catch { // Exceptions after the first are swallowed (need to get to dismissals later) case t: Throwable => setThrownIfEmpty(t) synchronized { notifyAll() } } } /** * Wait for an exception to be produced by the by-name passed to apply or the specified number of dismissals. * *

* This method may only be invoked by the thread that created the Waiter. * Each time this method completes, its internal dismissal count is reset to zero, so it can be invoked multiple times. However, * once await has completed abruptly with an exception produced during a call to apply, it will continue * to complete abruptly with that exception. The default value for the dismissals parameter is 1. *

* *

* The timeout parameter allows you to specify a timeout after which a TestFailedException will be thrown with * a detail message indicating the await call timed out. The default value for timeout is -1, which indicates * no timeout at all. Any positive value (or zero) will be interpreted as a timeout expressed in milliseconds. If no calls to apply * have produced an exception and an insufficient number of dismissals has been received by the time the timeout number * of milliseconds has passed, await will complete abruptly with TestFailedException. *

* * @param timeout the number of milliseconds timeout, or -1 to indicate no timeout (default is -1) * @param dismissals the number of dismissals to wait for (default is 1) */ private def awaitImpl(timeout: Span, dismissals: Int = 1) { if (Thread.currentThread != creatingThread) throw new NotAllowedException(Resources("awaitMustBeCalledOnCreatingThread"), 2) val startTime: Long = System.nanoTime val endTime: Long = startTime + timeout.totalNanos def timedOut: Boolean = endTime < System.nanoTime synchronized { while (dismissedCount < dismissals && !timedOut && thrown.isEmpty) { val timeLeft: Span = { val diff = endTime - System.nanoTime if (diff > 0) Span(diff, Nanoseconds) else Span.ZeroLength } wait(timeLeft.millisPart, timeLeft.nanosPart) } } // it should never be the case that we get all the expected dismissals and still throw // a timeout failure - clients trying to debug code would find that very surprising. // Calls to timedOut subsequent to while loop exit constitute a kind of "double jepardy". if (thrown.isDefined) throw thrown.get else if (dismissedCount == dismissals) dismissedCount = 0 // reset the dismissed count to support multiple await calls else if (timedOut) throw new TestFailedException(Resources("awaitTimedOut"), 2) else Predef.assert(false, "unreachable condition - maybe time went backwards?!") } /** * Wait for an exception to be produced by the by-name passed to apply, or one dismissal, * sleeping an interval between checks and timing out after a timeout, both configured * by an implicit PatienceConfig. * *

* This method may only be invoked by the thread that created the Waiter. * Each time this method completes, its internal dismissal count is reset to zero, so it can be invoked multiple times. However, * once await has completed abruptly with an exception produced during a call to apply, it will continue * to complete abruptly with that exception. *

* *

* The timeout parameter allows you to specify a timeout after which a * TestFailedException will be thrown with a detail message indicating the await call * timed out. If no calls to apply have produced an exception and an insufficient number of * dismissals has been received by the time the timeout has expired, await will * complete abruptly with TestFailedException. *

* *

* As used here, a "check" is checking to see whether an exception has been thrown by a by-name passed * to apply or a dismissal has occurred. The "interval" is the amount * of time the thread that calls await will sleep between "checks." *

* * @param config the PatienceConfig object containing the timeout parameter */ def await()(implicit config: PatienceConfig) { awaitImpl(config.timeout) } /** * Wait for an exception to be produced by the by-name passed to apply, or one dismissal, * timing out after the specified timeout and sleeping an interval between checks configured * by an implicit PatienceConfig. * *

* This method may only be invoked by the thread that created the Waiter. * Each time this method completes, its internal dismissal count is reset to zero, so it can be invoked multiple times. However, * once await has completed abruptly with an exception produced during a call to apply, it will continue * to complete abruptly with that exception. *

* *

* The timeout parameter allows you to specify a timeout after which a * TestFailedException will be thrown with a detail message indicating the await call * timed out. If no calls to apply have produced an exception and an insufficient number of * dismissals has been received by the time the timeout has expired, await will * complete abruptly with TestFailedException. *

* *

* As used here, a "check" is checking to see whether an exception has been thrown by a by-name passed * to apply or a dismissal has occurred. The "interval" is the amount * of time the thread that calls await will sleep between "checks." *

* * @param timeout: the Timeout configuration parameter containing the specified timeout */ def await(timeout: Timeout) { awaitImpl(timeout.value) } /** * Wait for an exception to be produced by the by-name passed to apply, or the specified * number of dismissals, sleeping an interval between checks and timing out after a timeout, both configured * by an implicit PatienceConfig. * *

* This method may only be invoked by the thread that created the Waiter. * Each time this method completes, its internal dismissal count is reset to zero, so it can be invoked multiple times. However, * once await has completed abruptly with an exception produced during a call to apply, it will continue * to complete abruptly with that exception. *

* *

* The timeout parameter allows you to specify a timeout after which a * TestFailedException will be thrown with a detail message indicating the await call * timed out. If no calls to apply have produced an exception and an insufficient number of * dismissals has been received by the time the timeout has expired, await will * complete abruptly with TestFailedException. *

* *

* As used here, a "check" is checking to see whether an exception has been thrown by a by-name passed * to apply or the specified number of dismissals has occurred. The "interval" is the amount * of time the thread that calls await will sleep between "checks." *

* * @param dismissals: the Dismissals configuration parameter containing the number of * dismissals for which to wait * @param config the PatienceConfig object containing the timeout parameter */ def await(dismissals: Dismissals)(implicit config: PatienceConfig) { awaitImpl(config.timeout, dismissals.value) } /** * Wait for an exception to be produced by the by-name passed to apply, or the specified * number of dismissals, timing out after the specified timeout and sleeping an interval between checks configured * by an implicit PatienceConfig. * *

* This method may only be invoked by the thread that created the Waiter. * Each time this method completes, its internal dismissal count is reset to zero, so it can be invoked multiple times. However, * once await has completed abruptly with an exception produced during a call to apply, it will continue * to complete abruptly with that exception. *

* *

* The timeout parameter allows you to specify a timeout after which a * TestFailedException will be thrown with a detail message indicating the await call * timed out. If no calls to apply have produced an exception and an insufficient number of * dismissals has been received by the time the timeout has expired, await will * complete abruptly with TestFailedException. *

* *

* As used here, a "check" is checking to see whether an exception has been thrown by a by-name passed * to apply or the specified number of dismissals has occurred. The "interval" is the amount * of time the thread that calls await will sleep between "checks." *

* * @param timeout: the Timeout configuration parameter containing the specified timeout * @param dismissals: the Dismissals configuration parameter containing the number of * dismissals for which to wait */ def await(timeout: Timeout, dismissals: Dismissals) { awaitImpl(timeout.value, dismissals.value) } /** * Increases the dismissal count by one. * *

* Once the dismissal count has reached the value passed to await (and no prior invocations of apply * produced an exception), await will return normally. *

*/ def dismiss() { /* * Synchronized to serialize access to `dismissedCount` used in the wait condition, * and to make the increment atomic. */ synchronized { dismissedCount += 1 notifyAll() } } } } /** * Companion object that facilitates the importing of AsyncAssertions members as * an alternative to mixing in the trait. One use case is to import AsyncAssertions's members so you can use * them in the Scala interpreter. */ object AsyncAssertions extends AsyncAssertions




© 2015 - 2024 Weber Informatics LLC | Privacy Policy