diff --git a/modules/core/shared/src/main/scala/weaver/Comparison.scala b/modules/core/shared/src/main/scala/weaver/Comparison.scala new file mode 100644 index 00000000..d6f7055a --- /dev/null +++ b/modules/core/shared/src/main/scala/weaver/Comparison.scala @@ -0,0 +1,77 @@ +package weaver + +import cats.Eq +import cats.Show +import com.eed3si9n.expecty._ +import scala.annotation.implicitNotFound + +/** + * A type class used to compare two instances of the same type and construct an + * informative report. + * + * If the comparison succeeds with [[Result.Success]] then no report is printed. + * If the comparison fails with [[Result.Failure]], then the report is printed + * with the test failure. + * + * The report is generally a diff of the `expected` and `found` values. It may + * use ANSI escape codes to add color. + */ +@implicitNotFound("Could not find an implicit Comparison[${A}]. Does ${A} have an associated cats.Eq[${A}] instance?") +trait Comparison[A] { + + def diff(expected: A, found: A): Comparison.Result +} + +object Comparison { + sealed trait Result + object Result { + case object Success extends Result + case class Failure(report: String) extends Result + } + + /** + * Create a [[Comparison]] instance from an [[cats.kernel.Eq]] implementation. + * + * Uses the [[cats.Show]] instance or [[cats.Show.fromToString]] to construct + * a string diff of the `expected` and `found` values on failure. + */ + implicit def fromEq[A]( + implicit eqv: Eq[A], + showA: Show[A] = Show.fromToString[A] + ): Comparison[A] = { + new Comparison[A] { + def diff(expected: A, found: A): Result = { + if (eqv.eqv(found, expected)) { + Result.Success + } else { + val expectedLines = showA.show(expected).linesIterator.toSeq + val foundLines = showA.show(found).linesIterator.toSeq + val report = DiffUtil + .mkColoredLineDiff(expectedLines, foundLines) + .linesIterator + .toSeq + .map(str => Console.RESET.toString + str) + .mkString("\n") + Result.Failure(report) + } + } + } + } + + /** + * Create a [[Comparison]] instance from a `diff` implementation. + */ + def instance[A](f: (A, A) => Option[String]): Comparison[A] = + new Comparison[A] { + def diff(expected: A, found: A): Result = f(expected, found) match { + case None => Result.Success + case Some(report) => Result.Failure(report) + } + } + + /** + * Create a [[Comparison]] instance from a `diff` implementation. + */ + def instance[A](f: PartialFunction[(A, A), String]): Comparison[A] = + instance((expected, found) => f.lift((expected, found))) +} diff --git a/modules/core/shared/src/main/scala/weaver/internals/ExpectSame.scala b/modules/core/shared/src/main/scala/weaver/internals/ExpectSame.scala index 794d4a3d..82dccb07 100644 --- a/modules/core/shared/src/main/scala/weaver/internals/ExpectSame.scala +++ b/modules/core/shared/src/main/scala/weaver/internals/ExpectSame.scala @@ -5,35 +5,21 @@ import cats.Show import cats.data.{ NonEmptyList, Validated } import cats.kernel.Eq -import com.eed3si9n.expecty._ - private[weaver] trait ExpectSame { def eql[A]( expected: A, found: A)( - implicit eqA: Eq[A], - showA: Show[A] = Show.fromToString[A], + implicit comparisonA: Comparison[A], loc: SourceLocation): Expectations = { - - if (eqA.eqv(expected, found)) - Expectations(Validated.validNel(())) - else { - val header = "Values not equal:" - - val expectedLines = showA.show(expected).linesIterator.toSeq - val foundLines = showA.show(found).linesIterator.toSeq - val sourceLocs = NonEmptyList.of(loc) - val diff = DiffUtil - .mkColoredLineDiff(expectedLines, foundLines) - .linesIterator - .toSeq - .map(str => Console.RESET.toString + str) - .mkString("\n") - - Expectations( - Validated.invalidNel[AssertionException, Unit]( - new AssertionException(header + "\n\n" + diff, sourceLocs))) + comparisonA.diff(expected, found) match { + case Comparison.Result.Success => Expectations(Validated.validNel(())) + case Comparison.Result.Failure(report) => + val header = "Values not equal:" + val sourceLocs = NonEmptyList.of(loc) + Expectations( + Validated.invalidNel[AssertionException, Unit]( + new AssertionException(header + "\n\n" + report, sourceLocs))) } } @@ -43,7 +29,8 @@ private[weaver] trait ExpectSame { def same[A]( expected: A, found: A)( - implicit eqA: Eq[A] = Eq.fromUniversalEquals[A], - showA: Show[A] = Show.fromToString[A], - loc: SourceLocation): Expectations = eql(expected, found) + implicit comparisonA: Comparison[A] = + Comparison.fromEq[A](Eq.fromUniversalEquals, Show.fromToString), + loc: SourceLocation): Expectations = + eql(expected, found)(comparisonA, loc) } diff --git a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala index 8eae0f88..0e7cda57 100644 --- a/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala +++ b/modules/framework-cats/shared/src/test/scala/DogFoodTests.scala @@ -255,16 +255,41 @@ object DogFoodTests extends IOSuite { } test( - "expect.same delegates to show when an instance is found") { + "expect.eql delegates to Comparison show when an instance is found") { _.runSuite(Meta.Rendering).map { case (logs, _) => val actual = extractLogEventAfterFailures(logs) { - case LoggedEvent.Error(msg) if msg.contains("(cats.Show)") => msg + case LoggedEvent.Error(msg) if msg.contains("(eql Comparison)") => + msg }.get val expected = """ - |- (cats.Show) 0ms + |- (eql Comparison) 0ms + | Values not equal: (src/main/DogFoodTests.scala:5) + | + | Foo { | Foo { + | s: foo | s: foo + | i: [1] | i: [2] + | } | } + """.stripMargin.trim + + expect.same(actual, expected) + } + } + + test( + "expect.same delegates to Comparison show when an instance is found") { + _.runSuite(Meta.Rendering).map { + case (logs, _) => + val actual = + extractLogEventAfterFailures(logs) { + case LoggedEvent.Error(msg) if msg.contains("(same Comparison)") => + msg + }.get + + val expected = """ + |- (same Comparison) 0ms | Values not equal: (src/main/DogFoodTests.scala:5) | | Foo { | Foo { diff --git a/modules/framework-cats/shared/src/test/scala/ExpectationsTests.scala b/modules/framework-cats/shared/src/test/scala/ExpectationsTests.scala index 8fdfa666..fa694cb6 100644 --- a/modules/framework-cats/shared/src/test/scala/ExpectationsTests.scala +++ b/modules/framework-cats/shared/src/test/scala/ExpectationsTests.scala @@ -61,9 +61,14 @@ object ExpectationsTests extends SimpleIOSuite { }) } - pureTest("expect.same respects cats.kernel.Eq") { + pureTest("expect.eql respects cats.kernel.Eq") { implicit val eqInt: Eq[Int] = Eq.allEqual - expect.same(0, 1) + expect.eql(0, 1) + } + + pureTest("expect.eql respects weaver.Comparison") { + implicit val comparison: Comparison[Int] = Comparison.fromEq(Eq.allEqual) + expect.eql(0, 1) } pureTest("when success") { diff --git a/modules/framework-cats/shared/src/test/scala/Meta.scala b/modules/framework-cats/shared/src/test/scala/Meta.scala index 8c7edd68..6d6b5e33 100644 --- a/modules/framework-cats/shared/src/test/scala/Meta.scala +++ b/modules/framework-cats/shared/src/test/scala/Meta.scala @@ -40,21 +40,26 @@ object Meta { cancel("I was cancelled :(") } - pureTest("(cats.Show)") { - import cats.Show - case class Foo(s: String, i: Int) - object Foo { - implicit val show: Show[Foo] = Show.show[Foo] { - case Foo(s, i) => - s""" + import cats.Show + case class Foo(s: String, i: Int) + object Foo { + val show: Show[Foo] = Show.show[Foo] { + case Foo(s, i) => + s""" |Foo { | s: ${Show[String].show(s)} | i: ${Show[Int].show(i)} |} """.stripMargin.trim() - } } + implicit val comparison: Comparison[Foo] = + Comparison.fromEq[Foo](cats.Eq.fromUniversalEquals, show) + } + pureTest("(eql Comparison)") { + expect.eql(Foo("foo", 1), Foo("foo", 2)) + } + pureTest("(same Comparison)") { expect.same(Foo("foo", 1), Foo("foo", 2)) } }