Skip to content

Commit

Permalink
Merge pull request #64 from olafurpg/new-test
Browse files Browse the repository at this point in the history
Use consistent solution to customize tests, suites and values
  • Loading branch information
olafurpg authored Mar 14, 2020
2 parents 775f73e + 51609d5 commit 577fd68
Show file tree
Hide file tree
Showing 19 changed files with 460 additions and 226 deletions.
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

0 comments on commit 577fd68

Please sign in to comment.