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

Use consistent solution to customize tests, suites and values #64

Merged
merged 2 commits into from
Mar 14, 2020
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: 26 additions & 20 deletions docs/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ to add make sure that `LazyFuture.run()` gets called.
```scala mdoc
import scala.concurrent.ExecutionContext.Implicits.global
class TaskSuite extends munit.FunSuite {
override def munitTestValue(testValue: => Any): Future[Any] =
super.munitTestValue(testValue).flatMap {
override def munitValueTransforms = super.munitValueTransforms ++ List(
new ValueTransform("LazyFuture", {
case LazyFuture(run) => run()
case value => Future.successful(value)
}
})
)
implicit val ec = ExecutionContext.global
test("ok-task") {
LazyFuture {
Expand Down Expand Up @@ -197,18 +197,22 @@ evaluate the body multiple times.

```scala mdoc
case class Rerun(count: Int) extends munit.Tag("Rerun")
class MyWindowsSuite extends munit.FunSuite {
override def munitRunTest(options: munit.TestOptions, body: () => Future[Any]): Future[Any] = {
val rerunCount = options.tags.collectFirst {
case Rerun(n) => n
}.getOrElse(1)
val futures: Seq[Future[Any]] = 1.to(rerunCount).map(_ =>
super.munitRunTest(options, body))
val result: Future[Seq[Any]] = Future.sequence(futures)
result
}
test("files".tag(Rerun(10))) {
println("Hello") // will run 10 times
class MyRerunSuite extends munit.FunSuite {
override def munitTestTransforms = super.munitTestTransforms ++ List(
new TestTransform("Rerun", { test =>
val rerunCount = test.tags
.collectFirst { case Rerun(n) => n }
.getOrElse(1)
if (rerunCount == 1) test
else {
test.withBody(() => {
Future.sequence(1.to(rerunCount).map(_ => test.body()))
})
}
})
)
test("files".tag(Rerun(3))) {
println("Hello") // will run 3 times
}
test("files") {
// will run once, like normal
Expand All @@ -229,8 +233,11 @@ condition.
```scala mdoc
class ScalaVersionSuite extends munit.FunSuite {
val scalaVersion = scala.util.Properties.versionNumberString
override def munitNewTest(test: Test): Test =
test.withName(test.name + "-" + scalaVersion)
override def munitTestTransforms = super.munitTestTransforms ++ List(
new TestTransform("append Scala version", { test =>
test.withName(test.name + "-" + scalaVersion)
})
)
test("foo") {
assert(!scalaVersion.startsWith("2.11"))
}
Expand Down Expand Up @@ -295,8 +302,7 @@ in your project.
```scala mdoc
abstract class BaseSuite extends munit.FunSuite {
override val munitTimeout = Duration(1, "min")
override def munitTestValue(value: => Any): Future[Any] =
???
override def munitTestTransforms = super.munitTestTransforms ++ List(???)
// ...
}
class MyFirstSuite extends BaseSuite { /* ... */ }
Expand Down
51 changes: 51 additions & 0 deletions munit/shared/src/main/scala/munit/FunFixtures.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package munit

trait FunFixtures { self: FunSuite =>

class FunFixture[T](
val setup: TestOptions => T,
val teardown: T => Unit
) {
def test(options: TestOptions)(
body: T => Any
)(implicit loc: Location): Unit = {
self.test(options) {
val argument = setup(options)
try body(argument)
finally teardown(argument)
}(loc)
}
}
object FunFixture {
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))
},
teardown = {
case (argumentA, argumentB) =>
try a.teardown(argumentA)
finally b.teardown(argumentB)
}
)
def map3[A, B, C](
a: FunFixture[A],
b: FunFixture[B],
c: FunFixture[C]
): FunFixture[(A, B, C)] =
new FunFixture[(A, B, C)](
setup = { options =>
(a.setup(options), b.setup(options), c.setup(options))
},
teardown = {
case (argumentA, argumentB, argumentC) =>
try a.teardown(argumentA)
finally {
try b.teardown(argumentB)
finally c.teardown(argumentC)
}
}
)
}

}
185 changes: 17 additions & 168 deletions munit/shared/src/main/scala/munit/FunSuite.scala
Original file line number Diff line number Diff line change
@@ -1,195 +1,44 @@
package munit

import munit.internal.console.StackTraces
import munit.internal.FutureCompat._

import scala.collection.mutable
import scala.util.Failure
import scala.util.Success
import scala.concurrent.Future
import scala.util.control.NonFatal
import scala.util.Try
import scala.concurrent.duration.Duration
import munit.internal.PlatformCompat

