diff --git a/.circleci/config.yml b/.circleci/config.yml index 92cf46268ca8..11b9e0de9127 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,6 +55,14 @@ load_cache: &load_cache - restore_cache: key: sbt-cache-v2 +clean_cache: &clean_cache + - run: + name: Clean unwanted files from cache + command: | + rm -fv $HOME/.ivy2/.sbt.ivy.lock + find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete + find $HOME/.sbt -name "*.lock" -print -delete + save_cache: &save_cache - save_cache: key: sbt-cache-v2 @@ -92,6 +100,7 @@ compile: &compile - run: name: Compile code command: ./sbt ++${SCALA_VERSION}! compileJVM + - <<: *clean_cache - <<: *save_cache lint: &lint @@ -101,6 +110,7 @@ lint: &lint - run: name: Lint code command: ./sbt ++${SCALA_VERSION}! check + - <<: *clean_cache - <<: *save_cache mdoc: &mdoc @@ -112,6 +122,7 @@ mdoc: &mdoc command: | ./sbt coreJVM/doc coreJS/doc streamsJVM/doc streamsJS/doc testJVM/doc testJS/doc ./sbt ++${SCALA_VERSION}! mdoc + - <<: *clean_cache - <<: *save_cache testJVM: &testJVM @@ -122,7 +133,10 @@ testJVM: &testJVM - run: name: Run tests command: ./sbt ++${SCALA_VERSION}! testJVM + - <<: *clean_cache - <<: *save_cache + - store_test_results: + path: core-tests/jvm/target/test-reports testJS: &testJS steps: @@ -133,7 +147,10 @@ testJS: &testJS - run: name: Run tests command: ./sbt ++${SCALA_VERSION}! testJS + - <<: *clean_cache - <<: *save_cache + - store_test_results: + path: core-tests/js/target/test-reports release: &release steps: @@ -180,6 +197,7 @@ microsite: µsite node -v ./sbt docs/docusaurusCreateSite ./sbt docs/docusaurusPublishGhpages + - <<: *clean_cache - <<: *save_cache jobs: diff --git a/core-tests/shared/src/test/scala/zio/ArbitraryCause.scala b/core-tests/shared/src/test/scala/zio/ArbitraryCause.scala deleted file mode 100644 index aed850aa9e37..000000000000 --- a/core-tests/shared/src/test/scala/zio/ArbitraryCause.scala +++ /dev/null @@ -1,30 +0,0 @@ -package zio - -import org.scalacheck.{ Arbitrary, Gen } -import zio.Cause.Traced - -object ArbitraryCause { - implicit def arbCause[T](implicit arbT: Arbitrary[T]): Arbitrary[Cause[T]] = - Arbitrary { - Gen.oneOf( - Gen.const(Cause.interrupt), - Arbitrary.arbitrary[String].map(s => Cause.die(new RuntimeException(s))), - arbT.arbitrary.map(Cause.fail), - Gen.lzy { - arbCause[T].arbitrary.map(Traced(_, ZTrace(0, Nil, Nil, None))) - }, - Gen.lzy { - for { - left <- arbCause[T].arbitrary - right <- arbCause[T].arbitrary - } yield Cause.Then(left, right) - }, - Gen.lzy { - for { - left <- arbCause[T].arbitrary - right <- arbCause[T].arbitrary - } yield Cause.Both(left, right) - } - ) - } -} diff --git a/core-tests/shared/src/test/scala/zio/CauseSpec.scala b/core-tests/shared/src/test/scala/zio/CauseSpec.scala index 57f501ec47d6..3cb8a2c5544a 100644 --- a/core-tests/shared/src/test/scala/zio/CauseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/CauseSpec.scala @@ -1,59 +1,127 @@ package zio -import org.specs2.{ ScalaCheck, Specification } - -class CauseSpec extends Specification with ScalaCheck { - import Cause._ - import ArbitraryCause._ - - def is = "CauseSpec".title ^ s2""" - Cause - `Cause#died` and `Cause#stripFailures` are consistent $e1 - `Cause.equals` is symmetric $e2 - `Cause.equals` and `Cause.hashCode` satisfy the contract $e3 - `Cause#untraced` removes all traces $e8 - Then - `Then.equals` satisfies associativity $e4 - `Then.equals` satisfies distributivity $e5 - Both - `Both.equals` satisfies associativity $e6 - `Both.equals` satisfies commutativity $e7 - """ - - private def e1 = prop { c: Cause[String] => - if (c.died) c.stripFailures must beSome - else c.stripFailures must beNone - } - - private def e2 = prop { (a: Cause[String], b: Cause[String]) => - (a == b) must_== (b == a) - } - - private def e3 = - prop { (a: Cause[String], b: Cause[String]) => - (a == b) ==> (a.hashCode must_== (b.hashCode)) - }.set(minTestsOk = 10, maxDiscardRatio = 99.0f) - - private def e4 = prop { (a: Cause[String], b: Cause[String], c: Cause[String]) => - Then(Then(a, b), c) must_== Then(a, Then(b, c)) - Then(a, Then(b, c)) must_== Then(Then(a, b), c) - } - - private def e5 = prop { (a: Cause[String], b: Cause[String], c: Cause[String]) => - Then(a, Both(b, c)) must_== Both(Then(a, b), Then(a, c)) - Then(Both(a, b), c) must_== Both(Then(a, c), Then(b, c)) - } - - private def e6 = prop { (a: Cause[String], b: Cause[String], c: Cause[String]) => - Both(Both(a, b), c) must_== Both(a, Both(b, c)) - Both(Both(a, b), c) must_== Both(a, Both(b, c)) - } - - private def e7 = prop { (a: Cause[String], b: Cause[String]) => - Both(a, b) must_== Both(b, a) - } - - private def e8 = prop { (c: Cause[String]) => - c.untraced.traces.headOption must beNone - } +import zio.Cause.{ Both, Then } +import zio.random.Random +import zio.test._ +import zio.test.Assertion._ + +import zio.CauseSpecUtil._ + +object CauseSpec + extends ZIOBaseSpec( + suite("CauseSpec")( + suite("Cause")( + testM("`Cause#died` and `Cause#stripFailures` are consistent") { + check(causes) { c => + assert(c.stripFailures, if (c.died) isSome(anything) else isNone) + } + }, + testM("`Cause.equals` is symmetric") { + check(causes, causes) { (a, b) => + assert(a == b, equalTo(b == a)) + } + }, + testM("`Cause.equals` and `Cause.hashCode` satisfy the contract") { + check(equalCauses) { + case (a, b) => + assert(a.hashCode, equalTo(b.hashCode)) + } + }, + testM("`Cause#untraced` removes all traces") { + check(causes) { c => + assert(c.untraced.traces.headOption, isNone) + } + } + ), + suite("Then")( + testM("`Then.equals` satisfies associativity") { + check(causes, causes, causes) { (a, b, c) => + assert(Then(Then(a, b), c), equalTo(Then(a, Then(b, c)))) && + assert(Then(a, Then(b, c)), equalTo(Then(Then(a, b), c))) + } + }, + testM("`Then.equals` satisfies distributivity") { + check(causes, causes, causes) { (a, b, c) => + assert(Then(a, Both(b, c)), equalTo(Both(Then(a, b), Then(a, c)))) && + assert(Then(Both(a, b), c), equalTo(Both(Then(a, c), Then(b, c)))) + } + } + ), + suite("Both")( + testM("`Both.equals` satisfies associativity") { + check(causes, causes, causes) { (a, b, c) => + assert(Both(Both(a, b), c), equalTo(Both(a, Both(b, c)))) && + assert(Both(a, Both(b, c)), equalTo(Both(Both(a, b), c))) + } + }, + testM("`Both.equals` satisfies distributivity") { + check(causes, causes, causes) { (a, b, c) => + assert(Both(Then(a, b), Then(a, c)), equalTo(Then(a, Both(b, c)))) && + assert(Both(Then(a, c), Then(b, c)), equalTo(Then(Both(a, b), c))) + } + }, + testM("`Both.equals` satisfies commutativity") { + check(causes, causes) { (a, b) => + assert(Both(a, b), equalTo(Both(b, a))) + } + } + ), + suite("Meta")( + testM("`Meta` is excluded from equals") { + check(causes) { c => + assert(Cause.stackless(c), equalTo(c)) && + assert(c, equalTo(Cause.stackless(c))) + } + }, + testM("`Meta` is excluded from hashCode") { + check(causes) { c => + assert(Cause.stackless(c).hashCode, equalTo(c.hashCode)) + } + } + ), + suite("Monad Laws:")( + testM("Left identity") { + check(causes) { c => + assert(c.flatMap(Cause.fail), equalTo(c)) + } + }, + testM("Right identity") { + check(errors, errorCauseFunctions) { (e, f) => + assert(Cause.fail(e).flatMap(f), equalTo(f(e))) + } + }, + testM("Associativity") { + check(causes, errorCauseFunctions, errorCauseFunctions) { (c, f, g) => + assert(c.flatMap(f).flatMap(g), equalTo(c.flatMap(e => f(e).flatMap(g)))) + } + } + ) + ) + ) + +object CauseSpecUtil { + + val causes: Gen[Random with Sized, Cause[String]] = + Gen.causes(Gen.anyString, Gen.anyString.map(s => new RuntimeException(s))) + + val equalCauses: Gen[Random with Sized, (Cause[String], Cause[String])] = + (causes <*> causes <*> causes).flatMap { + case ((a, b), c) => + Gen.elements( + (a, a), + (a, Cause.traced(a, ZTrace(0, Nil, Nil, None))), + (Then(Then(a, b), c), Then(a, Then(b, c))), + (Then(a, Both(b, c)), Both(Then(a, b), Then(a, c))), + (Both(Both(a, b), c), Both(a, Both(b, c))), + (Both(Then(a, c), Then(b, c)), Then(Both(a, b), c)), + (Both(a, b), Both(b, a)), + (a, Cause.stackless(a)) + ) + } + + val errorCauseFunctions: Gen[Random with Sized, String => Cause[String]] = + Gen.function(causes) + + val errors: Gen[Random with Sized, String] = + Gen.anyString } diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala index d0a7e0f8883a..4fdc67776ac6 100644 --- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala @@ -5,9 +5,9 @@ import zio.ZIOSpecHelper._ import zio.clock.Clock import zio.duration._ import zio.test._ -import zio.test.mock.live +import zio.test.mock._ import zio.test.Assertion._ -import zio.test.TestAspect.{ flaky, jvm, nonFlaky } +import zio.test.TestAspect.{ flaky, ignore, jvm, nonFlaky } import scala.annotation.tailrec import scala.util.{ Failure, Success } @@ -641,7 +641,7 @@ object ZIOSpec } yield ()).interruptChildren r <- pa.await zip pb.await } yield assert(r, equalTo((1, 2))) - } @@ flaky, + } @@ ignore, testM("supervise fibers in race") { for { pa <- Promise.make[Nothing, Int] @@ -1152,6 +1152,31 @@ object ZIOSpec assertM(io, isTrue) } @@ flaky + ), + suite("timeoutFork")( + testM("returns `Right` with the produced value if the effect completes before the timeout elapses") { + assertM(ZIO.unit.timeoutFork(100.millis), isRight(isUnit)) + }, + testM("returns `Left` with the interrupting fiber otherwise") { + for { + fiber <- ZIO.never.uninterruptible.timeoutFork(100.millis).fork + _ <- MockClock.adjust(100.millis) + result <- fiber.join + } yield assert(result, isLeft(anything)) + } + ), + suite("unsandbox")( + testM("no information is lost during composition") { + val causes = Gen.causes(Gen.anyString, Gen.throwable) + def cause[R, E](zio: ZIO[R, E, Nothing]): ZIO[R, Nothing, Cause[E]] = + zio.foldCauseM(ZIO.succeed, ZIO.fail) + checkM(causes) { c => + for { + result <- cause(ZIO.halt(c).sandbox.mapErrorCause(e => e.untraced).unsandbox) + } yield assert(result, equalTo(c)) && + assert(result.prettyPrint, equalTo(c.prettyPrint)) + } + } ) ) ) diff --git a/core-tests/shared/src/test/scala/zio/ZQueueSpec.scala b/core-tests/shared/src/test/scala/zio/ZQueueSpec.scala index 6f723436679d..cd97ef6c1355 100644 --- a/core-tests/shared/src/test/scala/zio/ZQueueSpec.scala +++ b/core-tests/shared/src/test/scala/zio/ZQueueSpec.scala @@ -5,7 +5,7 @@ import zio.clock.Clock import zio.duration._ import zio.test._ import zio.test.Assertion._ -import zio.test.TestAspect.nonFlaky +import zio.test.TestAspect.{ jvm, nonFlaky } import zio.ZQueueSpecUtil.waitForSize object ZQueueSpec @@ -716,7 +716,7 @@ object ZQueueSpec _ <- q.shutdown _ <- f.await } yield assert(true, isTrue) - } @@ nonFlaky(100), + } @@ jvm(nonFlaky(100)), testM("shutdown race condition with take") { for { q <- Queue.bounded[Int](2) @@ -726,7 +726,7 @@ object ZQueueSpec _ <- q.shutdown _ <- f.await } yield assert(true, isTrue) - } @@ nonFlaky(100) + } @@ jvm(nonFlaky(100)) ) ) diff --git a/core/shared/src/main/scala/zio/Cause.scala b/core/shared/src/main/scala/zio/Cause.scala index c0feae3a0356..76e0e67d1f51 100644 --- a/core/shared/src/main/scala/zio/Cause.scala +++ b/core/shared/src/main/scala/zio/Cause.scala @@ -25,7 +25,7 @@ sealed trait Cause[+E] extends Product with Serializable { self => final def defects: List[Throwable] = self - .fold(List.empty[Throwable]) { + .foldLeft(List.empty[Throwable]) { case (z, Die(v)) => v :: z } .reverse @@ -36,15 +36,28 @@ sealed trait Cause[+E] extends Product with Serializable { self => case Then(left, right) => left.died || right.died case Both(left, right) => left.died || right.died case Traced(cause, _) => cause.died + case Meta(cause, _) => cause.died case _ => false } + /** + * Returns the `Throwable` associated with the first `Die` in this `Cause` if + * one exists. + */ + final def dieOption: Option[Throwable] = + fold(failCase = _ => None, dieCase = t => Some(t), interruptCase = None)( + thenCase = _ orElse _, + bothCase = _ orElse _, + tracedCase = (z, _) => z + ) + final def failed: Boolean = self match { case Fail(_) => true case Then(left, right) => left.failed || right.failed case Both(left, right) => left.failed || right.failed case Traced(cause, _) => cause.failed + case Meta(cause, _) => cause.failed case _ => false } @@ -60,18 +73,53 @@ sealed trait Cause[+E] extends Product with Serializable { self => final def failures[E1 >: E]: List[E1] = self - .fold(List.empty[E1]) { + .foldLeft(List.empty[E1]) { case (z, Fail(v)) => v :: z } .reverse - final def fold[Z](z: Z)(f: PartialFunction[(Z, Cause[E]), Z]): Z = - (f.lift(z -> self).getOrElse(z), self) match { - case (z, Then(left, right)) => right.fold(left.fold(z)(f))(f) - case (z, Both(left, right)) => right.fold(left.fold(z)(f))(f) - case (z, Traced(cause, _)) => cause.fold(z)(f) + final def flatMap[E1](f: E => Cause[E1]): Cause[E1] = self match { + case Fail(value) => f(value) + case c @ Die(_) => c + case Interrupt => Interrupt + case Then(left, right) => Then(left.flatMap(f), right.flatMap(f)) + case Both(left, right) => Both(left.flatMap(f), right.flatMap(f)) + case Traced(cause, trace) => Traced(cause.flatMap(f), trace) + case Meta(cause, data) => Meta(cause.flatMap(f), data) + } - case (z, _) => z + final def flatten[E1](implicit ev: E <:< Cause[E1]): Cause[E1] = + flatMap(e => e) + + final def fold[Z]( + failCase: E => Z, + dieCase: Throwable => Z, + interruptCase: => Z + )(thenCase: (Z, Z) => Z, bothCase: (Z, Z) => Z, tracedCase: (Z, ZTrace) => Z): Z = + self match { + case Fail(value) => + failCase(value) + case Die(value) => + dieCase(value) + case Interrupt => + interruptCase + case Then(left, right) => + thenCase( + left.fold(failCase, dieCase, interruptCase)(thenCase, bothCase, tracedCase), + right.fold(failCase, dieCase, interruptCase)(thenCase, bothCase, tracedCase) + ) + case Both(left, right) => + bothCase( + left.fold(failCase, dieCase, interruptCase)(thenCase, bothCase, tracedCase), + right.fold(failCase, dieCase, interruptCase)(thenCase, bothCase, tracedCase) + ) + case Traced(cause, trace) => + tracedCase( + cause.fold(failCase, dieCase, interruptCase)(thenCase, bothCase, tracedCase), + trace + ) + case Meta(cause, _) => + cause.fold(failCase, dieCase, interruptCase)(thenCase, bothCase, tracedCase) } final def interrupted: Boolean = @@ -80,22 +128,17 @@ sealed trait Cause[+E] extends Product with Serializable { self => case Then(left, right) => left.interrupted || right.interrupted case Both(left, right) => left.interrupted || right.interrupted case Traced(cause, _) => cause.interrupted + case Meta(cause, _) => cause.interrupted case _ => false } - final def map[E1](f: E => E1): Cause[E1] = self match { - case Fail(value) => Fail(f(value)) - case c @ Die(_) => c - case Interrupt => Interrupt - - case Then(left, right) => Then(left.map(f), right.map(f)) - case Both(left, right) => Both(left.map(f), right.map(f)) - case Traced(cause, trace) => Traced(cause.map(f), trace) - } + final def map[E1](f: E => E1): Cause[E1] = + flatMap(f andThen fail) final def untraced: Cause[E] = self match { - case Traced(cause, _) => cause.untraced + case Traced(cause, _) => cause.untraced + case Meta(cause, data) => Meta(cause.untraced, data) case c @ Fail(_) => c case c @ Die(_) => c @@ -120,30 +163,32 @@ sealed trait Cause[+E] extends Product with Serializable { self => (p1 + head) :: tail.map(p2 + _) } - def parallelSegments(cause: Cause[Any]): List[Sequential] = + def parallelSegments(cause: Cause[Any], maybeData: Option[Data]): List[Sequential] = cause match { - case Cause.Both(left, right) => parallelSegments(left) ++ parallelSegments(right) - case _ => List(causeToSequential(cause)) + case Cause.Both(left, right) => parallelSegments(left, maybeData) ++ parallelSegments(right, maybeData) + case _ => List(causeToSequential(cause, maybeData)) } - def linearSegments(cause: Cause[Any]): List[Step] = + def linearSegments(cause: Cause[Any], maybeData: Option[Data]): List[Step] = cause match { - case Cause.Then(first, second) => linearSegments(first) ++ linearSegments(second) - case _ => causeToSequential(cause).all + case Cause.Then(first, second) => linearSegments(first, maybeData) ++ linearSegments(second, maybeData) + case _ => causeToSequential(cause, maybeData).all } // Inline definition of `StringOps.lines` to avoid calling either of `.linesIterator` or `.lines` // since both are deprecated in either 2.11 or 2.13 respectively. def lines(str: String): List[String] = augmentString(str).linesWithSeparators.map(_.stripLineEnd).toList - def renderThrowable(e: Throwable): List[String] = { - import java.io.{ PrintWriter, StringWriter } - - val sw = new StringWriter() - val pw = new PrintWriter(sw) - - e.printStackTrace(pw) - lines(sw.toString) + def renderThrowable(e: Throwable, maybeData: Option[Data]): List[String] = { + val stackless = maybeData.fold(false)(_.stackless) + if (stackless) List(e.toString) + else { + import java.io.{ PrintWriter, StringWriter } + val sw = new StringWriter() + val pw = new PrintWriter(sw) + e.printStackTrace(pw) + lines(sw.toString) + } } def renderTrace(maybeTrace: Option[ZTrace]): List[String] = @@ -156,12 +201,12 @@ sealed trait Cause[+E] extends Product with Serializable { self => List(Failure("A checked error was not handled." :: error ++ renderTrace(maybeTrace))) ) - def renderFailThrowable(t: Throwable, maybeTrace: Option[ZTrace]): Sequential = - renderFail(renderThrowable(t), maybeTrace) + def renderFailThrowable(t: Throwable, maybeTrace: Option[ZTrace], maybeData: Option[Data]): Sequential = + renderFail(renderThrowable(t, maybeData), maybeTrace) - def renderDie(t: Throwable, maybeTrace: Option[ZTrace]): Sequential = + def renderDie(t: Throwable, maybeTrace: Option[ZTrace], maybeData: Option[Data]): Sequential = Sequential( - List(Failure("An unchecked error was produced." :: renderThrowable(t) ++ renderTrace(maybeTrace))) + List(Failure("An unchecked error was produced." :: renderThrowable(t, maybeData) ++ renderTrace(maybeTrace))) ) def renderInterrupt(maybeTrace: Option[ZTrace]): Sequential = @@ -169,34 +214,37 @@ sealed trait Cause[+E] extends Product with Serializable { self => List(Failure("An unchecked error was produced." :: renderTrace(maybeTrace))) ) - def causeToSequential(cause: Cause[Any]): Sequential = + def causeToSequential(cause: Cause[Any], maybeData: Option[Data]): Sequential = cause match { case Cause.Fail(t: Throwable) => - renderFailThrowable(t, None) + renderFailThrowable(t, None, maybeData) case Cause.Fail(error) => renderFail(lines(error.toString), None) case Cause.Die(t) => - renderDie(t, None) + renderDie(t, None, maybeData) case Cause.Interrupt => renderInterrupt(None) - case t: Cause.Then[Any] => Sequential(linearSegments(t)) - case b: Cause.Both[Any] => Sequential(List(Parallel(parallelSegments(b)))) + case t: Cause.Then[Any] => Sequential(linearSegments(t, maybeData)) + case b: Cause.Both[Any] => Sequential(List(Parallel(parallelSegments(b, maybeData)))) case Traced(c, trace) => c match { case Cause.Fail(t: Throwable) => - renderFailThrowable(t, Some(trace)) + renderFailThrowable(t, Some(trace), maybeData) case Cause.Fail(error) => renderFail(lines(error.toString), Some(trace)) case Cause.Die(t) => - renderDie(t, Some(trace)) + renderDie(t, Some(trace), maybeData) case Cause.Interrupt => renderInterrupt(Some(trace)) case _ => Sequential( - Failure("An error was rethrown with a new trace." :: renderTrace(Some(trace))) :: causeToSequential(c).all + Failure("An error was rethrown with a new trace." :: renderTrace(Some(trace))) :: + causeToSequential(c, maybeData).all ) } + case Meta(cause, data) => + causeToSequential(cause, Some(data)) } @@ -218,7 +266,7 @@ sealed trait Cause[+E] extends Product with Serializable { self => } ++ List("▼") } - val sequence = causeToSequential(this) + val sequence = causeToSequential(this, None) ("Fiber failed." :: { sequence match { @@ -274,66 +322,137 @@ sealed trait Cause[+E] extends Product with Serializable { self => } case Traced(c, trace) => c.stripFailures.map(Traced(_, trace)) + case Meta(c, data) => c.stripFailures.map(Meta(_, data)) } final def succeeded: Boolean = !failed final def traces: List[ZTrace] = self - .fold(List.empty[ZTrace]) { + .foldLeft(List.empty[ZTrace]) { case (z, Traced(_, trace)) => trace :: z } .reverse + private def foldLeft[Z](z: Z)(f: PartialFunction[(Z, Cause[E]), Z]): Z = + (f.lift(z -> self).getOrElse(z), self) match { + case (z, Then(left, right)) => right.foldLeft(left.foldLeft(z)(f))(f) + case (z, Both(left, right)) => right.foldLeft(left.foldLeft(z)(f))(f) + case (z, Traced(cause, _)) => cause.foldLeft(z)(f) + case (z, Meta(cause, _)) => cause.foldLeft(z)(f) + + case (z, _) => z + } } object Cause extends Serializable { - final def die(defect: Throwable): Cause[Nothing] = Die(defect) - final def fail[E](error: E): Cause[E] = Fail(error) - final val interrupt: Cause[Nothing] = Interrupt - final def traced[E](cause: Cause[E], trace: ZTrace): Traced[E] = Traced(cause, trace) + final def die(defect: Throwable): Cause[Nothing] = Die(defect) + final def fail[E](error: E): Cause[E] = Fail(error) + final val interrupt: Cause[Nothing] = Interrupt + final def stack[E](cause: Cause[E]): Cause[E] = Meta(cause, Data(false)) + final def stackless[E](cause: Cause[E]): Cause[E] = Meta(cause, Data(true)) + final def traced[E](cause: Cause[E], trace: ZTrace): Cause[E] = Traced(cause, trace) + + /** + * Converts the specified `Cause[Option[E]]` to an `Option[Cause[E]]` by + * recursively stripping out any failures with the error `None`. + */ + final def sequenceCauseOption[E](c: Cause[Option[E]]): Option[Cause[E]] = + c match { + case Cause.Traced(cause, trace) => sequenceCauseOption(cause).map(Cause.Traced(_, trace)) + case Cause.Meta(cause, data) => sequenceCauseOption(cause).map(Cause.Meta(_, data)) + case Cause.Interrupt => Some(Cause.Interrupt) + case d @ Cause.Die(_) => Some(d) + case Cause.Fail(Some(e)) => Some(Cause.Fail(e)) + case Cause.Fail(None) => None + case Cause.Then(left, right) => + (sequenceCauseOption(left), sequenceCauseOption(right)) match { + case (Some(cl), Some(cr)) => Some(Cause.Then(cl, cr)) + case (None, Some(cr)) => Some(cr) + case (Some(cl), None) => Some(cl) + case (None, None) => None + } + + case Cause.Both(left, right) => + (sequenceCauseOption(left), sequenceCauseOption(right)) match { + case (Some(cl), Some(cr)) => Some(Cause.Both(cl, cr)) + case (None, Some(cr)) => Some(cr) + case (Some(cl), None) => Some(cl) + case (None, None) => None + } + } - final case class Fail[E](value: E) extends Cause[E] { + private final case class Fail[E](value: E) extends Cause[E] { override final def equals(that: Any): Boolean = that match { case fail: Fail[_] => value == fail.value case traced: Traced[_] => this == traced.cause + case meta: Meta[_] => this == meta.cause case _ => false } } - final case class Die(value: Throwable) extends Cause[Nothing] { + object Fail { + def apply[E](value: E): Cause[E] = + new Fail(value) + } + + private final case class Die(value: Throwable) extends Cause[Nothing] { override final def equals(that: Any): Boolean = that match { case die: Die => value == die.value case traced: Traced[_] => this == traced.cause + case meta: Meta[_] => this == meta.cause case _ => false } } + object Die { + final def apply(value: Throwable): Cause[Nothing] = + new Die(value) + } + case object Interrupt extends Cause[Nothing] { override final def equals(that: Any): Boolean = (this eq that.asInstanceOf[AnyRef]) || (that match { case traced: Traced[_] => this == traced.cause + case meta: Meta[_] => this == meta.cause case _ => false }) } // Traced is excluded completely from equals & hashCode - final case class Traced[E](cause: Cause[E], trace: ZTrace) extends Cause[E] { + private final case class Traced[E](cause: Cause[E], trace: ZTrace) extends Cause[E] { override final def hashCode: Int = cause.hashCode() override final def equals(obj: Any): Boolean = obj match { case traced: Traced[_] => cause == traced.cause + case meta: Meta[_] => cause == meta.cause + case _ => cause == obj + } + } + + object Traced { + def apply[E](cause: Cause[E], trace: ZTrace): Cause[E] = + new Traced(cause, trace) + } + + // Meta is excluded completely from equals & hashCode + private final case class Meta[E](cause: Cause[E], data: Data) extends Cause[E] { + override final def hashCode: Int = cause.hashCode + override final def equals(obj: Any): Boolean = obj match { + case traced: Traced[_] => cause == traced.cause + case meta: Meta[_] => cause == meta.cause case _ => cause == obj } } - final case class Then[E](left: Cause[E], right: Cause[E]) extends Cause[E] { self => + private final case class Then[E](left: Cause[E], right: Cause[E]) extends Cause[E] { self => override final def equals(that: Any): Boolean = that match { case traced: Traced[_] => self.equals(traced.cause) + case meta: Meta[_] => self.equals(meta.cause) case other: Cause[_] => eq(other) || sym(assoc)(other, self) || sym(dist)(self, other) case _ => false } - override final def hashCode: Int = flatten(self).hashCode + override final def hashCode: Int = Cause.flatten(self).hashCode private def eq(that: Cause[_]): Boolean = (self, that) match { case (tl: Then[_], tr: Then[_]) => tl.left == tr.left && tl.right == tr.right @@ -356,28 +475,53 @@ object Cause extends Serializable { } } - final case class Both[E](left: Cause[E], right: Cause[E]) extends Cause[E] { self => + object Then { + def apply[E](left: Cause[E], right: Cause[E]): Cause[E] = + new Then(left, right) + } + + private final case class Both[E](left: Cause[E], right: Cause[E]) extends Cause[E] { self => override final def equals(that: Any): Boolean = that match { case traced: Traced[_] => self.equals(traced.cause) - case other: Cause[_] => eq(other) || sym(assoc)(self, other) || comm(other) + case meta: Meta[_] => self.equals(meta.cause) + case other: Cause[_] => eq(other) || sym(assoc)(self, other) || sym(dist)(self, other) || comm(other) case _ => false } - override final def hashCode: Int = flatten(self).hashCode + override final def hashCode: Int = Cause.flatten(self).hashCode private def eq(that: Cause[_]) = (self, that) match { case (bl: Both[_], br: Both[_]) => bl.left == br.left && bl.right == br.right case _ => false } + private def assoc(l: Cause[_], r: Cause[_]): Boolean = (l, r) match { case (Both(Both(al, bl), cl), Both(ar, Both(br, cr))) => al == ar && bl == br && cl == cr case _ => false } + + private def dist(l: Cause[_], r: Cause[_]): Boolean = (l, r) match { + case (Both(Then(al1, bl), Then(al2, cl)), Then(ar, Both(br, cr))) + if al1 == al2 && al1 == ar && bl == br && cl == cr => + true + case (Both(Then(al, cl1), Then(bl, cl2)), Then(Both(ar, br), cr)) + if cl1 == cl2 && al == ar && bl == br && cl1 == cr => + true + case _ => false + } + private def comm(that: Cause[_]): Boolean = (self, that) match { case (Both(al, bl), Both(ar, br)) => al == br && bl == ar case _ => false } } + object Both { + def apply[E](left: Cause[E], right: Cause[E]): Cause[E] = + new Both(left, right) + } + + private final case class Data(stackless: Boolean) + private[Cause] def sym(f: (Cause[_], Cause[_]) => Boolean): (Cause[_], Cause[_]) => Boolean = (l, r) => f(l, r) || f(r, l) diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala index 6128cbee2ed3..445f04b36c25 100644 --- a/core/shared/src/main/scala/zio/ZIO.scala +++ b/core/shared/src/main/scala/zio/ZIO.scala @@ -1287,6 +1287,19 @@ sealed trait ZIO[-R, +E, +A] extends Serializable { self => final def timeoutFail[E1 >: E](e: E1)(d: Duration): ZIO[R with Clock, E1, A] = ZIO.flatten(timeoutTo(ZIO.fail(e))(ZIO.succeed)(d)) + /** + * Returns an effect that will attempt to timeout this effect, but will not + * wait for the running effect to terminate if the timeout elapses without + * producing a value. Returns `Right` with the produced value if the effect + * completes before the timeout or `Left` with the interrupting fiber + * otherwise. + */ + final def timeoutFork(d: Duration): ZIO[R with Clock, E, Either[Fiber[E, A], A]] = + raceWith(ZIO.sleep(d))( + (exit, timeoutFiber) => ZIO.done(exit).map(Right(_)) <* timeoutFiber.interrupt, + (_, fiber) => fiber.interrupt.flatMap(ZIO.done).fork.map(Left(_)) + ) + /** * Returns an effect that will timeout this effect, returning either the * default value if the timeout elapses before the effect has produced a @@ -2477,7 +2490,8 @@ private[zio] trait ZIOFunctions extends Serializable { * Terminates with exceptions on the `Left` side of the `Either` error, if it * exists. Otherwise extracts the contained `IO[E, A]` */ - final def unsandbox[R, E, A](v: ZIO[R, Cause[E], A]): ZIO[R, E, A] = v.catchAll[R, E, A](halt) + final def unsandbox[R, E, A](v: ZIO[R, Cause[E], A]): ZIO[R, E, A] = + v.mapErrorCause(_.flatten) /** * Disables supervision for this effect. This will cause fibers forked by diff --git a/core/shared/src/main/scala/zio/ZManaged.scala b/core/shared/src/main/scala/zio/ZManaged.scala index 92d17dd2e41f..53de53fd6f30 100644 --- a/core/shared/src/main/scala/zio/ZManaged.scala +++ b/core/shared/src/main/scala/zio/ZManaged.scala @@ -1385,7 +1385,7 @@ object ZManaged { * The inverse operation to `sandbox`. Submerges the full cause of failure. */ final def unsandbox[R, E, A](v: ZManaged[R, Cause[E], A]): ZManaged[R, E, A] = - v.catchAll(halt) + v.mapErrorCause(_.flatten) /** * Unwraps a `ZManaged` that is inside a `ZIO`. diff --git a/docs/datatypes/schedule.md b/docs/datatypes/schedule.md index 68b13ca3f9fd..bc7ec3b8daaa 100644 --- a/docs/datatypes/schedule.md +++ b/docs/datatypes/schedule.md @@ -96,3 +96,9 @@ recur, using the minimum of the two delays between recurrences: ```scala mdoc:silent val expCapped = Schedule.exponential(100.milliseconds) || Schedule.spaced(1.second) ``` + +Stops retrying after a specified amount of time has elapsed: + +```scala mdoc:silent +val expMaxElapsed = Schedule.exponential(10.milliseconds) && ZSchedule.elapsed.whileOutput(_ < 30.seconds) +``` diff --git a/streams/shared/src/main/scala/zio/stream/ZStream.scala b/streams/shared/src/main/scala/zio/stream/ZStream.scala index 9ca2741797a6..aa0ae61dd285 100644 --- a/streams/shared/src/main/scala/zio/stream/ZStream.scala +++ b/streams/shared/src/main/scala/zio/stream/ZStream.scala @@ -635,7 +635,7 @@ class ZStream[-R, +E, +A](val process: ZManaged[R, E, Pull[R, E, A]]) extends Se } yield a } - Pull.sequenceCauseOption(e) match { + Cause.sequenceCauseOption(e) match { case None => Pull.end case Some(c) => next(c) } @@ -932,23 +932,29 @@ class ZStream[-R, +E, +A](val process: ZManaged[R, E, Pull[R, E, A]]) extends Se final def flatMap[R1 <: R, E1 >: E, B](f0: A => ZStream[R1, E1, B]): ZStream[R1, E1, B] = ZStream[R1, E1, B] { for { - as <- self.process - switchPull <- ZManaged.switchable[R1, E1, (URIO[R1, Any], Pull[R1, E1, B])] - currPull <- Ref.make[(URIO[R1, Any], Pull[R1, E1, B])]((UIO.unit, Pull.end)).toManaged_ + currPull <- Ref.make[Pull[R1, E1, B]](Pull.end).toManaged_ + as <- self.process + finalizer <- ZManaged.finalizerRef[R1](_ => UIO.unit) + pullOuter = ZIO.uninterruptibleMask { restore => + restore(as).flatMap { a => + (for { + reservation <- f0(a).process.reserve + bs <- restore(reservation.acquire) + _ <- finalizer.set(reservation.release) + _ <- currPull.set(bs) + } yield ()).mapError(Some(_)) + } + } bs = { def go: Pull[R1, E1, B] = - currPull.get.flatMap { - case (release, bs) => - bs.catchAll { - case e @ Some(_) => release *> ZIO.fail(e) - case None => - release *> - as.flatMap { a => - switchPull { - f0(a).process.withEarlyRelease.tap(n => ZManaged.fromEffectUninterruptible(currPull.set(n))) - }.mapError(Some(_)) - } *> go - } + currPull.get.flatten.catchAll { + case e @ Some(e1) => + (finalizer.get.flatMap(_(Exit.fail(e1))) *> finalizer.set(_ => UIO.unit)).uninterruptible *> ZIO.fail( + e + ) + case None => + (finalizer.get.flatMap(_(Exit.succeed(()))) *> finalizer + .set(_ => UIO.unit)).uninterruptible *> pullOuter *> go } go @@ -1423,7 +1429,7 @@ class ZStream[-R, +E, +A](val process: ZManaged[R, E, Pull[R, E, A]]) extends Se ZStream { self.process .mapErrorCause(f) - .map(_.mapErrorCause(Pull.sequenceCauseOption(_) match { + .map(_.mapErrorCause(Cause.sequenceCauseOption(_) match { case None => Cause.fail(None) case Some(c) => f(c).map(Some(_)) })) @@ -2289,30 +2295,6 @@ object ZStream { case Take.Fail(e) => halt(e) case Take.End => end } - - def sequenceCauseOption[E](c: Cause[Option[E]]): Option[Cause[E]] = - c match { - case Cause.Traced(cause, trace) => sequenceCauseOption(cause).map(Cause.Traced(_, trace)) - case Cause.Interrupt => Some(Cause.Interrupt) - case d @ Cause.Die(_) => Some(d) - case Cause.Fail(Some(e)) => Some(Cause.Fail(e)) - case Cause.Fail(None) => None - case Cause.Then(left, right) => - (sequenceCauseOption(left), sequenceCauseOption(right)) match { - case (Some(cl), Some(cr)) => Some(Cause.Then(cl, cr)) - case (None, Some(cr)) => Some(cr) - case (Some(cl), None) => Some(cl) - case (None, None) => None - } - - case Cause.Both(left, right) => - (sequenceCauseOption(left), sequenceCauseOption(right)) match { - case (Some(cl), Some(cr)) => Some(Cause.Both(cl, cr)) - case (None, Some(cr)) => Some(cr) - case (Some(cl), None) => Some(cl) - case (None, None) => None - } - } } /** @@ -2455,7 +2437,7 @@ object ZStream { k => runtime.unsafeRun( k.foldCauseM( - Pull.sequenceCauseOption(_) match { + Cause.sequenceCauseOption(_) match { case None => output.offer(Pull.end) case Some(c) => output.offer(Pull.halt(c)) }, @@ -2489,7 +2471,7 @@ object ZStream { k => runtime.unsafeRun( k.foldCauseM( - Pull.sequenceCauseOption(_) match { + Cause.sequenceCauseOption(_) match { case None => output.offer(Pull.end) case Some(c) => output.offer(Pull.halt(c)) }, @@ -2520,7 +2502,7 @@ object ZStream { k => runtime.unsafeRun( k.foldCauseM( - Pull.sequenceCauseOption(_) match { + Cause.sequenceCauseOption(_) match { case None => output.offer(Pull.end) case Some(c) => output.offer(Pull.halt(c)) }, diff --git a/test/shared/src/main/scala/zio/test/Assertion.scala b/test/shared/src/main/scala/zio/test/Assertion.scala index 35cff4e9dfad..0ae30eda2d6f 100644 --- a/test/shared/src/main/scala/zio/test/Assertion.scala +++ b/test/shared/src/main/scala/zio/test/Assertion.scala @@ -18,7 +18,7 @@ package zio.test import scala.reflect.ClassTag -import zio.{ Cause, Exit } +import zio.Exit import zio.test.Assertion._ import zio.test.Assertion.Render._ @@ -220,9 +220,9 @@ object Assertion { Assertion.assertionRec[Exit[Any, Any]]("dies")(param(assertion)) { (self, actual) => actual match { case Exit.Failure(cause) if cause.died => - cause.untraced match { - case Cause.Die(t) => assertion.run(t) - case _ => BoolAlgebra.failure(AssertionValue(self, actual)) + cause.dieOption match { + case Some(t) => assertion.run(t) + case _ => BoolAlgebra.failure(AssertionValue(self, actual)) } case _ => BoolAlgebra.failure(AssertionValue(self, actual)) } @@ -240,7 +240,7 @@ object Assertion { */ final def equalTo[A](expected: A): Assertion[A] = Assertion.assertion("equalTo")(param(expected)) { actual => - (expected, actual) match { + (actual, expected) match { case (left: Array[_], right: Array[_]) => left.sameElements[Any](right) case (left, right) => left == right } diff --git a/test/shared/src/main/scala/zio/test/DefaultTestReporter.scala b/test/shared/src/main/scala/zio/test/DefaultTestReporter.scala index 61f79f8e93da..0c89a64ad419 100644 --- a/test/shared/src/main/scala/zio/test/DefaultTestReporter.scala +++ b/test/shared/src/main/scala/zio/test/DefaultTestReporter.scala @@ -142,9 +142,9 @@ object DefaultTestReporter { else " did not satisfy " private def renderCause(cause: Cause[Any], offset: Int): String = - cause match { - case Cause.Die(TestTimeoutException(message)) => message - case _ => cause.prettyPrint.split("\n").map(withOffset(offset + tabSize)).mkString("\n") + cause.dieOption match { + case Some(TestTimeoutException(message)) => message + case _ => cause.prettyPrint.split("\n").map(withOffset(offset + tabSize)).mkString("\n") } private def withOffset(n: Int)(s: String): String = diff --git a/test/shared/src/main/scala/zio/test/GenZIO.scala b/test/shared/src/main/scala/zio/test/GenZIO.scala index 26232cc33f35..ccb736d06869 100644 --- a/test/shared/src/main/scala/zio/test/GenZIO.scala +++ b/test/shared/src/main/scala/zio/test/GenZIO.scala @@ -21,6 +21,41 @@ import zio.random.Random trait GenZIO { + /** + * A generator of `Cause` values + */ + final def causes[R <: Random with Sized, E](e: Gen[R, E], t: Gen[R, Throwable]): Gen[R, Cause[E]] = { + val failure = e.map(Cause.fail) + val die = t.map(Cause.die) + val interrupt = Gen.const(Cause.interrupt) + def traced(n: Int) = Gen.suspend(causesN(n - 1).map(Cause.Traced(_, ZTrace(0, Nil, Nil, None)))) + def meta(n: Int) = Gen.suspend(causesN(n - 1).flatMap(c => Gen.elements(Cause.stack(c), Cause.stackless(c)))) + + def sequential(n: Int) = Gen.suspend { + for { + i <- Gen.int(1, n - 1) + l <- causesN(i) + r <- causesN(n - i) + } yield Cause.Then(l, r) + } + + def parallel(n: Int) = Gen.suspend { + for { + i <- Gen.int(1, n - 1) + l <- causesN(i) + r <- causesN(n - i) + } yield Cause.Both(l, r) + } + + def causesN(n: Int): Gen[R, Cause[E]] = Gen.suspend { + if (n == 1) Gen.oneOf(failure, die, interrupt) + else if (n == 2) Gen.oneOf(traced(n), meta(n)) + else Gen.oneOf(traced(n), meta(n), sequential(n), parallel(n)) + } + + Gen.small(causesN, 1) + } + /** * A generator of effects that are the result of chaining the specified * effect with itself a random number of times.