diff --git a/build.sbt b/build.sbt index 541ac1ef..9e07406b 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,4 @@ -import com.typesafe.tools.mima.core.DirectMissingMethodProblem -import com.typesafe.tools.mima.core.ProblemFilters -import com.typesafe.tools.mima.core.MissingTypesProblem +import com.typesafe.tools.mima.core._ import sbtcrossproject.CrossPlugin.autoImport.crossProject import sbtcrossproject.CrossPlugin.autoImport.CrossType import scala.collection.mutable @@ -85,6 +83,26 @@ lazy val mimaEnable: List[Def.Setting[_]] = List( ), ProblemFilters.exclude[DirectMissingMethodProblem]( "munit.internal.junitinterface.JUnitComputer.this" + ), + // Known breaking changes for MUnit v1 + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.FunSuite.munitTestTransform" + ), + ProblemFilters.exclude[MissingClassProblem]("munit.GenericAfterEach"), + ProblemFilters.exclude[MissingClassProblem]("munit.GenericBeforeEach"), + ProblemFilters.exclude[MissingClassProblem]("munit.GenericTest"), + ProblemFilters.exclude[DirectMissingMethodProblem]( + "munit.MUnitRunner.createTestDescription" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.Suite.beforeEach" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.Suite.afterEach" + ), + ProblemFilters.exclude[MissingClassProblem]("munit.Suite$Fixture"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.TestTransforms#TestTransform.apply" ) ), mimaPreviousArtifacts := { diff --git a/docs/fixtures.md b/docs/fixtures.md index 2511f24b..e5503af6 100644 --- a/docs/fixtures.md +++ b/docs/fixtures.md @@ -57,12 +57,12 @@ to reusable or ad-hoc fixtures when necessary. ## Reusable test-local fixtures Reusable test-local fixtures are more powerful than functional test-local -fixtures because they can declare custom logic that gets evaluted before each +fixtures because they can declare custom logic that gets evaluated before each local test case and get torn down after each test case. These increased capabilities come at the price of ergonomics of the API. -Override the `beforeEach()` and `afterEach()` methods in the `Fixture[T]` trait -to configure a reusable test-local fixture. +Override the `beforeEach()`, `afterEach()` and `munitFixtures` methods in the +`Fixture[T]` trait to configure a reusable test-local fixture. ```scala mdoc:reset import java.nio.file._ @@ -179,6 +179,73 @@ class MySuite extends munit.FunSuite { } ``` +## Asynchronous fixtures with `FutureFixture` + +Extend `FutureFixture[T]` to return `Future[T]` values from the lifecycle +methods `beforeAll`, `beforeEach`, `afterEach` and `afterAll`. + +```scala mdoc:reset +import java.nio.file._ +import java.sql.Connection +import java.sql.DriverManager +import munit.FutureFixture +import munit.FunSuite +import scala.concurrent.Future +import scala.concurrent.ExecutionContext.Implicits.global + +class AsyncFilesSuite extends FunSuite { + + val file = new FutureFixture[Path]("files") { + var file: Path = null + def apply() = file + override def beforeEach(context: BeforeEach): Future[Unit] = Future { + file = Files.createTempFile("files", context.test.name) + } + override def afterEach(context: AfterEach): Future[Unit] = Future { + // Always gets called, even if test failed. + Files.deleteIfExists(file) + } + } + + override def munitFixtures = List(file) + + test("exists") { + // `file` is the temporary file that was created for this test case. + assert(Files.exists(file())) + } +} +``` + +## Asynchronous fixtures with custom effect type + +First, create a new `EffectFixture[T]` class that extends `munit.AnyFixture[T]` +and overrides all lifecycle methods to return values of type `Effect[Unit]`. For +example: + +```scala mdoc:reset +import munit.AfterEach +import munit.BeforeEach + +// Hypothetical effect type called "Resource" +sealed abstract class Resource[+T] +object Resource { + def unit: Resource[Unit] = ??? +} + +abstract class ResourceFixture[T](name: String) extends munit.AnyFixture[T](name) { + // The main purpose of "ResourceFixture" is to help IDEs auto-complete + // the result type "Resource[Unit]" instead of "Any" when implementing the + // "ResourceFixture" class. + override def beforeAll(): Resource[Unit] = Resource.unit + override def beforeEach(context: BeforeEach): Resource[Unit] = Resource.unit + override def afterEach(context: AfterEach): Resource[Unit] = Resource.unit + override def afterAll(): Resource[Unit] = Resource.unit +} +``` + +Next, extend `munitValueTransforms` to convert `Resource[T]` into `Future[T]`, +see [declare async tests](tests.md#declare-async-test) for more details. + ## Avoid stateful operations in the class constructor Test classes may sometimes get initialized even if no tests run so it's best to diff --git a/docs/tests.md b/docs/tests.md index 10bbde94..3463d7fa 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -86,8 +86,8 @@ test("buggy-task") { ``` Since tasks are lazy, a test that returns `LazyFuture[T]` will always pass since -you need to call `run()` to start the task execution. Override `munitValueTransforms` -to make sure that `LazyFuture.run()` gets called. +you need to call `run()` to start the task execution. Override +`munitValueTransforms` to make sure that `LazyFuture.run()` gets called. ```scala mdoc import scala.concurrent.ExecutionContext.Implicits.global @@ -118,9 +118,10 @@ parallel test suite execution in sbt, add the following setting to `build.sbt`. Test / parallelExecution := false ``` -In case you do not run your tests in parallel, you can also disable buffered logging, -which is on by default to prevent test results of multiple suites from appearing interleaved. -Switching buffering off would give you immediate feedback on the console while a suite is running. +In case you do not run your tests in parallel, you can also disable buffered +logging, which is on by default to prevent test results of multiple suites from +appearing interleaved. Switching buffering off would give you immediate feedback +on the console while a suite is running. ```sh Test / testOptions += Tests.Argument(TestFrameworks.MUnit, "-b") @@ -199,9 +200,9 @@ bug report but don't have a solution to fix the issue yet. ## Customize evaluation of tests with tags -Override `munitTestTransforms()` to extend the default behavior for how test bodies are -evaluated. For example, use this feature to implement a `Rerun(N)` modifier to -evaluate the body multiple times. +Override `munitTestTransforms()` to extend the default behavior for how test +bodies are evaluated. For example, use this feature to implement a `Rerun(N)` +modifier to evaluate the body multiple times. ```scala mdoc case class Rerun(count: Int) extends munit.Tag("Rerun") @@ -228,9 +229,9 @@ class MyRerunSuite extends munit.FunSuite { } ``` -The `munitTestTransforms()` method is similar to `munitValueTransforms()` but is different in -that you also have access information about the test in `TestOptions` such as -tags. +The `munitTestTransforms()` method is similar to `munitValueTransforms()` but is +different in that you also have access information about the test in +`TestOptions` such as tags. ## Customize test name based on a dynamic condition @@ -316,46 +317,3 @@ abstract class BaseSuite extends munit.FunSuite { class MyFirstSuite extends BaseSuite { /* ... */ } class MySecondSuite extends BaseSuite { /* ... */ } ``` - -## Roll our own testing library with `munit.Suite` - -The `munit.FunSuite` class comes with a lot of built-in functionality such as -assertions, fixtures, `munitTimeout()` helpers and more. These features may not -be necessary or even desirable when writing tests. You may sometimes prefer a -smaller API. - -Extend the base class `munit.Suite` to implement a minimal test suite that -includes no optional MUnit features. At its core, MUnit operates on a data -structure `GenericTest[TestValue]` where the type parameter `TestValue` -represents the return value of test bodies. This type parameter can be -customized per-suite. In `munit.FunSuite`, the type parameter `TestValue` is -defined as `Any` and `type Test = GenericTest[Any]`. - -Below is an example custom test suite with `type TestValue = Future[String]`. - -```scala -class MyCustomSuite extends munit.Suite { - override type TestValue = Future[String] - override def munitTests() = List( - new Test( - "name", - // compile error if it's not a Future[String] - body = () => Future.successful("Hello world!"), - tags = Set.empty[Tag], - location = implicitly[Location] - ) - ) -} -``` - -Some use-cases where you may want to define a custom `munit.Suite`: - -- implement APIs that mimic testing libraries to simplify the migration to MUnit -- design stricter APIs that don't use `Any` -- design purely functional APIs with no publicly facing side-effects - -In application code, it's desirable to use strong types avoid mutable state. -However, it's not clear that those best practices yield the same cost/benefit -ratio when writing test code. MUnit intentionally exposes types such `Any` and -side-effecting methods like `test("name") { ... }` because they subjectively -make the testing API nice-to-use. diff --git a/munit-scalacheck/shared/src/main/scala/munit/ScalaCheckSuite.scala b/munit-scalacheck/shared/src/main/scala/munit/ScalaCheckSuite.scala index 58b2bac3..a0d3d4b7 100644 --- a/munit-scalacheck/shared/src/main/scala/munit/ScalaCheckSuite.scala +++ b/munit-scalacheck/shared/src/main/scala/munit/ScalaCheckSuite.scala @@ -41,7 +41,7 @@ trait ScalaCheckSuite extends FunSuite { new TestTransform( "ScalaCheck Prop", t => { - t.withBodyMap[TestValue]( + t.withBodyMap( _.transformCompat { case Success(prop: Prop) => propToTry(prop, t) case r => r diff --git a/munit/js/src/main/scala/munit/internal/PlatformCompat.scala b/munit/js/src/main/scala/munit/internal/PlatformCompat.scala index 700088ad..fa16b03d 100644 --- a/munit/js/src/main/scala/munit/internal/PlatformCompat.scala +++ b/munit/js/src/main/scala/munit/internal/PlatformCompat.scala @@ -9,6 +9,7 @@ import sbt.testing.EventHandler import sbt.testing.Logger import scala.concurrent.Promise import scala.concurrent.duration.Duration +import scala.concurrent.ExecutionContext object PlatformCompat { def executeAsync( @@ -20,7 +21,12 @@ object PlatformCompat { task.execute(eventHandler, loggers, _ => p.success(())) p.future } - def waitAtMost[T](future: Future[T], duration: Duration): Future[T] = { + + def waitAtMost[T]( + future: Future[T], + duration: Duration, + ec: ExecutionContext + ): Future[T] = { future } diff --git a/munit/jvm/src/main/scala/munit/internal/PlatformCompat.scala b/munit/jvm/src/main/scala/munit/internal/PlatformCompat.scala index 2f46d7d8..4eb2918e 100644 --- a/munit/jvm/src/main/scala/munit/internal/PlatformCompat.scala +++ b/munit/jvm/src/main/scala/munit/internal/PlatformCompat.scala @@ -4,11 +4,15 @@ import scala.concurrent.Future import sbt.testing.Task import sbt.testing.EventHandler import sbt.testing.Logger -import scala.concurrent.Await import scala.concurrent.duration.Duration -import scala.util.Try +import java.util.concurrent.Executors +import scala.concurrent.Promise +import scala.concurrent.ExecutionContext +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException object PlatformCompat { + private val sh = Executors.newSingleThreadScheduledExecutor() def executeAsync( task: Task, eventHandler: EventHandler, @@ -17,8 +21,38 @@ object PlatformCompat { task.execute(eventHandler, loggers) Future.successful(()) } - def waitAtMost[T](future: Future[T], duration: Duration): Future[T] = { - Future.fromTry(Try(Await.result(future, duration))) + @deprecated("use the overload with an explicit ExecutionContext", "1.0.0") + def waitAtMost[T]( + future: Future[T], + duration: Duration + ): Future[T] = { + waitAtMost(future, duration, ExecutionContext.global) + } + def waitAtMost[T]( + future: Future[T], + duration: Duration, + ec: ExecutionContext + ): Future[T] = { + if (future.value.isDefined) { + // Avoid heavy timeout overhead for non-async tests. + future + } else { + val onComplete = Promise[T]() + var onCancel: () => Unit = () => () + future.onComplete { result => + onComplete.tryComplete(result) + }(ec) + val timeout = sh.schedule[Unit]( + () => + onComplete.tryFailure( + new TimeoutException(s"test timed out after $duration") + ), + duration.toMillis, + TimeUnit.MILLISECONDS + ) + onCancel = () => timeout.cancel(false) + onComplete.future + } } def isIgnoreSuite(cls: Class[_]): Boolean = diff --git a/munit/native/src/main/scala/munit/internal/PlatformCompat.scala b/munit/native/src/main/scala/munit/internal/PlatformCompat.scala index 464b3067..c93d87e3 100644 --- a/munit/native/src/main/scala/munit/internal/PlatformCompat.scala +++ b/munit/native/src/main/scala/munit/internal/PlatformCompat.scala @@ -8,6 +8,7 @@ import sbt.testing.Task import sbt.testing.EventHandler import sbt.testing.Logger import scala.concurrent.duration.Duration +import scala.concurrent.ExecutionContext object PlatformCompat { def executeAsync( @@ -18,7 +19,11 @@ object PlatformCompat { task.execute(eventHandler, loggers) Future.successful(()) } - def waitAtMost[T](future: Future[T], duration: Duration): Future[T] = { + def waitAtMost[T]( + future: Future[T], + duration: Duration, + ec: ExecutionContext + ): Future[T] = { future } diff --git a/munit/shared/src/main/scala/munit/AfterEach.scala b/munit/shared/src/main/scala/munit/AfterEach.scala new file mode 100644 index 00000000..74b8d234 --- /dev/null +++ b/munit/shared/src/main/scala/munit/AfterEach.scala @@ -0,0 +1,5 @@ +package munit + +class AfterEach( + val test: Test +) extends Serializable diff --git a/munit/shared/src/main/scala/munit/AnyFixture.scala b/munit/shared/src/main/scala/munit/AnyFixture.scala new file mode 100644 index 00000000..d44cc124 --- /dev/null +++ b/munit/shared/src/main/scala/munit/AnyFixture.scala @@ -0,0 +1,37 @@ +package munit + +/** + * AnyFixture allows you to acquire resources during setup and clean up resources after the tests finish running. + * + * Fixtures can be local to a single test case by overriding `beforeEach` and + * `afterEach`, or they can be re-used for an entire test suite by extending + * `beforeAll` and `afterAll`. + * + * It's preferable to use a sub-class like `munit.Fixture` or + * `munit.FutureFixture` instead of this class. Extend this class if you're + * writing an integration a third-party type like Cats `Resource`. + * + * @see https://scalameta.org/munit/docs/fixtures.html + * @param fixtureName The name of this fixture, used for displaying an error message if + * `beforeAll()` or `afterAll()` fail. + */ +abstract class AnyFixture[T](val fixtureName: String) { + + /** The value produced by this suite-local fixture that can be reused for all test cases. */ + def apply(): T + + /** Runs once before the test suite starts */ + def beforeAll(): Any = () + + /** + * Runs before each individual test case. An error in this method aborts the test case. + */ + def beforeEach(context: BeforeEach): Any = () + + /** Runs after each individual test case. */ + def afterEach(context: AfterEach): Any = () + + /** Runs once after the test suite has finished, regardless if the tests failed or not. */ + def afterAll(): Any = () + +} diff --git a/munit/shared/src/main/scala/munit/BeforeEach.scala b/munit/shared/src/main/scala/munit/BeforeEach.scala new file mode 100644 index 00000000..99c8406b --- /dev/null +++ b/munit/shared/src/main/scala/munit/BeforeEach.scala @@ -0,0 +1,5 @@ +package munit + +class BeforeEach( + val test: Test +) extends Serializable diff --git a/munit/shared/src/main/scala/munit/Fixture.scala b/munit/shared/src/main/scala/munit/Fixture.scala new file mode 100644 index 00000000..ec174704 --- /dev/null +++ b/munit/shared/src/main/scala/munit/Fixture.scala @@ -0,0 +1,23 @@ +package munit + +/** + * Fixture allows you to acquire resources during setup and clean up resources after the tests finish running. + * + * Fixtures can be local to a single test case by overriding `beforeEach` and + * `afterEach`, or they can be re-used for an entire test suite by extending + * `beforeAll` and `afterAll`. + * + * There is no functional difference between extending `Fixture[T]` or + * `AnyFixture[T]`. The only difference is that an IDE will auto-complete `Unit` + * in the result type instead of `Any`. + * + * @see https://scalameta.org/munit/docs/fixtures.html + * @param fixtureName The name of this fixture, used for displaying an error message if + * `beforeAll()` or `afterAll()` fail. + */ +abstract class Fixture[T](name: String) extends AnyFixture[T](name) { + override def beforeAll(): Unit = () + override def beforeEach(context: BeforeEach): Unit = () + override def afterEach(context: AfterEach): Unit = () + override def afterAll(): Unit = () +} diff --git a/munit/shared/src/main/scala/munit/FunSuite.scala b/munit/shared/src/main/scala/munit/FunSuite.scala index 7ef2ee1b..b9bdd6b7 100644 --- a/munit/shared/src/main/scala/munit/FunSuite.scala +++ b/munit/shared/src/main/scala/munit/FunSuite.scala @@ -17,8 +17,6 @@ abstract class FunSuite with SuiteTransforms with ValueTransforms { self => - final type TestValue = Future[Any] - final val munitTestsBuffer: mutable.ListBuffer[Test] = mutable.ListBuffer.empty[Test] def munitTests(): Seq[Test] = { @@ -48,6 +46,6 @@ abstract class FunSuite def munitTimeout: Duration = new FiniteDuration(30, TimeUnit.SECONDS) private final def waitForCompletion[T](f: Future[T]) = - PlatformCompat.waitAtMost(f, munitTimeout) + PlatformCompat.waitAtMost(f, munitTimeout, munitExecutionContext) } diff --git a/munit/shared/src/main/scala/munit/FutureFixture.scala b/munit/shared/src/main/scala/munit/FutureFixture.scala new file mode 100644 index 00000000..c38a6181 --- /dev/null +++ b/munit/shared/src/main/scala/munit/FutureFixture.scala @@ -0,0 +1,29 @@ +package munit + +import scala.concurrent.Future + +/** + * FutureFixture allows you to acquire resources during setup and clean up resources after the tests finish running. + * + * Fixtures can be local to a single test case by overriding `beforeEach` and + * `afterEach`, or they can be re-used for an entire test suite by extending + * `beforeAll` and `afterAll`. + * + * There is no functional difference between extending `FutureFixture[T]` or + * `AnyFixture[T]`. The only difference is that an IDE will auto-complete + * `Future[Unit]` in the result type instead of `Any`. + * + * @see https://scalameta.org/munit/docs/fixtures.html + * @param fixtureName The name of this fixture, used for displaying an error message if + * `beforeAll()` or `afterAll()` fail. + */ +abstract class FutureFixture[T](name: String) extends AnyFixture[T](name) { + override def beforeAll(): Future[Unit] = + Future.successful(()) + override def beforeEach(context: BeforeEach): Future[Unit] = + Future.successful(()) + override def afterEach(context: AfterEach): Future[Unit] = + Future.successful(()) + override def afterAll(): Future[Unit] = + Future.successful(()) +} diff --git a/munit/shared/src/main/scala/munit/GenericBeforeEach.scala b/munit/shared/src/main/scala/munit/GenericBeforeEach.scala deleted file mode 100644 index dcaf6e64..00000000 --- a/munit/shared/src/main/scala/munit/GenericBeforeEach.scala +++ /dev/null @@ -1,9 +0,0 @@ -package munit - -class GenericBeforeEach[T]( - val test: GenericTest[T] -) extends Serializable - -class GenericAfterEach[T]( - val test: GenericTest[T] -) extends Serializable diff --git a/munit/shared/src/main/scala/munit/MUnitRunner.scala b/munit/shared/src/main/scala/munit/MUnitRunner.scala index ee8eeb54..4cbe4116 100644 --- a/munit/shared/src/main/scala/munit/MUnitRunner.scala +++ b/munit/shared/src/main/scala/munit/MUnitRunner.scala @@ -1,32 +1,31 @@ package munit -import munit.internal.junitinterface.Configurable +import munit.internal.FutureCompat._ import munit.internal.PlatformCompat +import munit.internal.console.Printers +import munit.internal.console.StackTraces +import munit.internal.junitinterface.Configurable +import munit.internal.junitinterface.Settings +import org.junit.AssumptionViolatedException import org.junit.runner.Description +import org.junit.runner.Runner +import org.junit.runner.manipulation.Filter +import org.junit.runner.manipulation.Filterable +import org.junit.runner.notification.Failure import org.junit.runner.notification.RunNotifier + import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier import java.lang.reflect.UndeclaredThrowableException - -import munit.internal.FutureCompat._ -import munit.internal.console.StackTraces -import org.junit.runner.notification.Failure - -import scala.util.control.NonFatal -import org.junit.runner.manipulation.Filterable -import org.junit.runner.manipulation.Filter -import org.junit.runner.Runner -import org.junit.AssumptionViolatedException - +import java.util.concurrent.ExecutionException import scala.collection.mutable -import scala.util.Try import scala.concurrent.Await -import scala.concurrent.duration.Duration -import scala.concurrent.Future import scala.concurrent.ExecutionContext -import java.util.concurrent.ExecutionException -import munit.internal.junitinterface.Settings -import munit.internal.console.Printers +import scala.concurrent.Future +import scala.concurrent.duration.Duration +import scala.util.Success +import scala.util.Try +import scala.util.control.NonFatal class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) extends Runner @@ -43,13 +42,26 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) @volatile private var settings: Settings = Settings.defaults() @volatile private var suiteAborted: Boolean = false - private val descriptions: mutable.Map[suite.Test, Description] = - mutable.Map.empty[suite.Test, Description] + private val descriptions: mutable.Map[Test, Description] = + mutable.Map.empty[Test, Description] private val testNames: mutable.Set[String] = mutable.Set.empty[String] - private lazy val munitTests: mutable.ArrayBuffer[suite.Test] = - mutable.ArrayBuffer[suite.Test](suite.munitTests(): _*) + private lazy val munitTests: mutable.ArrayBuffer[Test] = + mutable.ArrayBuffer[Test](suite.munitTests(): _*) + private val suiteFixture = new Fixture[Unit](cls.getName()) { + def apply(): Unit = () + override def beforeAll(): Unit = + suite.beforeAll() + override def beforeEach(context: BeforeEach): Unit = + suite.beforeEach(context) + override def afterEach(context: AfterEach): Unit = + suite.afterEach(context) + override def afterAll(): Unit = + suite.afterAll() + } + private lazy val munitFixtures: List[AnyFixture[_]] = + suiteFixture :: suite.munitFixtures.toList override def filter(filter: Filter): Unit = { val newTests = munitTests.filter { t => @@ -62,7 +74,7 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) this.settings = settings } - def createTestDescription(test: suite.Test): Description = { + private def createTestDescription(test: Test): Description = { descriptions.getOrElseUpdate( test, { val escapedName = Printers.escapeNonVisible(test.name) @@ -109,35 +121,56 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) def runAsync(notifier: RunNotifier): Future[Unit] = { val description = getDescription() notifier.fireTestSuiteStarted(description) - try { - runAll(notifier) - } catch { - case ex: Throwable => - Future.successful( - fireHiddenTest(notifier, "expected error running tests", ex) + runAll(notifier) + .transformCompat[Unit](result => { + result.failed.foreach(ex => + fireFailedHiddenTest(notifier, "unexpected error running tests", ex) ) - } finally { - notifier.fireTestSuiteFinished(description) - } + notifier.fireTestSuiteFinished(description) + util.Success(()) + }) } - private def runAsyncTestsSynchronously( + private def runTests( notifier: RunNotifier - ): Future[Unit] = { - def loop(it: Iterator[suite.Test]): Future[Unit] = + ): Future[List[Try[Boolean]]] = { + sequenceFutures( + munitTests.iterator.map(t => runTest(notifier, t)) + ) + } + + // Similar `Future.sequence` but with cleaner stack traces for non-async code. + private def sequenceFutures[A]( + futures: Iterator[Future[A]] + ): Future[List[Try[A]]] = { + def loop( + it: Iterator[Future[A]], + acc: mutable.ListBuffer[Try[A]] + ): Future[List[Try[A]]] = if (!it.hasNext) { - Future.successful(()) + Future.successful(acc.toList) } else { - val future = runTest(notifier, it.next()) + val future = it.next() future.value match { - case Some(_) => + case Some(value) => + acc += value // use tail-recursive call if possible to keep stack traces clean. - loop(it) + loop(it, acc) case None => - future.flatMap(_ => loop(it)) + future.flatMap(t => { + acc += util.Success(t) + loop(it, acc) + }) } } - loop(munitTests.iterator) + loop(futures, mutable.ListBuffer.empty) + } + + private def munitTimeout(): Option[Duration] = { + suite match { + case funSuite: FunSuite => Some(funSuite.munitTimeout) + case _ => None + } } private def runAll(notifier: RunNotifier): Future[Unit] = { @@ -146,83 +179,96 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) notifier.fireTestIgnored(description) return Future.successful(()) } - var isBeforeAllRun = false - val result = { - val isContinue = runBeforeAll(notifier) - isBeforeAllRun = isContinue - if (isContinue) { - runAsyncTestsSynchronously(notifier) - } else { - Future.successful(()) - } - } - result.transformCompat { s => - if (isBeforeAllRun) { - runAfterAll(notifier) + for { + beforeAll <- runBeforeAll(notifier) + _ <- { + if (beforeAll.isSuccess) { + runTests(notifier) + } else { + Future.successful(Nil) + } } - s - } + _ <- runAfterAll(notifier, beforeAll) + } yield () } - private def runBeforeAll(notifier: RunNotifier): Boolean = { - var isContinue = runHiddenTest(notifier, "beforeAll", suite.beforeAll()) - suite.munitFixtures.foreach { fixture => - isContinue &= runHiddenTest( - notifier, - s"beforeAllFixture(${fixture.fixtureName})", - fixture.beforeAll() + private[munit] class BeforeAllResult( + val isSuccess: Boolean, + val loadedFixtures: List[AnyFixture[_]], + val errors: List[Throwable] + ) + + private def runBeforeAll(notifier: RunNotifier): Future[BeforeAllResult] = { + val result: Future[List[Try[(AnyFixture[_], Boolean)]]] = + sequenceFutures( + munitFixtures.iterator.map(f => + runHiddenTest( + notifier, + s"beforeAll(${f.fixtureName})", + () => f.beforeAll() + ).map(isSuccess => f -> isSuccess) + ) ) + result.map { results => + val loadedFixtures = results.collect { case Success((fixture, true)) => + fixture + } + val errors = results.collect { case util.Failure(ex) => ex } + val isSuccess = loadedFixtures.length == results.length + new BeforeAllResult(isSuccess, loadedFixtures, errors) } - isContinue } - private def runAfterAll(notifier: RunNotifier): Unit = { - suite.munitFixtures.foreach { fixture => - runHiddenTest( - notifier, - s"afterAllFixture(${fixture.fixtureName})", - fixture.afterAll() + + private def runAfterAll( + notifier: RunNotifier, + beforeAll: BeforeAllResult + ): Future[Unit] = { + sequenceFutures[Boolean]( + beforeAll.loadedFixtures.iterator.map(f => + runHiddenTest( + notifier, + s"afterAll(${f.fixtureName})", + () => f.afterAll() + ) ) - } - runHiddenTest(notifier, "afterAll", suite.afterAll()) + ).map(_ => ()) } - class BeforeEachResult( + private[munit] class BeforeEachResult( val error: Option[Throwable], - val loadedFixtures: List[suite.Fixture[_]] + val loadedFixtures: List[AnyFixture[_]] ) + private def runBeforeEach( - test: suite.Test - ): BeforeEachResult = { - val beforeEach = new GenericBeforeEach(test) - val fixtures = mutable.ListBuffer.empty[suite.Fixture[_]] - val error = foreachUnsafe( - List(() => suite.beforeEach(beforeEach)) ++ - suite.munitFixtures.map(fixture => - () => { - fixture.beforeEach(beforeEach) - fixtures += fixture - () - } - ) - ) - new BeforeEachResult(error.failed.toOption, fixtures.toList) + test: Test + ): Future[BeforeAllResult] = { + val context = new BeforeEach(test) + val fixtures = mutable.ListBuffer.empty[AnyFixture[_]] + sequenceFutures( + munitFixtures.iterator.map(f => + valueTransform(() => f.beforeEach(context)).map(_ => f) + ) + ).map { results => + val loadedFixtures = results.collect { case Success(f) => f } + val errors = results.collect { case util.Failure(ex) => ex } + val isSuccess = loadedFixtures.length == results.length + new BeforeAllResult(isSuccess, loadedFixtures, errors) + } } private def runAfterEach( - test: suite.Test, - fixtures: List[suite.Fixture[_]] - ): Unit = { - val afterEach = new GenericAfterEach(test) - val error = foreachUnsafe( - fixtures.map(fixture => () => fixture.afterEach(afterEach)) ++ - List(() => suite.afterEach(afterEach)) - ) - error.get // throw exception if it exists. + test: Test, + fixtures: List[AnyFixture[_]] + ): Future[Unit] = { + val context = new AfterEach(test) + sequenceFutures( + fixtures.iterator.map(f => valueTransform(() => f.afterEach(context))) + ).map(_ => ()) } private def runTest( notifier: RunNotifier, - test: suite.Test + test: Test ): Future[Boolean] = { val description = createTestDescription(test) @@ -287,20 +333,24 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) private def runTestBody( notifier: RunNotifier, description: Description, - test: suite.Test + test: Test ): Future[Unit] = { - val result: Future[Any] = StackTraces.dropOutside { - val beforeEachResult = runBeforeEach(test) - val any = beforeEachResult.error match { - case None => - try test.body() - finally runAfterEach(test, beforeEachResult.loadedFixtures) - case Some(error) => - try runAfterEach(test, beforeEachResult.loadedFixtures) - finally throw error + val result: Future[Any] = + runBeforeEach(test).flatMap[Any] { beforeEach => + beforeEach.errors match { + case Nil => + StackTraces + .dropOutside(test.body()) + .transformWithCompat(result => + runAfterEach(test, beforeEach.loadedFixtures) + .transformCompat(_ => result) + ) + case error :: errors => + errors.foreach(err => error.addSuppressed(err)) + try runAfterEach(test, beforeEach.loadedFixtures) + finally throw error + } } - futureFromAny(any) - } result.map { case f: TestValues.FlakyFailure => trimStackTrace(f) @@ -312,11 +362,22 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) } } - private def foreachUnsafe(thunks: Iterable[() => Unit]): Try[Unit] = { + private[munit] class ForeachUnsafeResult( + val sync: Try[Unit], + val async: List[Future[Any]] + ) + private def foreachUnsafe( + thunks: Iterable[() => Any] + ): ForeachUnsafeResult = { var errors = mutable.ListBuffer.empty[Throwable] + val async = mutable.ListBuffer.empty[Future[Any]] thunks.foreach { thunk => try { - thunk() + thunk() match { + case f: Future[_] => + async += f + case _ => + } } catch { case ex if NonFatal(ex) => errors += ex @@ -329,37 +390,45 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) head.addSuppressed(e) } } - scala.util.Failure(head) + new ForeachUnsafeResult(scala.util.Failure(head), Nil) case _ => - scala.util.Success(()) + new ForeachUnsafeResult(scala.util.Success(()), Nil) } } private def runHiddenTest( notifier: RunNotifier, name: String, - thunk: => Unit - ): Boolean = { + thunk: () => Any + ): Future[Boolean] = { try { - StackTraces.dropOutside(thunk) - true + StackTraces.dropOutside { + valueTransform(thunk) + .transformCompat { + case util.Success(value) => + util.Success(true) + case util.Failure(exception) => + fireFailedHiddenTest(notifier, name, exception) + util.Success(false) + } + } } catch { case ex: Throwable => - fireHiddenTest(notifier, name, ex) - false + fireFailedHiddenTest(notifier, name, ex) + Future.successful(false) } } - private def fireHiddenTest( + private def fireFailedHiddenTest( notifier: RunNotifier, name: String, ex: Throwable ): Unit = { - val test = new suite.Test(name, () => ???, Set.empty, Location.empty) + val test = new Test(name, () => ???, Set.empty, Location.empty) val description = createTestDescription(test) notifier.fireTestStarted(description) trimStackTrace(ex) - notifier.fireTestFailure(new Failure(description, ex)) + notifier.fireTestFailure(new Failure(description, rootCause(ex))) notifier.fireTestFinished(description) } private def trimStackTrace(ex: Throwable): Unit = { @@ -368,6 +437,13 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) } } + private def valueTransform(thunk: () => Any): Future[Any] = { + suite match { + case funSuite: FunSuite => funSuite.munitValueTransform(thunk()) + case _ => futureFromAny(thunk()) + } + } + } object MUnitRunner { private def ensureEligibleConstructor( diff --git a/munit/shared/src/main/scala/munit/Suite.scala b/munit/shared/src/main/scala/munit/Suite.scala index 03d4d052..ab856aa3 100644 --- a/munit/shared/src/main/scala/munit/Suite.scala +++ b/munit/shared/src/main/scala/munit/Suite.scala @@ -2,6 +2,7 @@ package munit import org.junit.runner.RunWith import scala.concurrent.ExecutionContext +import scala.concurrent.Future /** * The base class for all test suites. @@ -11,16 +12,17 @@ import scala.concurrent.ExecutionContext abstract class Suite extends PlatformSuite { /** The value produced by test bodies. */ - type TestValue - final type Test = GenericTest[TestValue] - final type BeforeEach = GenericBeforeEach[TestValue] - final type AfterEach = GenericAfterEach[TestValue] + final type TestValue = Future[Any] + final type Fixture[T] = munit.Fixture[T] + final type Test = munit.Test + final type BeforeEach = munit.BeforeEach + final type AfterEach = munit.AfterEach /** The base class for all test suites */ def munitTests(): Seq[Test] - /** Functinonal fixtures that can be reused for individual test cases or entire suites. */ - def munitFixtures: Seq[Fixture[_]] = Nil + /** Fixtures that can be reused for individual test cases or entire suites. */ + def munitFixtures: Seq[AnyFixture[_]] = Nil private val parasiticExecutionContext = new ExecutionContext { def execute(runnable: Runnable): Unit = runnable.run() @@ -28,32 +30,6 @@ abstract class Suite extends PlatformSuite { } def munitExecutionContext: ExecutionContext = parasiticExecutionContext - /** - * @param name The name of this fixture, used for displaying an error message if - * `beforeAll()` or `afterAll()` fail. - */ - abstract class Fixture[T](val fixtureName: String) { - - /** The value produced by this suite-local fixture that can be reused for all test cases. */ - def apply(): T - - /** Runs once before the test suite starts */ - def beforeAll(): Unit = () - - /** - * Runs before each individual test case. - * An error in this method aborts the test case. - */ - def beforeEach(context: BeforeEach): Unit = () - - /** Runs after each individual test case. */ - def afterEach(context: AfterEach): Unit = () - - /** Runs once after the test suite has finished, regardless if the tests failed or not. */ - def afterAll(): Unit = () - - } - /** * Runs once before all test cases and before all suite-local fixtures are setup. * An error in this method aborts the test suite. diff --git a/munit/shared/src/main/scala/munit/SuiteTransforms.scala b/munit/shared/src/main/scala/munit/SuiteTransforms.scala index 1a60af08..26fa13db 100644 --- a/munit/shared/src/main/scala/munit/SuiteTransforms.scala +++ b/munit/shared/src/main/scala/munit/SuiteTransforms.scala @@ -54,7 +54,7 @@ trait SuiteTransforms { this: FunSuite => } else { onlySuite.map(t => if (t.tags(Only)) { - t.withBody[TestValue](() => + t.withBody(() => fail("'Only' tag is not allowed when `isCI=true`")(t.location) ) } else { diff --git a/munit/shared/src/main/scala/munit/GenericTest.scala b/munit/shared/src/main/scala/munit/Test.scala similarity index 65% rename from munit/shared/src/main/scala/munit/GenericTest.scala rename to munit/shared/src/main/scala/munit/Test.scala index f808abc6..74d9ff8c 100644 --- a/munit/shared/src/main/scala/munit/GenericTest.scala +++ b/munit/shared/src/main/scala/munit/Test.scala @@ -2,6 +2,7 @@ package munit import java.lang.annotation.Annotation import scala.collection.mutable +import scala.concurrent.Future /** * Metadata about a single test case. @@ -10,37 +11,37 @@ import scala.collection.mutable * @param tags the annotated tags for this test case. * @param location the file and line number where this test was defined. */ -class GenericTest[T]( +final class Test( val name: String, - val body: () => T, + val body: () => Future[Any], val tags: Set[Tag], val location: Location ) extends Serializable { - def this(name: String, body: () => T)(implicit loc: Location) = + def this(name: String, body: () => Future[Any])(implicit loc: Location) = this(name, body, Set.empty, loc) - def withName(newName: String): GenericTest[T] = + def withName(newName: String): Test = copy(name = newName) - def withBody[A](newBody: () => A): GenericTest[A] = + def withBody(newBody: () => Future[Any]): Test = copy(body = newBody) - def withTags(newTags: Set[Tag]): GenericTest[T] = + def withTags(newTags: Set[Tag]): Test = copy(tags = newTags) - def tag(newTag: Tag): GenericTest[T] = + def tag(newTag: Tag): Test = withTags(tags + newTag) - def withLocation(newLocation: Location): GenericTest[T] = + def withLocation(newLocation: Location): Test = copy(location = newLocation) - def withBodyMap[A](newBody: T => A): GenericTest[A] = - withBody[A](() => newBody(body())) + def withBodyMap(newBody: Future[Any] => Future[Any]): Test = + withBody(() => newBody(body())) private[this] def copy[A]( name: String = this.name, - body: () => A = this.body, + body: () => Future[Any] = this.body, tags: Set[Tag] = this.tags, location: Location = this.location - ): GenericTest[A] = { - new GenericTest(name, body, tags, location) + ): Test = { + new Test(name, body, tags, location) } - override def toString(): String = s"GenericTest($name, $tags, $location)" + override def toString(): String = s"Test($name, $tags, $location)" // NOTE(olafur): tests have reference equality because there's no reasonable // structural equality that we can use to compare the test body function. override def equals(obj: Any): Boolean = this.eq(obj.asInstanceOf[AnyRef]) diff --git a/munit/shared/src/main/scala/munit/TestTransforms.scala b/munit/shared/src/main/scala/munit/TestTransforms.scala index 3fda7824..5e17fcde 100644 --- a/munit/shared/src/main/scala/munit/TestTransforms.scala +++ b/munit/shared/src/main/scala/munit/TestTransforms.scala @@ -26,7 +26,7 @@ trait TestTransforms { this: FunSuite => } } catch { case NonFatal(e) => - test.withBody[TestValue](() => Future.failed(e)) + test.withBody(() => Future.failed(e)) } } @@ -35,7 +35,7 @@ trait TestTransforms { this: FunSuite => "fail", { t => if (t.tags(Fail)) { - t.withBodyMap[TestValue]( + t.withBodyMap( _.transformCompat { case Success(value) => Failure( diff --git a/munit/shared/src/main/scala/munit/package.scala b/munit/shared/src/main/scala/munit/package.scala index 3f25dc83..75c54da1 100644 --- a/munit/shared/src/main/scala/munit/package.scala +++ b/munit/shared/src/main/scala/munit/package.scala @@ -4,4 +4,13 @@ package object munit { val Flaky = new Tag("Flaky") val Fail = new Tag("Fail") val Slow = new Tag("Slow") + + @deprecated("use BeforeEach instead", "1.0.0") + type GenericBeforeEach[T] = BeforeEach + + @deprecated("use AfterEach instead", "1.0.0") + type GenericAfterEach[T] = AfterEach + + @deprecated("use Test instead", "1.0.0") + type GenericTest[T] = Test } diff --git a/tests/jvm/src/test/scala/munit/AsyncFixtureSuite.scala b/tests/jvm/src/test/scala/munit/AsyncFixtureSuite.scala new file mode 100644 index 00000000..d1784dd1 --- /dev/null +++ b/tests/jvm/src/test/scala/munit/AsyncFixtureSuite.scala @@ -0,0 +1,98 @@ +package munit + +import scala.concurrent.Promise +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import scala.concurrent.ExecutionContext +import java.util.concurrent.ScheduledExecutorService + +class AsyncFixtureSuite extends BaseSuite { + case class PromiseWrapper(name: String, promise: Promise[_]) + override def munitValueTransforms: List[ValueTransform] = + super.munitValueTransforms ++ List( + new ValueTransform( + "PromiseWrapper", + { case p: PromiseWrapper => + p.promise.future + } + ) + ) + class ScheduledMessage() extends AnyFixture[String]("AsyncFixture") { + val sh: ScheduledExecutorService = + Executors.newSingleThreadScheduledExecutor() + private var didBeforeAllEvaluateAsync = false + private var promise = Promise[String]() + private val timeout = 20 + def apply(): String = promise.future.value.get.get + override def beforeAll(): PromiseWrapper = { + val setBeforeAllBit = Promise[Unit]() + sh.schedule[Unit]( + () => { + didBeforeAllEvaluateAsync = true + setBeforeAllBit.success(()) + }, + timeout, + TimeUnit.MILLISECONDS + ) + PromiseWrapper("beforeAll", setBeforeAllBit) + } + override def beforeEach(context: BeforeEach): PromiseWrapper = { + assertEquals( + promise.future.value, + None, + "promise did not get reset from afterEach" + ) + assert( + didBeforeAllEvaluateAsync, + "beforeAll promise did not complete yet" + ) + sh.schedule[Unit]( + () => promise.success(s"beforeEach-${context.test.name}"), + timeout, + TimeUnit.MILLISECONDS + ) + PromiseWrapper("beforeEach", promise) + } + override def afterEach(context: AfterEach): PromiseWrapper = { + val resetPromise = Promise[Unit]() + sh.schedule[Unit]( + () => { + promise = Promise[String]() + resetPromise.success(()) + }, + timeout, + TimeUnit.MILLISECONDS + ) + PromiseWrapper("afterEach", resetPromise) + } + override def afterAll(): PromiseWrapper = { + val shutdownPromise = Promise[Unit]() + ExecutionContext.global.execute(() => { + Thread.sleep(timeout) + val runningJobs = sh.shutdownNow() + assert(runningJobs.isEmpty(), runningJobs) + shutdownPromise.success(()) + }) + PromiseWrapper("afterAll", shutdownPromise) + } + } + val message = new ScheduledMessage() + val latest: Fixture[Unit] = new Fixture[Unit]("latest") { + def apply(): Unit = () + override def afterAll(): Unit = { + assert( + message.sh.isShutdown(), + "message.afterAll did not complete yet. " + + "We may want to remove this assertion in the future if we allow fixtures to load in parallel." + ) + } + } + + override def munitFixtures: Seq[AnyFixture[_]] = List(message, latest) + + 1.to(3).foreach { i => + test(s"test-$i") { + assertEquals(message(), s"beforeEach-test-$i") + } + } +} diff --git a/tests/jvm/src/test/scala/munit/AsyncFixtureOrderSuite.scala b/tests/jvm/src/test/scala/munit/AsyncFunFixtureOrderSuite.scala similarity index 95% rename from tests/jvm/src/test/scala/munit/AsyncFixtureOrderSuite.scala rename to tests/jvm/src/test/scala/munit/AsyncFunFixtureOrderSuite.scala index c2ca2b31..c6eda50b 100644 --- a/tests/jvm/src/test/scala/munit/AsyncFixtureOrderSuite.scala +++ b/tests/jvm/src/test/scala/munit/AsyncFunFixtureOrderSuite.scala @@ -3,7 +3,7 @@ package munit import scala.concurrent.Future import scala.concurrent.Promise -class AsyncFixtureOrderSuite extends FunSuite { +class AsyncFunFixtureOrderSuite extends FunSuite { val latch: Promise[Unit] = Promise[Unit] var completedFromTest: Option[Boolean] = None var completedFromTeardown: Option[Boolean] = None diff --git a/tests/jvm/src/test/scala/munit/TimeoutSuite.scala b/tests/jvm/src/test/scala/munit/TimeoutSuite.scala index 2186aa2f..e8f6b480 100644 --- a/tests/jvm/src/test/scala/munit/TimeoutSuite.scala +++ b/tests/jvm/src/test/scala/munit/TimeoutSuite.scala @@ -4,15 +4,41 @@ import scala.concurrent.duration.Duration import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.FiniteDuration +import scala.concurrent.ExecutionContext class TimeoutSuite extends munit.FunSuite { override val munitTimeout: FiniteDuration = Duration(100, "ms") + override def munitExecutionContext: ExecutionContext = global + test("fast-1") { + Future { + Thread.sleep(1) + } + } test("slow".fail) { Future { Thread.sleep(1000) } } - test("fast") { + test("fast-2") { + Future { + Thread.sleep(1) + } + } + test("infinite-loop".fail) { + Future { + while (true) { + def fib(n: Int): Int = { + if (n < 1) 0 + else if (n == 1) n + else fib(n - 1) + fib(n - 2) + } + // Some computationally intensive calculation + 1.to(1000).foreach(i => fib(i)) + println("Loop") + } + } + } + test("fast-3") { Future { Thread.sleep(1) } diff --git a/tests/shared/src/main/scala/munit/AsyncFixtureFrameworkSuite.scala b/tests/shared/src/main/scala/munit/AsyncFunFixtureFrameworkSuite.scala similarity index 61% rename from tests/shared/src/main/scala/munit/AsyncFixtureFrameworkSuite.scala rename to tests/shared/src/main/scala/munit/AsyncFunFixtureFrameworkSuite.scala index b8af92ca..83697faa 100644 --- a/tests/shared/src/main/scala/munit/AsyncFixtureFrameworkSuite.scala +++ b/tests/shared/src/main/scala/munit/AsyncFunFixtureFrameworkSuite.scala @@ -2,7 +2,7 @@ package munit import scala.concurrent.Future -class AsyncFixtureFrameworkSuite extends FunSuite { +class AsyncFunFixtureFrameworkSuite extends FunSuite { val failingSetup: FunFixture[Unit] = FunFixture.async[Unit]( _ => Future.failed(new Error("failure in setup")), _ => Future.successful(()) @@ -49,18 +49,18 @@ class AsyncFixtureFrameworkSuite extends FunSuite { .test("fail when even more nested mapped teardown fails") { _ => () } } -object AsyncFixtureFrameworkSuite +object AsyncFunFixtureFrameworkSuite 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 + classOf[AsyncFunFixtureFrameworkSuite], + """|==> failure munit.AsyncFunFixtureFrameworkSuite.fail when setup fails - failure in setup + |==> failure munit.AsyncFunFixtureFrameworkSuite.fail when teardown fails - failure in teardown + |==> failure munit.AsyncFunFixtureFrameworkSuite.fail when test and teardown fail - /scala/munit/AsyncFunFixtureFrameworkSuite.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 + |==> failure munit.AsyncFunFixtureFrameworkSuite.fail when mapped setup fails - failure in setup + |==> failure munit.AsyncFunFixtureFrameworkSuite.fail when even more nested mapped setup fails - failure in setup + |==> failure munit.AsyncFunFixtureFrameworkSuite.fail when mapped teardown fails - failure in teardown + |==> failure munit.AsyncFunFixtureFrameworkSuite.fail when even more nested mapped teardown fails - failure in teardown |""".stripMargin ) diff --git a/tests/shared/src/main/scala/munit/FixtureFrameworkSuite.scala b/tests/shared/src/main/scala/munit/FixtureOrderFrameworkSuite.scala similarity index 92% rename from tests/shared/src/main/scala/munit/FixtureFrameworkSuite.scala rename to tests/shared/src/main/scala/munit/FixtureOrderFrameworkSuite.scala index c6976ed5..ef1fdcd0 100644 --- a/tests/shared/src/main/scala/munit/FixtureFrameworkSuite.scala +++ b/tests/shared/src/main/scala/munit/FixtureOrderFrameworkSuite.scala @@ -1,6 +1,6 @@ package munit -class FixtureFrameworkSuite extends FunSuite { +class FixtureOrderFrameworkSuite extends FunSuite { def println(msg: String): Unit = TestingConsole.out.println(msg) private def fixture(name: String) = new Fixture[Int](name) { def apply(): Int = 1 @@ -42,10 +42,10 @@ class FixtureFrameworkSuite extends FunSuite { } } -object FixtureFrameworkSuite +object FixtureOrderFrameworkSuite extends FrameworkTest( - classOf[FixtureFrameworkSuite], - """|munit.FixtureFrameworkSuite: + classOf[FixtureOrderFrameworkSuite], + """|munit.FixtureOrderFrameworkSuite: |beforeAll(ad-hoc) |beforeAll(a) |beforeAll(b) @@ -53,29 +53,29 @@ object FixtureFrameworkSuite |beforeEach(a, 1) |beforeEach(b, 1) |test(1) + |afterEach(ad-hoc, 1) |afterEach(a, 1) |afterEach(b, 1) - |afterEach(ad-hoc, 1) | + 1 |beforeEach(ad-hoc, 2) |beforeEach(a, 2) |beforeEach(b, 2) |test(2) + |afterEach(ad-hoc, 2) |afterEach(a, 2) |afterEach(b, 2) - |afterEach(ad-hoc, 2) | + 2 |beforeEach(ad-hoc, 3) |beforeEach(a, 3) |beforeEach(b, 3) |test(3) + |afterEach(ad-hoc, 3) |afterEach(a, 3) |afterEach(b, 3) - |afterEach(ad-hoc, 3) | + 3 + |afterAll(ad-hoc) |afterAll(a) |afterAll(b) - |afterAll(ad-hoc) |""".stripMargin, format = StdoutFormat ) diff --git a/tests/shared/src/main/scala/munit/ValueTransformCrashFrameworkSuite.scala b/tests/shared/src/main/scala/munit/ValueTransformCrashFrameworkSuite.scala index 6eaf843e..8d6f89c7 100644 --- a/tests/shared/src/main/scala/munit/ValueTransformCrashFrameworkSuite.scala +++ b/tests/shared/src/main/scala/munit/ValueTransformCrashFrameworkSuite.scala @@ -2,10 +2,12 @@ package munit class ValueTransformCrashFrameworkSuite extends munit.FunSuite { override val munitValueTransforms: List[ValueTransform] = List( - new ValueTransform("boom", { case test => ??? }) + new ValueTransform("boom", { case "test-body" => ??? }) ) - test("hello") {} + test("hello") { + "test-body" + } } object ValueTransformCrashFrameworkSuite extends FrameworkTest( diff --git a/tests/shared/src/test/scala/munit/FrameworkSuite.scala b/tests/shared/src/test/scala/munit/FrameworkSuite.scala index 8ef72ad7..573af70c 100644 --- a/tests/shared/src/test/scala/munit/FrameworkSuite.scala +++ b/tests/shared/src/test/scala/munit/FrameworkSuite.scala @@ -10,7 +10,7 @@ class FrameworkSuite extends BaseFrameworkSuite { FailSuiteFrameworkSuite, TestNameFrameworkSuite, ScalaVersionFrameworkSuite, - FixtureFrameworkSuite, + FixtureOrderFrameworkSuite, TagsIncludeFramweworkSuite, TagsIncludeExcludeFramweworkSuite, TagsExcludeFramweworkSuite, @@ -21,7 +21,7 @@ class FrameworkSuite extends BaseFrameworkSuite { ValueTransformCrashFrameworkSuite, ValueTransformFrameworkSuite, ScalaCheckFrameworkSuite, - AsyncFixtureFrameworkSuite, + AsyncFunFixtureFrameworkSuite, AsyncFixtureTeardownFrameworkSuite, DuplicateNameFrameworkSuite, FullStackTraceFrameworkSuite, diff --git a/tests/shared/src/test/scala/munit/SuiteLocalFixtureSuite.scala b/tests/shared/src/test/scala/munit/SuiteLocalFixtureSuite.scala index 16c8c476..306731dc 100644 --- a/tests/shared/src/test/scala/munit/SuiteLocalFixtureSuite.scala +++ b/tests/shared/src/test/scala/munit/SuiteLocalFixtureSuite.scala @@ -8,9 +8,11 @@ class SuiteLocalFixtureSuite extends FunSuite { n } override def beforeAll(): Unit = { + assertEquals(n, 0) n = 1 } override def afterAll(): Unit = { + assertEquals(n, 17) n = -11 } } @@ -32,7 +34,7 @@ class SuiteLocalFixtureSuite extends FunSuite { } override def afterAll(): Unit = { - assertEquals(counter(), -10) + assertEquals(counter(), 17) } 1.to(5).foreach { i => diff --git a/tests/shared/src/test/scala/munit/TestLocalFixtureSuite.scala b/tests/shared/src/test/scala/munit/TestLocalFixtureSuite.scala index 7eb9cce7..88723142 100644 --- a/tests/shared/src/test/scala/munit/TestLocalFixtureSuite.scala +++ b/tests/shared/src/test/scala/munit/TestLocalFixtureSuite.scala @@ -13,7 +13,7 @@ class TestLocalFixtureSuite extends FunSuite { } val name2 = name - override def afterEach(context: GenericAfterEach[TestValue]): Unit = { + override def afterEach(context: AfterEach): Unit = { assertEquals(name(), context.test.name + "-after") } diff --git a/website/.tool-versions b/website/.tool-versions new file mode 100644 index 00000000..0288186e --- /dev/null +++ b/website/.tool-versions @@ -0,0 +1 @@ +yarn 1.22.4 diff --git a/website/blog/2020-02-01-hello-world.md b/website/blog/2020-02-01-hello-world.md index fe31ccd6..3594ebb2 100644 --- a/website/blog/2020-02-01-hello-world.md +++ b/website/blog/2020-02-01-hello-world.md @@ -65,49 +65,6 @@ class MySuite extends munit.FunSuite { Check out the [getting started guide](https://scalameta.org/munit/docs/getting-started.html). -## Tests as values - -If you know how to write normal Scala programs you should feel comfortable -reasoning about how MUnit works. - -Internally, a core MUnit data structure is `GenericTest[T]`, which represents a -single test case and is roughly defined like this. - -```scala -case class GenericTest[T]( - name: String, - body: () => T, - tags: Set[Tag], - location: Location -) -abstract class Suite { - type TestValue - type Test = GenericTest[TestValue] - def munitTests(): Seq[Test] -} -``` - -A test suite returns a `Seq[Test]`, which you as a user can generate and -abstract over any way you like. - -Importantly, MUnit test cases are not discovered via runtime reflection like in -JUnit and MUnit test cases are not generated via macros like in utest. - -MUnit provides a high-level API to write tests in a ScalaTest-inspired -`FunSuite` syntax where the type parameter for `GenericTest[T]` is defined as -`Future[Any]`. - -```scala -abstract class FunSuite extends Suite { - type TestValue = Future[Any] -} -``` - -For common usage of MUnit you are not expected to write raw -`GenericTest[T](...)` expressions but knowing this underlying data model helps -you implement features like test retries, disabling tests based on dynamic -conditions, enforce stricter type safety and more. - ## Rich filtering capabilities Using tags, MUnit provides a extensible way to disable/enable tests based on