org.scalatest.concurrent.TimeLimits.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.exceptions.StackDepthExceptionHelper.getStackDepthFun
import org.scalatest.exceptions.StackDepthExceptionHelper.posOrElseStackDepthFun
import org.scalatest.{FailureMessages, UnquotedString}
import org.scalatest.exceptions.{StackDepthException, TestFailedDueToTimeoutException, TestCanceledException}
import java.nio.channels.ClosedByInterruptException
import java.nio.channels.Selector
import java.net.Socket
import org.scalatest.Exceptional
import org.scalatest.time.Span
import org.scalatest.enablers.Timed
import org.scalactic._
/**
* Trait that provides failAfter
and cancelAfter
methods, which allow you to specify a time limit for an
* operation passed as a by-name parameter, as well as a way to signal it if the operation exceeds its time limit.
*
*
* The time limit is passed as the first parameter, as a Span
. The operation is
* passed as the second parameter. A Signaler
, a strategy for interrupting the operation, is
* passed as an implicit third parameter. Here's a simple example of its use:
*
*
*
* failAfter(Span(100, Millis)) {
* Thread.sleep(200)
* }
*
*
*
* The above code will eventually produce a TestFailedDueToTimeoutException
with a message
* that indicates a time limit has been exceeded:
*
*
*
* The code passed to failAfter did not complete within 100 milliseconds.
*
*
*
* If you use cancelAfter
in place of failAfter
, a TestCanceledException
will be thrown
* instead, also with a message that indicates a time limit has been exceeded:
*
*
*
* The code passed to cancelAfter did not complete within 100 milliseconds.
*
*
*
* If you prefer you can mix in or import the members of SpanSugar
and place a units value after the integer timeout.
* Here are some examples:
*
*
*
* import org.scalatest.time.SpanSugar._
*
* failAfter(100 millis) {
* Thread.sleep(200)
* }
*
* failAfter(1 second) {
* Thread.sleep(2000)
* }
*
*
*
* The code passed via the by-name parameter to failAfter
or cancelAfter
will be executed by the thread that invoked
* failAfter
or cancelAfter
, so that no synchronization is necessary to access variables declared outside the by-name.
*
*
*
* var result = -1 // No need to make this volatile
* failAfter(100 millis) {
* result = accessNetService()
* }
* result should be (99)
*
*
*
* The failAfter
or cancelAfter
method will create a timer that runs on a different thread than the thread that
* invoked failAfter
or cancelAfter
, so that it can detect when the time limit has been exceeded and attempt to signal
* the main thread. Because different operations can require different signaling strategies, the failAfter
and cancelAfter
* methods accept an implicit third parameter of type Signaler
that is responsible for signaling
* the main thread.
*
*
* Configuring failAfter
or cancelAfter
with a Signaler
*
*
* The Signaler
companion object declares an implicit val
of type Signaler
that returns
* a DoNotSignal
. This serves as the default signaling strategy.
* If you wish to use a different strategy, you can declare an implicit val
that establishes a different Signaler
* as the policy. Here's an example
* in which the default signaling strategy is changed to ThreadSignaler
, which does not attempt to
* interrupt the main thread in any way:
*
*
*
* override val signaler: Signaler = ThreadSignaler
* failAfter(100 millis) {
* Thread.sleep(500)
* }
*
*
*
* As with the default Signaler
, the above code will eventually produce a
* TestFailedDueToTimeoutException
with a message that indicates a timeout expired. However, instead
* of throwing the exception after approximately 500 milliseconds, it will throw it after approximately 100 milliseconds.
*
*
*
* This illustrates an important feature of failAfter
and cancelAfter
: it will throw a
* TestFailedDueToTimeoutException
(or TestCanceledException
in case of cancelAfter
)
* if the code passed as the by-name parameter takes longer than the specified timeout to execute, even if it
* is allowed to run to completion beyond the specified timeout and returns normally.
*
*
*
* ScalaTest provides the following Signaler
implementations:
*
*
*
*
*
* Signaler
implementation
*
*
* Usage
*
*
*
*
* DoNotSignal
*
*
* The default signaler, does not attempt to interrupt the main test thread in any way
*
*
*
*
* ThreadSignaler
*
*
* Invokes interrupt
on the main test thread. This will
* set the interrupted status for the main test thread and,
* if the main thread is blocked, will in some cases cause the main thread to complete abruptly with
* an InterruptedException
.
*
*
*
*
* SelectorSignaler
*
*
* Invokes wakeup
on the passed java.nio.channels.Selector
, which
* will cause the main thread, if blocked in Selector.select
, to complete abruptly with a
* ClosedSelectorException
.
*
*
*
*
* SocketSignaler
*
*
* Invokes close
on the java.io.Socket
, which
* will cause the main thread, if blocked in a read or write of an java.io.InputStream
or
* java.io.OutputStream
that uses the Socket
, to complete abruptly with a
* SocketException
.
*
*
*
*
*
* You may wish to create your own Signaler
in some situations. For example, if your operation is performing
* a loop and can check a volatile flag each pass through the loop, you could write a Signaler
that
* sets that flag so that the next time around, the loop would exit.
*
*
* @author Chua Chee Seng
* @author Bill Venners
*/
trait TimeLimits {
/*
*
* To change the default Signaler
configuration, define an implicit
* Signaler
in scope.
*
*/
// implicit val defaultSignaler: Signaler = ThreadSignaler
/**
* Executes the passed function, enforcing the passed time limit by attempting to signal the operation if the
* time limit is exceeded, and "failing" if the time limit has been
* exceeded after the function completes, where what it means to "fail" is determined by the implicitly passed Timed[T]
* instance.
*
*
* The Timed
companion object offers three implicits, one for FutureOutcome
, one for Future[U]
* and one for any other type. The implicit Timed[FutureOutcome]
defines failure as failing the FutureOutcome
with a TestFailedDueToTimeoutException
:
* no exception will be thrown. The implicit Timed[Future[U]]
defines failure as failing the Future[U]
with a TestFailedDueToTimeoutException
:
* no exception will be thrown. The implicit for any other type defines failure as throwing
* TestFailedDueToTimeoutException
. For the details, see the Scaladoc of the implicit Timed
providers
* in the Timed
companion object.
*
*
* @param timeout the maximimum amount of time allowed for the passed operation
* @param fun the operation on which to enforce the passed timeout
* @param signaler a strategy for signaling the passed operation
* @param prettifier a Prettifier
for prettifying error messages
* @param pos the Position
of the caller site
* @param timed the Timed
type class that provides the behavior implementation of the timing restriction.
*/
final inline def failAfter[T](timeout: Span)(fun: => T)(implicit signaler: Signaler, prettifier: Prettifier = implicitly[Prettifier], timed: Timed[T] = implicitly[Timed[T]]): T =
${ TimeLimits.failAfterMacro('{timeout}, '{signaler}, '{prettifier}, '{fun}, '{timed}) }
// TODO: Consider creating a TestCanceledDueToTimeoutException
/**
* Executes the passed function, enforcing the passed time limit by attempting to signal the operation if the
* time limit is exceeded, and "canceling" if the time limit has been
* exceeded after the function completes, where what it means to "cancel" is determined by the implicitly passed Timed[T]
* instance.
*
*
* The Timed
companion object offers three implicits, one for FutureOutcome
, one for Future[U]
* and one for any other type. The implicit Timed[FutureOutcome]
defines cancelation as canceling the FutureOutcome
:
* no exception will be thrown. The implicit Timed[Future[U]]
defines canceling as failing the Future[U]
with a TestCanceledException
:
* no exception will be thrown. The implicit for any other type defines failure as throwing
* TestCanceledException
. For the details, see the Scaladoc of the implicit Timed
providers
* in the Timed
companion object.
*
*
* @param timeout the maximimum amount of time allowed for the passed operation
* @param f the operation on which to enforce the passed timeout
* @param signaler a strategy for signaling the passed operation
* @param prettifier a Prettifier
for prettifying error messages
* @param pos the Position
of the caller site
* @param timed the Timed
type class that provides the behavior implementation of the timing restriction.
*/
final inline def cancelAfter[T](timeout: Span)(fun: => T)(implicit signaler: Signaler, prettifier: Prettifier = implicitly[Prettifier], timed: Timed[T] = implicitly[Timed[T]]): T =
${ TimeLimits.cancelAfterMacro('{timeout}, '{signaler}, '{prettifier}, '{fun}, '{timed}) }
}
/**
* Companion object that facilitates the importing of Timeouts
members as
* an alternative to mixing in the trait. One use case is to import Timeouts
's members so you can use
* them in the Scala interpreter.
*/
object TimeLimits extends TimeLimits {
private[scalatest] def failAfterImpl[T](timeout: Span, signaler: Signaler, prettifier: Prettifier, pos: Option[source.Position], stackDepthFun: StackDepthException => Int)(fun: => T)(implicit timed: Timed[T]): T = {
val stackTraceElements = Thread.currentThread.getStackTrace()
timed.timeoutAfter(
timeout,
fun,
signaler,
(cause: Option[Throwable]) => {
val e = new TestFailedDueToTimeoutException(
(_: StackDepthException) => Some(FailureMessages.timeoutFailedAfter(prettifier, UnquotedString(timeout.prettyString))),
cause,
posOrElseStackDepthFun(pos, stackDepthFun),
None,
timeout
)
e.setStackTrace(stackTraceElements)
e
}
)
}
import scala.quoted._
private[concurrent] def failAfterMacro[T](timeout: Expr[Span], signaler: Expr[Signaler], prettifier: Expr[Prettifier], fun: Expr[T], timed: Expr[Timed[T]])(using quotes: Quotes, typeT: Type[T]): Expr[T] = {
source.Position.withPosition[T]('{(pos: source.Position) => failAfterImpl(${timeout}, ${signaler}, ${prettifier}, Some(pos), getStackDepthFun(pos))(${fun})(${timed}) })
}
private[scalatest] def cancelAfterImpl[T](timeout: Span, signaler: Signaler, prettifier: Prettifier, pos: Option[source.Position], stackDepthFun: StackDepthException => Int)(fun: => T)(implicit timed: Timed[T]): T = {
val stackTraceElements = Thread.currentThread.getStackTrace()
timed.timeoutAfter(
timeout,
fun,
signaler,
(cause: Option[Throwable]) => {
val e = new TestCanceledException(
(_: StackDepthException) => Some(FailureMessages.timeoutCanceledAfter(prettifier, UnquotedString(timeout.prettyString))),
cause,
posOrElseStackDepthFun(pos, stackDepthFun),
None
)
e.setStackTrace(stackTraceElements)
e
}
)
}
import scala.quoted._
private[concurrent] def cancelAfterMacro[T](timeout: Expr[Span], signaler: Expr[Signaler], prettifier: Expr[Prettifier], fun: Expr[T], timed: Expr[Timed[T]])(using quotes: Quotes, typeT: Type[T]): Expr[T] = {
source.Position.withPosition[T]('{(pos: source.Position) => cancelAfterImpl(${timeout}, ${signaler}, ${prettifier}, Some(pos), getStackDepthFun(pos))(${fun})(${timed}) })
}
}