abstract class FunSuite
extends Suite
with Assertions
with TestOptionsConversions { self =>
with FunFixtures
with TestOptionsConversions
with TestTransforms
with SuiteTransforms
with ValueTransforms { self =>

final type TestValue = Future[Any]

def isCI: Boolean = "true" == System.getenv("CI")
def munitIgnore: Boolean = false
def munitFlakyOK: Boolean = "true" == System.getenv("MUNIT_FLAKY_OK")

private val defaultTimeout = Duration(30, "s")
def munitTimeout: Duration = defaultTimeout
val munitTestsBuffer: mutable.ArrayBuffer[Test] =
mutable.ArrayBuffer.empty[Test]
final val munitTestsBuffer: mutable.ListBuffer[Test] =
mutable.ListBuffer.empty[Test]
def munitTests(): Seq[Test] = {
if (munitIgnore) {
Nil
} else {
val onlyTests = munitTestsBuffer.filter(_.tags(Only))
if (onlyTests.nonEmpty) {
if (isCI) {
onlyTests.toSeq.map(t =>
if (t.tags(Only)) {
t.withBody[TestValue](() =>
fail("'Only' tag is not allowed when `isCI=true`")(t.location)
)
} else {
t
}
)
} else {
onlyTests.toSeq
}
} else {
munitTestsBuffer.toSeq
}
}
munitSuiteTransform(munitTestsBuffer.toList)
}

def munitTestValue(testValue: => Any): Future[Any] = {
// Takes an arbitrarily nested future `Future[Future[Future[...]]]` and
// returns a `Future[T]` where `T` is not a `Future`.
def flattenFuture(future: Future[_]): Future[_] = {
val nested = future.map {
case f: Future[_] => flattenFuture(f)
case x => Future.successful(x)
}(munitExecutionContext)
nested.flattenCompat(munitExecutionContext)
}
val wrappedFuture = Future.fromTry(Try(StackTraces.dropOutside(testValue)))
val flatFuture = flattenFuture(wrappedFuture)
val awaitedFuture = PlatformCompat.waitAtMost(flatFuture, munitTimeout)
awaitedFuture
}

def munitNewTest(test: Test): Test =
test

def test(name: String)(
body: => Any
)(implicit loc: Location): Unit = {
def test(name: String)(body: => Any)(implicit loc: Location): Unit = {
test(new TestOptions(name, Set.empty, loc))(body)
}

def test(options: TestOptions)(
body: => Any
)(implicit loc: Location): Unit = {
munitTestsBuffer += munitNewTest(
def test(options: TestOptions)(body: => Any)(implicit loc: Location): Unit = {
munitTestsBuffer += munitTestTransform(
new Test(
options.name, { () =>
munitRunTest(options, () => {
try {
munitTestValue(body)
} catch {
case NonFatal(e) =>
Future.failed(e)
}
})
try {
munitValueTransform(body)
} catch {
case NonFatal(e) =>
Future.failed(e)
}
},
options.tags.toSet,
loc
)
)
}

def munitRunTest(
options: TestOptions,
body: () => Future[Any]
): Future[Any] = {
if (options.tags(Fail)) {
munitExpectFailure(options, body)
} else if (options.tags(Flaky)) {
munitFlaky(options, body)
} else {
body()
}
}

def munitFlaky(
options: TestOptions,
body: () => Future[Any]
): Future[Any] = {
body().transformCompat {
case Success(value) => Success(value)
case Failure(exception) =>
if (munitFlakyOK) {
Success(new TestValues.FlakyFailure(exception))
} else {
throw exception
}
}(munitExecutionContext)
}

def munitExpectFailure(
options: TestOptions,
body: () => Future[Any]
): Future[Any] = {
body().transformCompat {
case Success(value) =>
Failure(
throw new FailException(
munitLines.formatLine(
options.location,
"expected failure but test passed"
),
options.location
)
)
case Failure(exception) =>
Success(())
}(munitExecutionContext)
}

class FunFixture[T](
val setup: TestOptions => T,
val teardown: T => Unit
) {
def test(options: TestOptions)(
body: T => Any
)(implicit loc: Location): Unit = {
self.test(options) {
val argument = setup(options)
try body(argument)
finally teardown(argument)
}(loc)
}
}
object FunFixture {
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))
},
teardown = {
case (argumentA, argumentB) =>
try a.teardown(argumentA)
finally b.teardown(argumentB)
}
)
def map3[A, B, C](
a: FunFixture[A],
b: FunFixture[B],
c: FunFixture[C]
): FunFixture[(A, B, C)] =
new FunFixture[(A, B, C)](
setup = { options =>
(a.setup(options), b.setup(options), c.setup(options))
},
teardown = {
case (argumentA, argumentB, argumentC) =>
try a.teardown(argumentA)
finally {
try b.teardown(argumentB)
finally c.teardown(argumentC)
}
}
)
}

}
4 changes: 4 additions & 0 deletions munit/shared/src/main/scala/munit/GenericTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class GenericTest[T](
withTags(tags + newTag)
def withLocation(newLocation: Location): GenericTest[T] =
copy(location = newLocation)

def withBodyMap[A](newBody: T => A): GenericTest[A] =
withBody[A](() => newBody(body()))

private[this] def copy[A](
name: String = this.name,
body: () => A = this.body,
Expand Down
10 changes: 9 additions & 1 deletion munit/shared/src/main/scala/munit/MUnitRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import java.util.concurrent.ExecutionException

class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite)
extends Runner
Expand Down Expand Up @@ -222,7 +223,14 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite)
Future.successful(())
case NonFatal(ex) =>
StackTraces.trimStackTrace(ex)
val failure = new Failure(description, ex)
val cause = ex match {
case e: ExecutionException
if "Boxed Exception" == e.getMessage() &&
e.getCause() != null =>
e.getCause()
case e => e
}
val failure = new Failure(description, cause)
ex match {
case _: AssumptionViolatedException =>
notifier.fireTestAssumptionFailed(failure)
Expand Down
Loading