Skip to content

Commit

Permalink
fixed the .await method for future matchers and failed futures. fixes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
etorreborre committed Jun 25, 2014
1 parent 9ed4b66 commit 78b251c
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 25 deletions.
52 changes: 34 additions & 18 deletions matcher/src/main/scala/org/specs2/matcher/FutureMatchers.scala
Expand Up @@ -9,51 +9,67 @@ import execute.{AsResult, Failure, Result}
/**
* This trait is for transforming matchers of values to matchers of Futures
*/
trait FutureMatchers extends Expectations {
trait FutureMatchers extends Expectations with ConcurrentExecutionContext {

/**
* add an `await` method to any matcher `Matcher[T]` so that it can be transformed into a `Matcher[Future[T]]`
*/
implicit class FutureMatchable[T](m: Matcher[T]) {
def await(implicit executionContext: ExecutionContext): Matcher[Future[T]] =
await()

def await(retries: Int = 0, timeout: FiniteDuration = 1.seconds)(implicit executionContext: ExecutionContext): Matcher[Future[T]] =
awaitFor(m)(retries, timeout)
def await: Matcher[Future[T]] = await()
def await(retries: Int = 0, timeout: FiniteDuration = 1.seconds): Matcher[Future[T]] = awaitFor(m)(retries, timeout)
}

/**
* when a Future contains a result, it can be awaited to return this result
*/
implicit class futureAsResult[T : AsResult](f: Future[T]) {
def await(implicit executionContext: ExecutionContext): Result =
await()

def await(retries: Int = 0, timeout: FiniteDuration = 1.seconds)(implicit executionContext: ExecutionContext): Result = {
def await: Result = await()
def await(retries: Int = 0, timeout: FiniteDuration = 1.seconds): Result = {
def awaitFor(retries: Int, totalDuration: FiniteDuration = 0.seconds): Result = {
try Await.result(f.map(value => AsResult(value)), timeout)
catch {
case e: TimeoutException =>
case e: TimeoutException =>
if (retries <= 0) Failure(s"Timeout after ${totalDuration + timeout}")
else awaitFor(retries - 1, totalDuration + timeout)

case other: Throwable => throw other
}
}
awaitFor(retries)
}
}

def await[T](m: Matcher[T])(retries: Int = 0, timeout: FiniteDuration = 1.seconds)(implicit executionContext: ExecutionContext): Matcher[Future[T]] =
awaitFor(m)(retries, timeout)
def await[T](m: Matcher[T]): Matcher[Future[T]] = awaitFor(m)()
def await[T](m: Matcher[T])(retries: Int = 0, timeout: FiniteDuration = 1.seconds): Matcher[Future[T]] = awaitFor(m)(retries, timeout)

private def awaitFor[T](m: Matcher[T])(retries: Int = 0, timeout: FiniteDuration = 1.seconds)(implicit executionContext: ExecutionContext): Matcher[Future[T]] =
new Matcher[Future[T]] {
def apply[S <: Future[T]](a: Expectable[S]) = {
private def awaitFor[T](m: Matcher[T])(retries: Int = 0, timeout: FiniteDuration = 1.seconds): Matcher[Future[T]] = new Matcher[Future[T]] {
def apply[S <: Future[T]](a: Expectable[S]) = {
try {
val r = a.value.map(v => createExpectable(v).applyMatcher(m).toResult).await(retries, timeout)
result(r.isSuccess, r.message, r.message, a)
} catch {
// if awaiting on the future throws an exception because it was a failed future
// there try to match again because the matcher can be a `throwA` matcher
case t: Throwable =>
val r = createExpectable(throw t).applyMatcher(m).toResult
result(r.isSuccess, r.message, r.message, a)
}
}
}
}

object FutureMatchers extends FutureMatchers

/**
* Specification of the execution context to be used for executing futures
* This can be overridden to pass in your own execution context
*/
trait ConcurrentExecutionContext {
implicit def concurrentExecutionContext: ExecutionContext = concurrent.ExecutionContext.Implicits.global
}

object FutureMatchers extends FutureMatchers
/**
* stack this trait to remove the implicit execution context used to evaluate features
*/
trait NoConcurrentExecutionContext extends ConcurrentExecutionContext {
override def concurrentExecutionContext: ExecutionContext = super.concurrentExecutionContext
}
23 changes: 16 additions & 7 deletions tests/src/test/scala/org/specs2/matcher/FutureMatchersSpec.scala
Expand Up @@ -2,25 +2,34 @@ package org.specs2
package matcher

import specification._
import core.Env
import script._
import concurrent._
import duration._
import ExecutionContext.Implicits.global
import java.util.concurrent.{Executors, ThreadPoolExecutor, ForkJoinPool, Executor}

class FutureMatchersSpec extends Specification with Groups with ResultMatchers { def is = sequential ^ s2"""

Any `Matcher[T]` can be transformed into a `Matcher[Future[T]]` with the `await` method
${ implicit c: ExecutionContext => Future(1) must be_>(0).await }
${ Future(1) must be_>(0).await }

with a retries number
${ implicit c: ExecutionContext => Future({ Thread.sleep(100); 1 }) must be_>(0).await(retries = 2, timeout = 100.millis) }
${ implicit c: ExecutionContext => (Future({ Thread.sleep(800); 1 }) must be_>(0).await(retries = 4, timeout = 50.millis)) returns "Timeout after 250 milliseconds" }
${ Future { Thread.sleep(100); 1 } must be_>(0).await(retries = 2, timeout = 100.millis) }
${ (Future { Thread.sleep(800); 1 } must be_>(0).await(retries = 4, timeout = 50.millis)) returns "Timeout after 250 milliseconds" }

A `Future` returning a `Matcher[T]` can be transformed into a `Result`
${ implicit c: ExecutionContext => Future(1 === 1).await }
${ Future(1 === 1).await }

A `throwA[T]` matcher can be used to match a failed future with the `await` method
${ Future.failed[Int](new RuntimeException) must throwA[RuntimeException].await }
${ { Future.failed[Int](new RuntimeException) must be_===(1).await } must throwA[RuntimeException] }

"""

}
// the current execution context can be overridden here
val pool = Executors.newFixedThreadPool(4)

case class CustomException(e: String) extends Exception(e)
override implicit val concurrentExecutionContext: ExecutionContext =
concurrent.ExecutionContext.fromExecutor(pool)

}

0 comments on commit 78b251c

Please sign in to comment.