-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use consistent solution to customize tests, suites and values
Previously, users could override several methods to customize how tests were transformed, how suites filtered tests and how values got lifted into futures. While this system worked OK, it wasn't easy to explain since it was quite inconsistent. Now, this commit introduces three consistent solutions to transform: * test cases: a function `Test => Test` * test suites: a function `List[Test] => List[Test]` * test values: a partial function `Any => Future[Any]` Users register new transforms using a similar API as you register fixtures: ```scala override def munitTestTransforms: List[TestTransform] = super.munitTestTransforms ++ List( new TestTransform("append scala version", { test => test.withName(test.name + "-" + scalaVersion) }) ) ```
- Loading branch information
Olafur Pall Geirsson
committed
Mar 11, 2020
1 parent
775f73e
commit 94a596c
Showing
11 changed files
with
327 additions
and
221 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.