-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revive "Strict Equality" for
assertEquals()
(#521)
* Make FailException and ComparisonFailException work more similarly. Previously, FailException had some custom nice-to-have features that ComparisonFailException didn't have. * Introduce "strict equality" mode for `assertEquals()` and friends. Previously, MUnit had a subtyping constraint on `assertEquals(a, b)` so that it would fail to compile if `a` was not a subtype of `b`. This was a suboptimal solution because the compile error messages could become cryptic in some cases. Additionally, this API didn't integrate with other libaries like Cats that has its own `cats.Eq[A,B]` type-class. Now, MUnit uses a new `munit.Compare[A,B]` type-class for comparing values of different types. By default, MUnit provides a "universal" instance that permits comparison between all types and uses the built-in `==` method. Users can optionally enable "strict equality" by adding the compiler option `"-Xmacro-settings.munit.strictEquality"` in Scala 2. In Scala 3, we use the `Eql[A, B]` type-classes instead to determine type equality. * Address review feedback * Drop strict equality, allow comparison between supertypes/subtypes This is a fourth attempt at improving strict equality in MUnit `assertEquals()` assertions. * First attempt (current release version): require second argument to be a supertype of the first argument. This has the flaw that the compile error message is cryptic and that the ordering of the arguments affects compilation. * Second attempt: use `Eql[A, B]` in Scala 3 and allow comparing any types in Scala 2. This has the flaw that it's a regression in some cases for Scala 2 users and that `Eql[A, B]` is not really usable in its current form, see related discussion https://contributors.scala-lang.org/t/should-multiversal-equality-provide-default-eql-instances/4574 * Third attempt: implement "strict equality" for Scala 2 with a macro and `Eql[T, T]` in Scala. This improves the situation for Scala 2, but would mean relying on a feature that we can't easily port to Scala 3. * Fourth attempt (this commit): improve the first attempt (current release) by allowing `Compare[A, B]` as long as `A <:< B` OR `B <:< A`. This is possible thanks to an observation by Gabriele Petronella that it's possible to layer the implicits to avoid diverging implicit search. The benefit of the fourth approach is that it works the same way for Scala 3 and Scala 3. It's very nice that we can avoid macros as well. * Address review feedback * Run scalafmtSbt * Remove unused import * Fix dotty tests in AssertionsSuite The Scala 3 (dotty) tests now use compareSubtypeWithSupertype instead of compareSupertypeWithSubtype. Additionally, the "unrelated" test was not seeing the context code above and so I've moved all the code into compileErrors. * Add mima exclusions for assertEquals and co * Remove unused import in scala-3 MacroCompat * Reintroduce special-case msgs for comparing arrays * Reintroduce better string inequality error msgs * Update Clue deprecation to 1.0 Co-authored-by: Ólafur Páll Geirsson <olafurpg@gmail.com> * Fix typo in AssertionsSuite test name Co-authored-by: Ólafur Páll Geirsson <olafurpg@gmail.com> Co-authored-by: Olafur Pall Geirsson <lgeirsson@twitter.com> Co-authored-by: Ólafur Páll Geirsson <olafurpg@gmail.com>
- Loading branch information
1 parent
675a25e
commit 8558ab1
Showing
18 changed files
with
483 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package munit | ||
|
||
import munit.internal.difflib.Diffs | ||
import munit.internal.difflib.ComparisonFailExceptionHandler | ||
import scala.annotation.implicitNotFound | ||
|
||
/** | ||
* A type-class that is used to compare values in MUnit assertions. | ||
* | ||
* By default, uses == and allows comparison between any two types as long | ||
* they have a supertype/subtype relationship. For example: | ||
* | ||
* - Compare[T, T] OK | ||
* - Compare[Some[Int], Option[Int]] OK, subtype | ||
* - Compare[Option[Int], Some[Int]] OK, supertype | ||
* - Compare[List[Int], collection.Seq[Int]] OK, subtype | ||
* - Compare[List[Int], Vector[Int]] Error, requires upcast to `Seq[Int]` | ||
*/ | ||
@implicitNotFound( | ||
// NOTE: Dotty ignores this message if the string is formatted as a multiline string """...""" | ||
"Can't compare these two types:\n First type: ${A}\n Second type: ${B}\nPossible ways to fix this error:\n Alternative 1: provide an implicit instance for Compare[${A}, ${B}]\n Alternative 2: upcast either type into `Any` or a shared supertype" | ||
) | ||
trait Compare[A, B] { | ||
|
||
/** | ||
* Returns true if the values are equal according to the rules of this `Compare[A, B]` instance. | ||
* | ||
* The default implementation of this method uses `==`. | ||
*/ | ||
def isEqual(obtained: A, expected: B): Boolean | ||
|
||
/** | ||
* Throws an exception to fail this assertion when two values are not equal. | ||
* | ||
* Override this method to customize the error message. For example, it may | ||
* be helpful to generate an image/HTML file if you're comparing visual | ||
* values. Anything is possible, use your imagination! | ||
* | ||
* @return should ideally throw a org.junit.ComparisonFailException in order | ||
* to support the IntelliJ diff viewer. | ||
*/ | ||
def failEqualsComparison( | ||
obtained: A, | ||
expected: B, | ||
title: Any, | ||
loc: Location, | ||
assertions: Assertions | ||
): Nothing = { | ||
val diffHandler = new ComparisonFailExceptionHandler { | ||
override def handle( | ||
message: String, | ||
_obtained: String, | ||
_expected: String, | ||
loc: Location | ||
): Nothing = | ||
assertions.failComparison( | ||
message, | ||
obtained, | ||
expected | ||
)(loc) | ||
} | ||
// Attempt 1: custom pretty-printer that produces multiline output, which is | ||
// optimized for line-by-line diffing. | ||
Diffs.assertNoDiff( | ||
assertions.munitPrint(obtained), | ||
assertions.munitPrint(expected), | ||
diffHandler, | ||
title = assertions.munitPrint(title), | ||
printObtainedAsStripMargin = false | ||
)(loc) | ||
|
||
// Attempt 2: try with `.toString` in case `munitPrint()` produces identical | ||
// formatting for both values. | ||
Diffs.assertNoDiff( | ||
obtained.toString(), | ||
expected.toString(), | ||
diffHandler, | ||
title = assertions.munitPrint(title), | ||
printObtainedAsStripMargin = false | ||
)(loc) | ||
|
||
// Attempt 3: string comparison is not working, unconditionally fail the test. | ||
if (obtained.toString() == expected.toString()) | ||
assertions.failComparison( | ||
s"values are not equal even if they have the same `toString()`: $obtained", | ||
obtained, | ||
expected | ||
)(loc) | ||
else | ||
assertions.failComparison( | ||
s"values are not equal, even if their text representation only differs in leading/trailing whitespace and ANSI escape characters: $obtained", | ||
obtained, | ||
expected | ||
)(loc) | ||
} | ||
|
||
} | ||
|
||
object Compare extends ComparePriority1 { | ||
private val anyEquality: Compare[Any, Any] = _ == _ | ||
def defaultCompare[A, B]: Compare[A, B] = | ||
anyEquality.asInstanceOf[Compare[A, B]] | ||
} | ||
|
||
/** Allows comparison between A and B when A is a subtype of B */ | ||
trait ComparePriority1 extends ComparePriority2 { | ||
implicit def compareSubtypeWithSupertype[A, B](implicit | ||
ev: A <:< B | ||
): Compare[A, B] = Compare.defaultCompare | ||
} | ||
|
||
/** | ||
* Allows comparison between A and B when B is a subtype of A. | ||
* | ||
* This implicit is defined separately from ComparePriority1 in order to avoid | ||
* diverging implicit search when comparing equal types. | ||
*/ | ||
trait ComparePriority2 { | ||
implicit def compareSupertypeWithSubtype[A, B](implicit | ||
ev: A <:< B | ||
): Compare[B, A] = Compare.defaultCompare | ||
} |
Oops, something went wrong.