org.scalatest.concurrent.Waiters.scala Maven / Gradle / Ivy
/*
* 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 org.scalatest.exceptions.StackDepthException
import time.{Nanoseconds, Second, Span}
import PatienceConfiguration._
import org.scalactic.source
/**
* Trait that facilitates performing assertions outside the main test thread, such as assertions in callback methods
* that are invoked asynchronously.
*
*
* Trait Waiters
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 Waiters
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.
*
*
*
* Note that if a Waiter
receives more than the expected number of dismissals, it will not report
* this as an error: i.e., receiving greater than the number of expected dismissals without any failed assertion will simply
* cause the the test to complete, not to fail. The only way a Waiter
will cause a test to fail is if one of the
* asynchronous assertions to which it is applied fails.
*
*
*
* 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.Waiters
* import scala.actors.Actor
*
* class ExampleSuite extends FunSuite with Matchers with Waiters {
*
* 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 Waiters 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 Waiters
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.Waiters
* import scala.actors.Actor
*
* class ExampleSuite extends FunSuite with Matchers with Waiters {
*
* 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): Unit = {
/*
* 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): 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, pos: source.Position, dismissals: Int = 1): Unit = {
if (Thread.currentThread != creatingThread)
throw new NotAllowedException(Resources.awaitMustBeCalledOnCreatingThread, None, pos)
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.Zero
}
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 jeopardy".
// This if-else must be in the synchronized block because it accesses and assigns dismissalsCount.
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((sde: StackDepthException) => Some(Resources.awaitTimedOut), None, pos, None)
else throw new Exception("Should never happen: thrown was not defined; dismissedCount was not greater than dismissals; and timedOut was false")
}
}
/**
* 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, pos: source.Position): Unit = {
awaitImpl(config.timeout, pos)
}
/**
* 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)(implicit pos: source.Position): Unit = {
awaitImpl(timeout.value, pos)
}
/**
* 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, pos: source.Position): Unit = {
awaitImpl(config.timeout, pos, 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)(implicit pos: source.Position): Unit = {
awaitImpl(timeout.value, pos, 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(): Unit = {
/*
* 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 Waiters
members as
* an alternative to mixing in the trait. One use case is to import Waiters
's members so you can use
* them in the Scala interpreter.
*/
object Waiters extends Waiters