org.scalatest.enablers.Timed.scala Maven / Gradle / Ivy
/*
* Copyright 2001-2016 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.enablers
import org.scalatest._
import org.scalatest.concurrent.Signaler
import org.scalatest.exceptions.StackDepthException
import org.scalactic.{Bad, Good}
import scala.util.{Failure, Success}
//import java.util.TimerTask
//import java.util.Timer
import org.scalatest.time.Span
import org.scalatest.concurrent.SignalerTimeoutTask
import scala.concurrent.{Promise, Future, ExecutionContext}
import org.scalactic.source
/**
* Trait that provides a timeoutAfter construct, which allows you to specify a timeout for an
* operation passed as a by-name parameter, as well as a way to signal/interrupt it if the operation exceeds its time limit.
*/
trait Timed[T] {
/**
* Execute the passed in function f and time it, if the time it takes to complete the function the function
* execution exceeds the passed in timeout, call the passed in exceptionFun to create an instance
* of [[org.scalatest.exceptions.StackDepthException StackDepthException]] and the implementation is responsible to handle it.
*
* @param timeout the maximum amount of time allowed for the passed function
* @param f the passed in function to be timed
* @param signaler the Signaler used to signal/interrupt the function when time limit exceeded
* @param exceptionFun the function to create StackDepthException for failure
* @return the T returned by function f
*/
def timeoutAfter(
timeout: Span,
f: => T,
signaler: Signaler,
exceptionFun: Option[Throwable] => StackDepthException
): T
}
/**
* Companion object for Timed typeclass that offers three implicit providers: one for FutureOutcome,
* one for Future of any type, and one for any other type.
*
*
* The details are in the documentation for the implicit providers themselves (methods timed, timedFutureOf,
* and timedFutureOutcome), but in short if a time limit is exceeded:
*
*
*
* - if the type
T in Timed[T] is FutureOutcome
* the FutureOutcome returned by timeoutAfter will result in either Failed or Canceled
* - if the type is
Future[U], the Future[U] returned by timeoutAfter will fail with either a
* TestFailedDueToTimeoutException or a TestCanceledException.
* - otherwise, the
timeoutAfter method will itself complete abruptly with either TestFailedDueToTimeoutException
* or TestCanceledException.
*
*/
object Timed {
/*
TODO: See if this bit of documentation from the old Timeouts is still relevant, and if so, insert it
below:
If the interrupted status of the main test thread (the thread that invoked failAfter) was not invoked
when failAfter was invoked, but is set after the operation times out, it is reset by this method before
it completes abruptly with a TestFailedDueToTimeoutException. The interrupted status will be set by
ThreadSignaler, the default Signaler implementation.
*/
/**
* Implicit method that provides Timed implementation for any T.
*
*
* If the function completes before the timeout expires:
*
*
*
* - If the function returns normally, this method will return normally.
* - If the function completes abruptly with an exception, this method will complete abruptly with that same exception.
*
*
*
* If the function completes after the timeout expires:
*
*
*
* - If the function returns normally, this method will complete abruptly with a
StackDepthException created from exceptionFun.
* - If the function completes abruptly with an exception, this method will complete abruptly with a
StackDepthException created from exceptionFun that includes the exception thrown by the function as its cause.
*
*
*
* This implementation will start a timer that when the time limit is exceeded while the passed in function f is still running, it will attempt to call the passed in Signaler to signal/interrupt the running function f.
*
*/
implicit def timed[T]: Timed[T] =
new Timed[T] {
def timeoutAfter(
timeout: Span,
f: => T,
signaler: Signaler,
exceptionFun: Option[Throwable] => StackDepthException
): T = {
val timer = new Timer
val task = new SignalerTimeoutTask(Thread.currentThread(), signaler)
val maxDuration = timeout.totalNanos / 1000 / 1000
timer.schedule(task, maxDuration) // TODO: Probably use a sleep so I can use nanos
val startTime = scala.compat.Platform.currentTime
try {
val result = f
val endTime = scala.compat.Platform.currentTime
task.cancel()
result match {
case Exceptional(ex) => throw ex // If the result is Exceptional, the exception is already wrapped, just re-throw it to get the old behavior.
case _ =>
if (task.timedOut || (endTime - startTime) > maxDuration) {
if (task.needToResetInterruptedStatus)
Thread.interrupted() // To reset the flag probably. He only does this if it was not set before and was set after, I think.
throw exceptionFun(None)
}
}
result
}
catch {
case t: Throwable =>
val endTime = scala.compat.Platform.currentTime
task.cancel() // Duplicate code could be factored out I think. Maybe into a finally? Oh, not that doesn't work. So a method.
if(task.timedOut || (endTime - startTime) > maxDuration) {
if (task.needToResetInterruptedStatus)
Thread.interrupted() // Clear the interrupt status (There's a race condition here, but not sure we an do anything about that.)
throw exceptionFun(Some(t))
}
else
throw t
}
}
}
/**
* Implicit method that provides Timed implementation for any Future[T].
*
*
* If the asynchronous function completes before the timeout expires:
*
*
*
* - If the function returns normally, the
Future will be completed with the return value of the function.
* - If the function completes abruptly with an exception, this method will complete the
Future with that same exception.
*
*
*
* If the asynchronous function completes after the timeout expires:
*
*
*
* - If the function returns normally, this method will complete the
Future with a StackDepthException created from exceptionFun.
* - If the function completes abruptly with an exception, this method will complete the
Future with a StackDepthException created from exceptionFun that includes the exception thrown by the function as its cause.
*
*
*
* This implementation will start a timer that when the time limit is exceeded while the passed in asynchronous function f is still running, it will attempt to call the passed in Signaler to signal/interrupt the running function f.
*
*/
implicit def timedFutureOf[T](implicit executionContext: ExecutionContext): Timed[Future[T]] =
new Timed[Future[T]] {
def timeoutAfter(
timeout: Span,
f: => Future[T],
signaler: Signaler,
exceptionFun: Option[Throwable] => StackDepthException
): Future[T] = {
val timer = new Timer
val maxDuration = timeout.totalNanos / 1000 / 1000
val startTime = scala.compat.Platform.currentTime
try {
val result = f
val endTime = scala.compat.Platform.currentTime
if ((endTime - startTime) > maxDuration) {
throw exceptionFun(None)
}
val promise = Promise[T]
val task = new SignalerTimeoutTask(Thread.currentThread(), signaler)
val delay = maxDuration - (scala.compat.Platform.currentTime - startTime)
val timer = new Timer
result.onComplete { t =>
t match {
case Success(r) =>
task.cancel()
if (!promise.isCompleted) { // If it completed already, it will fail or have failed with a timeout exception
val endTime = scala.compat.Platform.currentTime
val duration = endTime - startTime
if (duration > maxDuration)
promise.complete(Failure(exceptionFun(None)))
else
promise.success(r)
}
case Failure(e) =>
task.cancel()
if (!promise.isCompleted) { // If it completed already, it will fail or have failed with a timeout exception
val endTime = scala.compat.Platform.currentTime
val duration = endTime - startTime
if (duration > maxDuration)
promise.complete(Failure(exceptionFun(Some(e))))
else
promise.failure(e) // Chee Seng: I wonder if in this case should we use this exception instead of the other one? Not sure.
}
}
}
timer.schedule(task, delay)
promise.future
}
catch {
case t: Throwable =>
val endTime = scala.compat.Platform.currentTime
if((endTime - startTime) > maxDuration) {
throw exceptionFun(Some(t))
}
else
throw t
}
}
}
/*
Chee Seng: This one should catch TestCanceledException and change it into
a Canceled. It should catch TestPendingException and change it into
a Pending. It should catch any other non-suite-aborting exception and
turn it into a Failed. A timeout should become a Failed(TestFailedDueToTimeoutException).
I believe this is what you did in AsyncTimeouts.
*/
/**
* Implicit method that provides Timed implementation for FutureOutcome.
*
*
* If the asynchronous function completes before the timeout expires:
*
*
*
* - If the function returns normally, the
FutureOutcome will be completed with the Outcome returned from the function.
* - If the function completes abruptly with an
TestPendingException, this method will complete the FutureOutcome with Pending.
* - If the function completes abruptly with an
TestCanceledException, this method will complete the FutureOutcome with Canceled that contains the thrown exception.
* - If the function completes abruptly with a run-aborting exception, this method will complete the
FutureOutcome with Failed that contains the thrown exception.
* - If the function completes abruptly with a non-run-aborting exception, this method will fail the
FutureOutcome with ExecutionException that contains the thrown exception.
*
*
*
* If the asynchronous function completes after the timeout expires:
*
*
*
* - If the function returns normally, this method will complete the
FutureOutcome with a Outcome that's mapped from the exception thrown from exceptionFun.
* - If the function completes abruptly with an exception, this method will complete the
FutureOutcome with Outcome that's mapped from the exception thrown from exceptionFun that includes the exception thrown by the function as its cause.
*
*
*
* This implementation will start a timer that when the time limit is exceeded while the passed in asynchronous function f is still running, it will attempt to call the passed in Signaler to signal/interrupt the running function f.
*
*/
implicit def timedFutureOutcome(implicit executionContext: ExecutionContext): Timed[FutureOutcome] =
new Timed[FutureOutcome] {
def timeoutAfter(
timeout: Span,
f: => FutureOutcome,
signaler: Signaler,
exceptionFun: Option[Throwable] => StackDepthException
): FutureOutcome = {
val timer = new Timer
val maxDuration = timeout.totalNanos / 1000 / 1000
val startTime = scala.compat.Platform.currentTime
val result = f
val endTime = scala.compat.Platform.currentTime
if ((endTime - startTime) > maxDuration)
throw exceptionFun(None)
val task = new SignalerTimeoutTask(Thread.currentThread(), signaler)
val delay = maxDuration - (scala.compat.Platform.currentTime - startTime)
val futureOutcome = result.onCompletedThen { t =>
t match {
case Good(r) =>
task.cancel()
val endTime = scala.compat.Platform.currentTime
val duration = endTime - startTime
try {
if (duration > maxDuration) {
throw exceptionFun(None)
}
}
catch {
case t: Throwable =>
throw t
}
case Bad(e) =>
task.cancel()
val endTime = scala.compat.Platform.currentTime
val duration = endTime - startTime
if (duration > maxDuration)
throw exceptionFun(None)
else
throw e
}
}
timer.schedule(task, delay)
futureOutcome
}
}
}