Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Comparison trait. #24

Merged
merged 11 commits into from
Apr 8, 2024
77 changes: 77 additions & 0 deletions modules/core/shared/src/main/scala/weaver/Comparison.scala
Original file line number Diff line number Diff line change
@@ -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] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could potentially extend Eq as a Comparison instance does imply an Eq instance (and should satisfy Eq laws).

I'm not sure if the inheritance would play well with implicit searches.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question.

I think Comparison should not necessarily imply Eq, because you could have comparisons that verify if an instance A is contained in another A and I think that'd be a legitimate use. However there could be a more refined EqComparison that would imply Eq. In that case, the MTL treatment of having the implication reified as a def eqInstance: Eq[A] instead of OO-inheritance would probably be the right course of action, in order to avoid implicit search problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting case. In that vein, Comparison is a general predicate that has a string representation on failure? It doesn't have any laws associated with it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that vein, Comparison is a general predicate that has a string representation on failure? It doesn't have any laws associated with it.

I think so, yes


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)))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}

Expand All @@ -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)
}
31 changes: 28 additions & 3 deletions modules/framework-cats/shared/src/test/scala/DogFoodTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
21 changes: 13 additions & 8 deletions modules/framework-cats/shared/src/test/scala/Meta.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down