Skip to content

Commit

Permalink
ZIO 2 (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
poslegm authored Aug 22, 2022
1 parent 7a808a6 commit dc4b83d
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 99 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "3.3.3"
version = "3.5.9"
runner.dialect = scala3
project.git=true
align.preset = most
Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ import zio.*
class SimpleZIOSpec extends ZSuite:
testZ("1 + 1 = 2") {
for
a <- ZIO(1)
b <- ZIO(1)
a <- ZIO.attempt(1)
b <- ZIO.attempt(1)
yield assertEquals(a + b, 2)
}
```
Expand Down Expand Up @@ -149,7 +149,7 @@ testZ("effect should die") {

### Resource management

Resource management in ZSuite based on `ZManaged` and MUnit fixtures.
Resource management in ZSuite based on `Scoped` and MUnit fixtures.
"Test-local" means that resource will be acquired and released on __every__
`testZ` execution. "Suite-local" means that resource will be acquired __before
all__ `testZ` executions and released __after all__ `testZ` executions.
Expand All @@ -158,7 +158,7 @@ Resources from test- and suite-local fixtures can be accessed directly from

#### Test-local fixture

You can create test-local fixture from `ZManaged` or raw acquire/release effects.
You can create test-local fixture from `Scoped` or raw acquire/release effects.

```scala
// define fixture with raw acquire/release effects
Expand All @@ -169,34 +169,34 @@ val rawZIOFunFixture = ZTestLocalFixture(options => ZIO.succeed(s"acquired ${opt

// use it with `testZ` extension method with resource access
rawZIOFunFixture.testZ("allocate resource with ZIO FunFixture") { str => // <- resource
val effect = ZIO(str.trim)
val effect = ZIO.attempt(str.trim)
assertNoDiffZ(effect, "acquired allocate resource with ZIO FunFixture")
}

// similarly for `ZManaged`
val ZManagedFunFixture = ZTestLocalFixture { options =>
ZManaged.make(ZIO.succeed(s"acquired ${options.name} with ZManaged")) { str =>
putStrLn(s"cleanup [$str] with ZManaged").provideLayer(Console.live).orDie
// similarly for `Scoped`
val ScopedFunFixture = ZTestLocalFixture { options =>
ZIO.acquireRelease(ZIO.succeed(s"acquired ${options.name} with Scoped")) { str =>
printLine(s"cleanup [$str] with Scoped").orDie
}
}

ZManagedFunFixture.testZ("allocate resource with ZManaged FunFixture") { str =>
val effect = ZIO(str.trim)
assertNoDiffZ(effect, "acquired allocate resource with ZManaged FunFixture with ZManaged")
ScopedFunFixture.testZ("allocate resource with Scoped FunFixture") { str =>
val effect = ZIO.attempt(str.trim)
assertNoDiffZ(effect, "acquired allocate resource with Scoped FunFixture with Scoped")
}
```

#### Suite-local fixture

Suite-local fixture can be created from `ZManaged` and provides synchronous
Suite-local fixture can be created from `Scoped` and provides synchronous
access to its resource.

```scala
// dirty example, don't do it in real code
var state = 0
val fixture = ZSuiteLocalFixture(
"sample",
ZManaged.make(ZIO.effectTotal { state += 1; state })(_ => ZIO.effectTotal { state += 1 })
ZIO.acquireRelease(ZIO.attempt { state += 1; state })(_ => ZIO.attempt { state += 1 }.orDie)
)

// suite-local fixture should be necessarily initialized
Expand Down Expand Up @@ -225,8 +225,8 @@ def write: RIO[Has[Service], Unit] = ZIO.service[Service].flatMap(_.write)
// ===========

val layersFixture = ZTestLocalFixture { _ =>
// wire layers in `ZManaged`'s acquire
ZManaged.make(ZIO.succeed(StatefulRepository.test >+> Service.test))(layers =>
// wire layers in `Scoped`'s acquire
ZIO.acquireRelease(ZIO.succeed(StatefulRepository.test >+> Service.test))(layers =>
// graceful release resources after test execution
clean.provideLayer(layers)
)
Expand Down
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import sbt._

object Dependencies {
private val munitVersion = "0.7.29"
private val zioVersion = "1.0.13"
private val zioVersion = "2.0.1"

lazy val munit = "org.scalameta" %% "munit" % munitVersion
lazy val zio = "dev.zio" %% "zio" % zioVersion
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.6.2
sbt.version=1.7.1
30 changes: 17 additions & 13 deletions src/main/scala/munit/ZAssertions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ trait ZAssertions {
loc: Location,
ct: ClassTag[E1]
): ZIO[R, FailExceptionLike[?], E1] =
body.run.flatMap(runIntercept(None, _, false))
body.exit.flatMap(runIntercept(None, _, false))

/** Asserts that `ZIO[R, E, Any]` should die with provided exception `E`.
* {{{
Expand All @@ -132,7 +132,7 @@ trait ZAssertions {
loc: Location,
ct: ClassTag[E1]
): ZIO[R, FailExceptionLike[?], E1] =
body.run.flatMap(runIntercept(None, _, true))
body.exit.flatMap(runIntercept(None, _, true))

/** Asserts that `ZIO[R, E, Any]` should fail with provided exception `E` and message `message`.
* {{{
Expand All @@ -148,7 +148,7 @@ trait ZAssertions {
def interceptFailureMessage[E1 <: E](
message: String
)(implicit loc: Location, ct: ClassTag[E1]): ZIO[R, FailExceptionLike[?], E1] =
body.run.flatMap(runIntercept(Some(message), _, false))
body.exit.flatMap(runIntercept(Some(message), _, false))
}

private def runIntercept[E <: Throwable](
Expand All @@ -158,11 +158,13 @@ trait ZAssertions {
)(implicit loc: Location, E: ClassTag[E]): IO[FailExceptionLike[?], E] =
exit match {
case Exit.Success(_) =>
ZIO(
fail(
s"expected exception of type '${E.runtimeClass.getName()}' but body evaluated successfully"
)
).refineToOrDie[FailException]
ZIO
.attempt {
fail(
s"expected exception of type '${E.runtimeClass.getName()}' but body evaluated successfully"
)
}
.refineToOrDie[FailException]
case Exit.Failure(cause) =>
val e = if (shouldDie) cause.dieOption else cause.failureOption
e match {
Expand Down Expand Up @@ -197,11 +199,13 @@ trait ZAssertions {
}

case None =>
ZIO(
fail(
s"expected exception of type '${E.runtimeClass.getName()}' but body evaluated successfully"
)
).refineToOrDie[FailException]
ZIO
.attempt {
fail(
s"expected exception of type '${E.runtimeClass.getName()}' but body evaluated successfully"
)
}
.refineToOrDie[FailException]
}
}
}
61 changes: 31 additions & 30 deletions src/main/scala/munit/ZFixtures.scala
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
package munit

import zio.{IO, Managed, UIO, Exit, ZIO}
import zio.*

trait ZFixtures {
self: ZSuite =>

/** Test-local fixture.
*
* Can be created from raw setup/teardown effects or from ZManaged.
* Can be created from raw setup/teardown effects or from Scoped effect.
*
* {{{
* val rawZIOFunFixture = ZTestLocalFixture(options => ZIO.succeed(s"acquired \${options.name}")) { str =>
* putStrLn(s"cleanup [\$str]").provideLayer(Console.live)
* }
*
* val ZManagedFunFixture = ZTestLocalFixture { options =>
* ZManaged.make(ZIO.succeed(s"acquired \${options.name} with ZManaged")) { str =>
* putStrLn(s"cleanup [\$str] with ZManaged").provideLayer(Console.live).orDie
* val scopedFunFixture = ZTestLocalFixture { options =>
* ZIO.acquireRelease(ZIO.succeed(s"acquired \${options.name} with Scoped")) { str =>
* printLine(s"cleanup [\$str] with Scoped").orDie
* }
* }
*
* rawZIOFunFixture.test("allocate resource with ZIO FunFixture") { str =>
* assertNoDiff(str, "acquired allocate resource with ZIO FunFixture")
* }
*
* ZManagedFunFixture.test("allocate resource with ZManaged FunFixture") { str =>
* assertNoDiff(str, "acquired allocate resource with ZManaged FunFixture with ZManaged")
* scopedFunFixture.test("allocate resource with Scoped FunFixture") { str =>
* assertNoDiff(str, "acquired allocate resource with Scoped FunFixture with Scoped")
* }
* }}}
*/
Expand All @@ -36,33 +36,30 @@ trait ZFixtures {
t => unsafeRunToFuture(teardown(t))
)

def apply[E, A](create: TestOptions => Managed[E, A]): FunFixture[A] = {
var release: Exit[Any, Any] => UIO[Any] = null
def apply[E, A](create: TestOptions => ZIO[Scope, E, A]): FunFixture[A] = {
val scope = Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(Scope.make).getOrThrow()
}
FunFixture.async(
setup = { options =>
val effect = for {
res <- create(options).reserve
_ <- ZIO.effectTotal { release = res.release }
resource <- res.acquire
} yield resource
unsafeRunToFuture(effect)
unsafeRunToFuture(create(options).provideLayer(ZLayer.succeed(scope)))
},
teardown = { resource =>
val effect = release(Exit.succeed(resource)).unit
val effect = scope.close(Exit.succeed(resource)).unit
unsafeRunToFuture(effect)
}
)
}
}

/** Suite local fixture from ZManaged.
/** Suite local fixture from Scoped effect.
*
* {{{
*
* var state = 0
* val fixture = ZSuiteLocalFixture(
* "sample",
* ZManaged.make(ZIO.effectTotal { state += 1; state })(_ => ZIO.effectTotal { state -= 1 })
* ZIO.acquireRelease(ZIO.attempt { state += 1; state })(_ => ZIO.attempt { state -= 1 }.orDie)
* )
*
* override val munitFixtures = Seq(fixture)
Expand All @@ -77,26 +74,30 @@ trait ZFixtures {
extends Exception(
s"The fixture `$name` was not instantiated. Override `munitFixtures` and include a reference to this fixture."
)
private case class Resource[T](content: T, release: Exit[Any, Any] => UIO[Any])

def apply[E, A](name: String, managed: Managed[E, A]): Fixture[A] = {
var resource: Resource[A] = null
def apply[E, A](name: String, managed: ZIO[Scope, E, A]): Fixture[A] = {
var resource: Option[A] = None

val scope = Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(Scope.make).getOrThrow()
}
new Fixture[A](name) {
def apply(): A =
if (resource == null) throw new FixtureNotInstantiatedException(name)
else resource.content
resource.getOrElse(throw new FixtureNotInstantiatedException(name))

override def beforeAll(): Unit = {
val effect = for {
res <- managed.reserve
content <- res.acquire
_ <- ZIO.effectTotal { resource = Resource(content, res.release) }
} yield ()
runtime.unsafeRun(effect)
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(
managed.map { r => resource = Some(r) }.provideLayer(ZLayer.succeed(scope))
)
}
()
}

override def afterAll(): Unit = {
runtime.unsafeRun(resource.release(Exit.succeed(resource.content)))
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(scope.close(Exit.succeed(resource)))
}
()
}
}
Expand Down
26 changes: 15 additions & 11 deletions src/main/scala/munit/ZRuntime.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package munit

import zio.{Runtime, IO, Exit}
import zio.{Runtime, IO, Exit, Unsafe}

import scala.concurrent.{Promise, Future}

trait ZRuntime {
protected val runtime: Runtime[Any] = Runtime.global.withReportFailure { cause =>
cause.dieOption.foreach {
// suppress munit reports duplication
case _: FailExceptionLike[?] =>
case _ => System.err.println(cause.prettyPrint)
protected val runtime: Runtime[Any] =
Unsafe.unsafe { implicit unsafe =>
Runtime.unsafe.fromLayer(Runtime.setReportFatal {
// suppress munit reports duplication
case cause: FailExceptionLike[?] => throw cause
case cause =>
cause.printStackTrace
throw cause
})
}
}

/** Because original unsafeRunToFuture adds useless causes to `FailExceptionLike` and duplicates
* errors on every test failure. See `cause.squashTraceWith(identity)`
Expand All @@ -24,11 +27,12 @@ trait ZRuntime {
case t: Throwable => t
case other => new ZIOError(other)
}
runtime.unsafeRunAsync(task) {
case Exit.Success(res) => promise.success(res)
case Exit.Failure(cause) => promise.failure(cause.squash)
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.fork(task).unsafe.addObserver {
case Exit.Success(res) => promise.success(res)
case Exit.Failure(cause) => promise.failure(cause.squash)
}
}

promise.future
}

Expand Down
12 changes: 6 additions & 6 deletions src/test/scala/munit/ZAssertionsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ class ZAssertionsSpec extends ZSuite {
}

testZ("assertEqualsZ works (successful assertion)") {
val zio = ZIO(41).map(_ + 1)
val zio = ZIO.attempt(41).map(_ + 1)
assertEqualsZ(zio, 42)
}

testZ("assertEqualsZ works (failed assertion)".fail) {
val zio = ZIO(41).map(_ + 2)
val zio = ZIO.attempt(41).map(_ + 2)
assertEqualsZ(zio, 42)
}

testZ("assertNotEqualsZ works (successful assertion)") {
val zio = ZIO(41).map(_ + 2)
val zio = ZIO.attempt(41).map(_ + 2)
assertNotEqualsZ(zio, 42)
}

testZ("assertNotEqualsZ works (failed assertion)".fail) {
val zio = ZIO(41).map(_ + 1)
val zio = ZIO.attempt(41).map(_ + 1)
assertNotEqualsZ(zio, 42)
}

Expand Down Expand Up @@ -65,7 +65,7 @@ class ZAssertionsSpec extends ZSuite {
}

testZ("interceptFailure works (failed assertion: effect does not fail)".fail) {
val zio = ZIO(42)
val zio = ZIO.attempt(42)
zio.interceptFailure[IllegalArgumentException]
}

Expand Down Expand Up @@ -95,7 +95,7 @@ class ZAssertionsSpec extends ZSuite {
}

testZ("interceptFailureMessage works (failed assertion: ZIO does not fail)".fail) {
val zio = ZIO(42)
val zio = ZIO.attempt(42)
zio.interceptFailureMessage[IllegalArgumentException]("BOOM!")
}
}
Loading

0 comments on commit dc4b83d

Please sign in to comment.