Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Http4sMatchers to be generic on F-Type #2080

Merged
merged 3 commits into from
Sep 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala
Original file line number Diff line number Diff line change
@@ -1,56 +1,56 @@
package org.http4s
package testing

import cats.syntax.flatMap._
import cats.data.EitherT
import cats.effect.IO
import org.http4s.headers._
import org.specs2.matcher._

/** This might be useful in a testkit spinoff. Let's see what they do for us. */
// TODO these akas might be all wrong.
trait Http4sMatchers extends Matchers with IOMatchers {
def haveStatus(expected: Status): Matcher[Response[IO]] =
be_===(expected) ^^ { r: Response[IO] =>
trait Http4sMatchers[F[_]] extends Matchers with RunTimedMatchers[F] {
def haveStatus(expected: Status): Matcher[Response[F]] =
be_===(expected) ^^ { r: Response[F] =>
r.status.aka("the response status")
}

def returnStatus(s: Status): Matcher[IO[Response[IO]]] =
haveStatus(s) ^^ { r: IO[Response[IO]] =>
r.unsafeRunSync.aka("the returned")
def returnStatus(s: Status): Matcher[F[Response[F]]] =
haveStatus(s) ^^ { r: F[Response[F]] =>
runAwait(r).aka("the returned")
}

def haveBody[A: EntityDecoder[IO, ?]](a: ValueCheck[A]): Matcher[Message[IO]] =
returnValue(a) ^^ { m: Message[IO] =>
def haveBody[A: EntityDecoder[F, ?]](a: ValueCheck[A]): Matcher[Message[F]] =
returnValue(a) ^^ { m: Message[F] =>
m.as[A].aka("the message body")
}

def returnBody[A: EntityDecoder[IO, ?]](a: ValueCheck[A]): Matcher[IO[Message[IO]]] =
returnValue(a) ^^ { m: IO[Message[IO]] =>
def returnBody[A: EntityDecoder[F, ?]](a: ValueCheck[A]): Matcher[F[Message[F]]] =
returnValue(a) ^^ { m: F[Message[F]] =>
m.flatMap(_.as[A]).aka("the returned message body")
}

def haveHeaders(a: Headers): Matcher[Message[IO]] =
be_===(a) ^^ { m: Message[IO] =>
def haveHeaders(a: Headers): Matcher[Message[F]] =
be_===(a) ^^ { m: Message[F] =>
m.headers.aka("the headers")
}

def haveMediaType(mt: MediaType): Matcher[Message[IO]] =
beSome(mt) ^^ { m: Message[IO] =>
def haveMediaType(mt: MediaType): Matcher[Message[F]] =
beSome(mt) ^^ { m: Message[F] =>
m.headers.get(`Content-Type`).map(_.mediaType).aka("the media type header")
}

def haveContentCoding(c: ContentCoding): Matcher[Message[IO]] =
beSome(c) ^^ { m: Message[IO] =>
def haveContentCoding(c: ContentCoding): Matcher[Message[F]] =
beSome(c) ^^ { m: Message[F] =>
m.headers.get(`Content-Encoding`).map(_.contentCoding).aka("the content encoding header")
}

def returnRight[A, B](m: ValueCheck[B]): Matcher[EitherT[IO, A, B]] =
beRight(m) ^^ { et: EitherT[IO, A, B] =>
et.value.unsafeRunSync.aka("the either task")
def returnRight[A, B](m: ValueCheck[B]): Matcher[EitherT[F, A, B]] =
beRight(m) ^^ { et: EitherT[F, A, B] =>
runAwait(et.value).aka("the either task")
}

def returnLeft[A, B](m: ValueCheck[A]): Matcher[EitherT[IO, A, B]] =
beLeft(m) ^^ { et: EitherT[IO, A, B] =>
et.value.unsafeRunSync.aka("the either task")
def returnLeft[A, B](m: ValueCheck[A]): Matcher[EitherT[F, A, B]] =
beLeft(m) ^^ { et: EitherT[F, A, B] =>
runAwait(et.value).aka("the either task")
}
}
62 changes: 6 additions & 56 deletions testing/src/main/scala/org/http4s/testing/IOMatchers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,19 @@
*/
package org.http4s.testing

import cats.effect.IO
import org.specs2.matcher._
import org.specs2.matcher.ValueChecks._
import cats.effect.{IO, Sync}
import scala.concurrent.duration.FiniteDuration

/**
* Matchers for cats.effect.IO
*/
trait IOMatchers {
// This comes from a private trait in real IOMatchers
implicit class NotNullSyntax(s: String) {
def notNull: String =
Option(s).getOrElse("null")
}
trait IOMatchers extends RunTimedMatchers[IO] {

def returnOk[T]: IOMatcher[T] =
attemptRun(ValueCheck.alwaysOk, None)
protected implicit def F: Sync[IO] = IO.ioEffect
protected def runWithTimeout[A](fa: IO[A], timeout: FiniteDuration): Option[A] =
fa.unsafeRunTimed(timeout)
protected def runAwait[A](fa: IO[A]): A = fa.unsafeRunSync

def returnValue[T](check: ValueCheck[T]): IOMatcher[T] =
attemptRun(check, None)

def returnBefore[T](duration: FiniteDuration): IOMatcher[T] =
attemptRun(ValueCheck.alwaysOk, Some(duration))

private def attemptRun[T](check: ValueCheck[T], duration: Option[FiniteDuration]): IOMatcher[T] =
IOMatcher(check, duration)

case class IOMatcher[T](check: ValueCheck[T], duration: Option[FiniteDuration])
extends Matcher[IO[T]] {
def apply[S <: IO[T]](e: Expectable[S]): MatchResult[S] =
duration match {
case Some(d) =>
e.value.attempt
.unsafeRunTimed(d)
.fold(failedAttemptWithTimeout(e, d))(_.fold(failedAttempt(e), checkResult(e)))
case None => e.value.attempt.unsafeRunSync.fold(failedAttempt(e), checkResult(e))
}

def before(d: FiniteDuration): IOMatcher[T] =
copy(duration = Some(d))

def withValue(check: ValueCheck[T]): IOMatcher[T] =
copy(check = check)

def withValue(t: T): IOMatcher[T] =
withValue(valueIsTypedValueCheck(t))

private def failedAttemptWithTimeout[S <: IO[T]](
e: Expectable[S],
d: FiniteDuration): MatchResult[S] = {
val message = s"Timeout after ${d.toMillis} milliseconds"
result(false, message, message, e)
}

private def failedAttempt[S <: IO[T]](e: Expectable[S])(t: Throwable): MatchResult[S] = {
val message = s"an exception was thrown ${t.getMessage.notNull}"
result(false, message, message, e)
}

private def checkResult[S <: IO[T]](e: Expectable[S])(t: T): MatchResult[S] =
result(check.check(t), e)

}
}

object IOMatchers extends IOMatchers
80 changes: 80 additions & 0 deletions testing/src/main/scala/org/http4s/testing/RunTimedMatchers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* Derived from https://raw.githubusercontent.com/etorreborre/specs2/c0cbfc71390b644db1a5deeedc099f74a237ebde/matcher-extra/src/main/scala-scalaz-7.0.x/org/specs2/matcher/TaskMatchers.scala
* License: https://raw.githubusercontent.com/etorreborre/specs2/master/LICENSE.txt
*/
package org.http4s.testing

import cats.effect.Sync
import org.specs2.matcher._
import org.specs2.matcher.ValueChecks._
import scala.concurrent.duration.FiniteDuration

/**
* Matchers for cats.effect.F_
*/
trait RunTimedMatchers[F[_]] {

protected implicit def F: Sync[F]
protected def runWithTimeout[A](fa: F[A], timeout: FiniteDuration): Option[A]
protected def runAwait[A](fa: F[A]): A

// This comes from a private trait in real IOMatchers
implicit class NotNullSyntax(s: String) {
def notNull: String =
Option(s).getOrElse("null")
}

def returnOk[T]: TimedMatcher[T] =
attemptRun(ValueCheck.alwaysOk, None)

def returnValue[T](check: ValueCheck[T]): TimedMatcher[T] =
attemptRun(check, None)

def returnBefore[T](duration: FiniteDuration): TimedMatcher[T] =
attemptRun(ValueCheck.alwaysOk, Some(duration))

private def attemptRun[T](
check: ValueCheck[T],
duration: Option[FiniteDuration]): TimedMatcher[T] =
TimedMatcher(check, duration)

case class TimedMatcher[T](
check: ValueCheck[T],
duration: Option[FiniteDuration]
) extends Matcher[F[T]] {

override final def apply[S <: F[T]](expected: Expectable[S]): MatchResult[S] = {

def checkOrFail[A](res: Either[Throwable, T]): MatchResult[S] = res match {
case Left(error) =>
val message = s"an exception was thrown ${error.getMessage.notNull}"
result(false, message, message, expected)
case Right(actual) =>
result(check.check(actual), expected)
}

val theAttempt = F.attempt(expected.value)
duration match {
case Some(timeout) =>
runWithTimeout(theAttempt, timeout) match {
case None =>
val message = s"Timeout after ${timeout.toMillis} milliseconds"
result(false, message, message, expected)
case Some(result) => checkOrFail(result)
}
case None =>
checkOrFail(runAwait(theAttempt))
}

}

def before(d: FiniteDuration): TimedMatcher[T] =
copy(duration = Some(d))

def withValue(check: ValueCheck[T]): TimedMatcher[T] =
copy(check = check)

def withValue(t: T): TimedMatcher[T] =
withValue(valueIsTypedValueCheck(t))

}
}
2 changes: 1 addition & 1 deletion testing/src/test/scala/org/http4s/Http4sSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ trait Http4sSpec
with FragmentsDsl
with Discipline
with IOMatchers
with Http4sMatchers {
with Http4sMatchers[IO] {
implicit def testExecutionContext: ExecutionContext = Http4sSpec.TestExecutionContext
val testBlockingExecutionContext: ExecutionContext = Http4sSpec.TestBlockingExecutionContext
implicit val contextShift: ContextShift[IO] = Http4sSpec.TestContextShift
Expand Down