Skip to content

Commit

Permalink
Merge pull request #91 from 2m/feature/async-fixture-2m
Browse files Browse the repository at this point in the history
Add async setup/teardown support to FunFixture
  • Loading branch information
olafurpg authored Apr 6, 2020
2 parents a37ac27 + a304e2c commit 111171b
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ resources after a test case.
```scala mdoc:reset
import java.nio.file._
class FunFixtureSuite extends munit.FunSuite {
val files = new FunFixture[Path](
val files = FunFixture[Path](
setup = { test =>
Files.createTempFile("tmp", test.name)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,12 @@ object FutureCompat {
f.onComplete { t => p.complete(fn(t)) }
p.future
}
def transformWithCompat[B](
fn: Try[T] => Future[B]
)(implicit ec: ExecutionContext): Future[B] = {
val p = Promise[B]()
f.onComplete { t => p.completeWith(fn(t)) }
p.future
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ object FutureCompat {
)(implicit ec: ExecutionContext): Future[B] = {
f.transform(fn)
}
def transformWithCompat[B](
fn: Try[T] => Future[B]
)(implicit ec: ExecutionContext): Future[B] = {
f.transformWith(fn)
}
}
}
98 changes: 80 additions & 18 deletions munit/shared/src/main/scala/munit/FunFixtures.scala
Original file line number Diff line number Diff line change
@@ -1,47 +1,109 @@
package munit

import munit.internal.FutureCompat._

import scala.concurrent.Future
import scala.util.Success
import scala.util.Failure

trait FunFixtures { self: FunSuite =>

class FunFixture[T](
val setup: TestOptions => T,
val teardown: T => Unit
) {
class FunFixture[T] private (
val setup: TestOptions => Future[T],
val teardown: T => Future[Unit]
)(implicit dummy: DummyImplicit) {
@deprecated("Use `FunFixture(...)` without `new` instead", "0.7.2")
def this(setup: TestOptions => T, teardown: T => Unit) =
this(
(options: TestOptions) => Future(setup(options))(munitExecutionContext),
(argument: T) => Future(teardown(argument))(munitExecutionContext)
)

def test(options: TestOptions)(
body: T => Any
)(implicit loc: Location): Unit = {
self.test(options) {
val argument = setup(options)
try body(argument)
finally teardown(argument)
implicit val ec = munitExecutionContext
// the setup, test and teardown need to keep the happens-before execution order
setup(options).flatMap { argument =>
munitValueTransform(body(argument))
.transformWithCompat(testValue =>
teardown(argument).transformCompat {
case Success(_) => testValue
case teardownFailure @ Failure(teardownException) =>
testValue match {
case testFailure @ Failure(testException) =>
testException.addSuppressed(teardownException)
testFailure
case _ =>
teardownFailure
}
}
)
}
}(loc)
}
}

object FunFixture {
def apply[T](setup: TestOptions => T, teardown: T => Unit) = {
implicit val ec = munitExecutionContext
async[T](
options => Future { setup(options) },
argument => Future { teardown(argument) }
)
}
def async[T](setup: TestOptions => Future[T], teardown: T => Future[Unit]) =
new FunFixture(setup, teardown)

def map2[A, B](a: FunFixture[A], b: FunFixture[B]): FunFixture[(A, B)] =
new FunFixture[(A, B)](
setup = { options => (a.setup(options), b.setup(options)) },
FunFixture.async[(A, B)](
setup = { options =>
implicit val ec = munitExecutionContext
val setupA = a.setup(options)
val setupB = b.setup(options)
for {
argumentA <- setupA
argumentB <- setupB
} yield (argumentA, argumentB)
},
teardown = {
case (argumentA, argumentB) =>
try a.teardown(argumentA)
finally b.teardown(argumentB)
implicit val ec = munitExecutionContext
Future
.sequence(List(a.teardown(argumentA), b.teardown(argumentB)))
.map(_ => ())
}
)
def map3[A, B, C](
a: FunFixture[A],
b: FunFixture[B],
c: FunFixture[C]
): FunFixture[(A, B, C)] =
new FunFixture[(A, B, C)](
FunFixture.async[(A, B, C)](
setup = { options =>
(a.setup(options), b.setup(options), c.setup(options))
implicit val ec = munitExecutionContext
val setupA = a.setup(options)
val setupB = b.setup(options)
val setupC = c.setup(options)
for {
argumentA <- setupA
argumentB <- setupB
argumentC <- setupC
} yield (argumentA, argumentB, argumentC)
},
teardown = {
case (argumentA, argumentB, argumentC) =>
try a.teardown(argumentA)
finally {
try b.teardown(argumentB)
finally c.teardown(argumentC)
}
implicit val ec = munitExecutionContext
Future
.sequence(
List(
a.teardown(argumentA),
b.teardown(argumentB),
c.teardown(argumentC)
)
)
.map(_ => ())
}
)
}
Expand Down
10 changes: 9 additions & 1 deletion munit/shared/src/main/scala/munit/FunSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package munit
import scala.collection.mutable
import scala.concurrent.Future
import scala.util.control.NonFatal
import munit.internal.PlatformCompat
import scala.concurrent.duration.Duration
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

abstract class FunSuite
extends Suite
Expand All @@ -29,7 +33,7 @@ abstract class FunSuite
new Test(
options.name, { () =>
try {
munitValueTransform(body)
waitForCompletion(munitValueTransform(body))
} catch {
case NonFatal(e) =>
Future.failed(e)
Expand All @@ -41,4 +45,8 @@ abstract class FunSuite
)
}

def munitTimeout: Duration = new FiniteDuration(30, TimeUnit.SECONDS)
private final def waitForCompletion[T](f: Future[T]) =
PlatformCompat.waitAtMost(f, munitTimeout)

}
9 changes: 1 addition & 8 deletions munit/shared/src/main/scala/munit/ValueTransforms.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import scala.concurrent.Future
import munit.internal.FutureCompat._
import scala.util.Try
import munit.internal.console.StackTraces
import munit.internal.PlatformCompat
import scala.concurrent.duration.Duration
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

trait ValueTransforms { this: FunSuite =>

Expand All @@ -23,7 +19,6 @@ trait ValueTransforms { this: FunSuite =>
munitFutureTransform
)

def munitTimeout: Duration = new FiniteDuration(30, TimeUnit.SECONDS)
final def munitValueTransform(testValue: => Any): Future[Any] = {
// Takes an arbitrarily nested future `Future[Future[Future[...]]]` and
// returns a `Future[T]` where `T` is not a `Future`.
Expand All @@ -40,9 +35,7 @@ trait ValueTransforms { this: FunSuite =>
nested.flattenCompat(munitExecutionContext)
}
val wrappedFuture = Future.fromTry(Try(StackTraces.dropOutside(testValue)))
val flatFuture = flattenFuture(wrappedFuture)
val awaitedFuture = PlatformCompat.waitAtMost(flatFuture, munitTimeout)
awaitedFuture
flattenFuture(wrappedFuture)
}

final def munitFutureTransform: ValueTransform =
Expand Down
38 changes: 38 additions & 0 deletions tests/jvm/src/test/scala/munit/AsyncFixtureOrderSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package munit

import scala.concurrent.Future
import scala.concurrent.Promise

class AsyncFixtureOrderSuite extends FunSuite {
val latch = Promise[Unit]
var completedFromTest = Option.empty[Boolean]
var completedFromTeardown = Option.empty[Boolean]

val latchOnTeardown = FunFixture.async[String](
setup = { test => Future.successful(test.name) },
teardown = { name =>
implicit val ec = munitExecutionContext
Future {
completedFromTeardown = Some(latch.trySuccess(()));
}
}
)

override def afterAll(): Unit = {
// promise was completed first by the test
assertEquals(completedFromTest, Some(true))
// and then there was a completion attempt by the teardown
assertEquals(completedFromTeardown, Some(false))
}

latchOnTeardown.test("teardown runs only after test completes") { _ =>
import scala.concurrent.ExecutionContext.Implicits.global
Future {
// Simulate some work here, which increases the certainty that this test
// will fail by design and not by lucky scheduling if the happens-before
// relationship between the test and teardown is removed.
Thread.sleep(50)
completedFromTest = Some(latch.trySuccess(()))
}
}
}
66 changes: 66 additions & 0 deletions tests/shared/src/main/scala/munit/AsyncFixtureFrameworkSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package munit

import scala.concurrent.Future

class AsyncFixtureFrameworkSuite extends FunSuite {
val failingSetup = FunFixture.async[Unit](
_ => Future.failed(new Error("failure in setup")),
_ => Future.successful(())
)

val failingTeardown = FunFixture.async[Unit](
_ => Future.successful(()),
_ => Future.failed(new Error("failure in teardown"))
)

val unitFixture = FunFixture.async[Unit](
_ => Future.successful(()),
_ => Future.successful(())
)

failingSetup.test("fail when setup fails") { _ =>
fail("failing setup did not fail the test")
}

failingTeardown.test("fail when teardown fails") { _ => () }

failingTeardown.test("fail when test and teardown fail") { _ =>
fail("failure in test")
}

FunFixture
.map2(unitFixture, failingSetup)
.test("fail when mapped setup fails") { _ =>
fail("failing setup did not fail the test")
}

FunFixture
.map3(unitFixture, unitFixture, failingSetup)
.test("fail when even more nested mapped setup fails") { _ =>
fail("failing setup did not fail the test")
}

FunFixture
.map2(unitFixture, failingTeardown)
.test("fail when mapped teardown fails") { _ => () }

FunFixture
.map3(unitFixture, unitFixture, failingTeardown)
.test("fail when even more nested mapped teardown fails") { _ => () }
}

object AsyncFixtureFrameworkSuite
extends FrameworkTest(
classOf[AsyncFixtureFrameworkSuite],
"""|==> failure munit.AsyncFixtureFrameworkSuite.fail when setup fails - failure in setup
|==> failure munit.AsyncFixtureFrameworkSuite.fail when teardown fails - failure in teardown
|==> failure munit.AsyncFixtureFrameworkSuite.fail when test and teardown fail - /scala/munit/AsyncFixtureFrameworkSuite.scala:28 failure in test
|27: failingTeardown.test("fail when test and teardown fail") { _ =>
|28: fail("failure in test")
|29: }
|==> failure munit.AsyncFixtureFrameworkSuite.fail when mapped setup fails - failure in setup
|==> failure munit.AsyncFixtureFrameworkSuite.fail when even more nested mapped setup fails - failure in setup
|==> failure munit.AsyncFixtureFrameworkSuite.fail when mapped teardown fails - failure in teardown
|==> failure munit.AsyncFixtureFrameworkSuite.fail when even more nested mapped teardown fails - failure in teardown
|""".stripMargin
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package munit

import scala.concurrent.Future

class AsyncFixtureTeardownFrameworkSuite extends FunSuite {
@volatile var cleanedUp: Boolean = _

val cleanupInTeardown = FunFixture.async[Unit](
_ => { cleanedUp = false; Future.successful(()) },
_ => { cleanedUp = true; Future.successful(()) }
)

override def afterAll(): Unit = {
assert(cleanedUp)
}

cleanupInTeardown.test("calls teardown when test throws") { _ =>
throw new Error("failure in test")
}

cleanupInTeardown.test("calls teardown when test returns failed Future") {
_ => Future.failed(new Error("failure in test"))
}
}

object AsyncFixtureTeardownFrameworkSuite
extends FrameworkTest(
classOf[AsyncFixtureTeardownFrameworkSuite],
"""|==> failure munit.AsyncFixtureTeardownFrameworkSuite.calls teardown when test throws - failure in test
|==> failure munit.AsyncFixtureTeardownFrameworkSuite.calls teardown when test returns failed Future - failure in test
|""".stripMargin
)
4 changes: 3 additions & 1 deletion tests/shared/src/test/scala/munit/FrameworkSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class FrameworkSuite extends BaseFrameworkSuite {
TestTransformFrameworkSuite,
ValueTransformCrashFrameworkSuite,
ValueTransformFrameworkSuite,
ScalaCheckFrameworkSuite
ScalaCheckFrameworkSuite,
AsyncFixtureFrameworkSuite,
AsyncFixtureTeardownFrameworkSuite
)
tests.foreach { t => check(t) }
}
2 changes: 1 addition & 1 deletion tests/shared/src/test/scala/munit/FunFixtureSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package munit

class FunFixtureSuite extends FunSuite {
var tearDownName = ""
val files = new FunFixture[String](
val files = FunFixture[String](
setup = { test => test.name + "-setup" },
teardown = { name => tearDownName = name }
)
Expand Down

0 comments on commit 111171b

Please sign in to comment.