diff --git a/modules/core/shared/src/main/scala/weaver/Expectations.scala b/modules/core/shared/src/main/scala/weaver/Expectations.scala index d2613648..667881f7 100644 --- a/modules/core/shared/src/main/scala/weaver/Expectations.scala +++ b/modules/core/shared/src/main/scala/weaver/Expectations.scala @@ -96,15 +96,16 @@ case class Expectations(run: ValidatedNel[ExpectationFailed, Unit]) { Expectations(otherL.orElse(otherR).orElse(otherL.product(otherR).void)) } + /** Raises an error in an effect if an expectation has failed. */ def failFast[F[_]: Sync]: F[Unit] = this.run match { - case Invalid(e) => Sync[F].raiseError(e.head) + case Invalid(e) => Sync[F].raiseError(new ExpectationsFailed(e)) case Valid(_) => Sync[F].unit } /** * Adds the specified location to the list of locations that will be reported - * if an expectation is failed. + * if an expectation has failed. */ def traced(loc: SourceLocation): Expectations = Expectations(run.leftMap(_.map(e => diff --git a/modules/core/shared/src/main/scala/weaver/Result.scala b/modules/core/shared/src/main/scala/weaver/Result.scala index ff42db6a..49532efe 100644 --- a/modules/core/shared/src/main/scala/weaver/Result.scala +++ b/modules/core/shared/src/main/scala/weaver/Result.scala @@ -66,7 +66,7 @@ private[weaver] object Result { object Failures { final case class Failure( msg: String, - source: Throwable, + source: ExpectationFailed, locations: NonEmptyList[SourceLocation]) } @@ -104,13 +104,12 @@ private[weaver] object Result { def from(error: Throwable): Result = { error match { - case ex: ExpectationFailed => - Failures(NonEmptyList.of(Failures.Failure( - ex.message, - ex, - ex.locations))) case ex: IgnoredException => Ignored(ex.reason, ex.location) + case exs: ExpectationsFailed => + Failures(exs.failures.map { ex => + Failures.Failure(ex.message, ex, ex.locations) + }) case other => Exception(other) } diff --git a/modules/core/shared/src/main/scala/weaver/TestOutcome.scala b/modules/core/shared/src/main/scala/weaver/TestOutcome.scala index cc9c5609..c603de14 100644 --- a/modules/core/shared/src/main/scala/weaver/TestOutcome.scala +++ b/modules/core/shared/src/main/scala/weaver/TestOutcome.scala @@ -44,7 +44,8 @@ object TestOutcome { def cause: Option[Throwable] = result match { case Result.Exception(cause) => Some(cause) - case Result.Failures(failures) => Some(failures.head.source) + case Result.Failures(failures) => + Some(new ExpectationsFailed(failures.map(_.source))) case Result.OnlyTagNotAllowedInCI(_) | Result.Ignored( _, _) | Result.Success => None diff --git a/modules/core/shared/src/main/scala/weaver/exceptions.scala b/modules/core/shared/src/main/scala/weaver/exceptions.scala index d748cefc..d9cea861 100644 --- a/modules/core/shared/src/main/scala/weaver/exceptions.scala +++ b/modules/core/shared/src/main/scala/weaver/exceptions.scala @@ -8,8 +8,7 @@ sealed abstract class WeaverTestException private[weaver] ( final class ExpectationFailed( private[weaver] val message: String, - private[weaver] val locations: NonEmptyList[SourceLocation]) - extends WeaverTestException(message) { + private[weaver] val locations: NonEmptyList[SourceLocation]) { private[weaver] def withLocation( location: SourceLocation): ExpectationFailed = new ExpectationFailed(message, locations.append(location)) @@ -19,3 +18,8 @@ final class IgnoredException private[weaver] ( private[weaver] val reason: String, private[weaver] val location: SourceLocation) extends WeaverTestException(reason) + +final class ExpectationsFailed( + private[weaver] val failures: NonEmptyList[ExpectationFailed]) + extends WeaverTestException( + s"One or more expecations failed:\n${failures.map(_.message).toList.mkString(System.lineSeparator)}") diff --git a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala index 2ae004f8..c28ab675 100644 --- a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala +++ b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala @@ -401,6 +401,29 @@ object DogFoodTests extends IOSuite { } } + test("multiple failures are rendered for failFast") { + _.runSuite(Meta.Rendering).flatMap { + case (logs, _) => + val actual = extractFailureMessageForTest(logs, "(failFast)") + + assertInlineSnapshot( + actual, + """- (failFast) 0ms + [0] Values not equal: (src/main/DogFoodTests.scala:5) + [0] + [0] => Diff (- obtained, + expected) + [0] -2 + [0] +1 + + [1] Values not equal: (src/main/DogFoodTests.scala:5) + [1] + [1] => Diff (- obtained, + expected) + [1] -4 + [1] +3""" + ) + } + } + test("successes with clues are rendered correctly") { _.runSuite(Meta.Clue).flatMap { case (logs, _) => diff --git a/modules/framework-cats/shared/src/test/scala/Meta.scala b/modules/framework-cats/shared/src/test/scala/Meta.scala index 8ce7a835..b8d3a809 100644 --- a/modules/framework-cats/shared/src/test/scala/Meta.scala +++ b/modules/framework-cats/shared/src/test/scala/Meta.scala @@ -119,6 +119,9 @@ object Meta { expect(s"$x" == "2") } + test("(failFast)") { + expect.eql(1, 2).and(expect.eql(3, 4)).failFast.as(success) + } } object Clue extends SimpleIOSuite { diff --git a/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala b/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala index d398709f..d1ba02da 100644 --- a/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala +++ b/modules/scalacheck/shared/src/main/scala/weaver/scalacheck/Checkers.scala @@ -150,9 +150,10 @@ trait Checkers { x match { case Some((_, Right(ex))) if ex.run.isValid => TestResult.Success case Some((t, Right(ex))) => TestResult.Failure(t.show, ex) - case Some((t, Left(exception: ExpectationFailed))) => - TestResult.Failure(t.show, - Expectations(Validated.invalidNel(exception))) + case Some((t, Left(exception: ExpectationsFailed))) => + TestResult.Failure( + t.show, + Expectations(Validated.invalid(exception.failures))) case Some((t, Left(other))) => TestResult.Exception(t.show, other) case None => TestResult.Discard }