diff --git a/build.sbt b/build.sbt index 8cf89dfd..11713fa9 100644 --- a/build.sbt +++ b/build.sbt @@ -87,6 +87,24 @@ lazy val mimaEnable: List[Def.Setting[_]] = List( "munit.internal.junitinterface.JUnitComputer.this" ), // Known breaking changes for MUnit v1 + ProblemFilters.exclude[DirectMissingMethodProblem]( + "munit.Assertions.assertNotEquals" + ), + ProblemFilters.exclude[DirectMissingMethodProblem]( + "munit.Assertions.assertEquals" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.Assertions.assertNotEquals" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.Assertions.assertEquals" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.FunSuite.assertNotEquals" + ), + ProblemFilters.exclude[IncompatibleMethTypeProblem]( + "munit.FunSuite.assertEquals" + ), ProblemFilters.exclude[IncompatibleMethTypeProblem]( "munit.FunSuite.munitTestTransform" ), @@ -194,22 +212,8 @@ lazy val junit = project lazy val munit = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( sharedSettings, - Compile / unmanagedSourceDirectories ++= { - val root = (ThisBuild / baseDirectory).value / "munit" - val base = root / "shared" / "src" / "main" - val result = mutable.ListBuffer.empty[File] - val partialVersion = CrossVersion.partialVersion(scalaVersion.value) - if (isPreScala213(partialVersion)) { - result += base / "scala-pre-2.13" - } - if (isNotScala211(partialVersion)) { - result += base / "scala-post-2.11" - } - if (isScala2(partialVersion)) { - result += base / "scala-2" - } - result.toList - }, + Compile / unmanagedSourceDirectories ++= + crossBuildingDirectories("munit", "main").value, libraryDependencies ++= List( "org.scala-lang" % "scala-reflect" % { if (isScala3Setting.value) scala213 @@ -308,6 +312,8 @@ lazy val tests = crossProject(JSPlatform, JVMPlatform, NativePlatform) ((ThisBuild / baseDirectory).value / "tests" / "shared" / "src" / "main").getAbsolutePath.toString, scalaVersion ), + Test / unmanagedSourceDirectories ++= + crossBuildingDirectories("tests", "test").value, publish / skip := true ) .nativeConfigure(sharedNativeConfigure) @@ -348,3 +354,20 @@ lazy val docs = project Global / excludeLintKeys ++= Set( mimaPreviousArtifacts ) +def crossBuildingDirectories(name: String, config: String) = + Def.setting[Seq[File]] { + val root = (ThisBuild / baseDirectory).value / name + val base = root / "shared" / "src" / config + val result = mutable.ListBuffer.empty[File] + val partialVersion = CrossVersion.partialVersion(scalaVersion.value) + if (isPreScala213(partialVersion)) { + result += base / "scala-pre-2.13" + } + if (isNotScala211(partialVersion)) { + result += base / "scala-post-2.11" + } + if (isScala2(partialVersion)) { + result += base / "scala-2" + } + result.toList + } diff --git a/docs/assertions.md b/docs/assertions.md index faf893a8..b4067e7d 100644 --- a/docs/assertions.md +++ b/docs/assertions.md @@ -101,23 +101,29 @@ assertEquals( Comparing two values of different types is a compile error. ```scala mdoc:fail -assertEquals(1, "") +assertEquals(Option("message"), "message") ``` -The "expected" value (second argument) must be a subtype of the "obtained" value -(first argument). +It's a compile error even if the comparison is true at runtime. -```scala mdoc -assertEquals(Option(1), Some(1)) +```scala mdoc:fail +assertEquals(List(1), Vector(1)) ``` -It's a compile error if you swap the order of the arguments. - ```scala mdoc:fail -assertEquals(Some(1), Option(1)) +assertEquals('a', 'a'.toInt) +``` + +It's OK to compare two types as long as one argument is a subtype of the other +type. + +```scala mdoc +assertEquals(Option(1), Some(1)) // OK +assertEquals(Some(1), Option(1)) // OK ``` -Use `assertEquals[Any, Any]` if you really want to compare two different types. +Use `assertEquals[Any, Any]` if you think it's OK to compare the two types at +runtime. ```scala mdoc val right1: Either[String , Int] = Right(42) diff --git a/munit/shared/src/main/scala/munit/Assertions.scala b/munit/shared/src/main/scala/munit/Assertions.scala index 40576fcd..24642843 100644 --- a/munit/shared/src/main/scala/munit/Assertions.scala +++ b/munit/shared/src/main/scala/munit/Assertions.scala @@ -18,19 +18,6 @@ trait Assertions extends MacroCompat.CompileErrorMacro { def munitAnsiColors: Boolean = true - private def munitComparisonHandler( - actualObtained: Any, - actualExpected: Any - ): ComparisonFailExceptionHandler = - new ComparisonFailExceptionHandler { - override def handle( - message: String, - unusedObtained: String, - unusedExpected: String, - loc: Location - ): Nothing = failComparison(message, actualObtained, actualExpected)(loc) - } - private def munitFilterAnsi(message: String): String = if (munitAnsiColors) message else AnsiColors.filterAnsi(message) @@ -67,20 +54,25 @@ trait Assertions extends MacroCompat.CompileErrorMacro { Diffs.assertNoDiff( obtained, expected, - munitComparisonHandler(obtained, expected), + ComparisonFailExceptionHandler.fromAssertions(this, Clues.empty), munitPrint(clue), printObtainedAsStripMargin = true ) } } + /** + * Asserts that two elements are not equal according to the `Compare[A, B]` type-class. + * + * By default, uses `==` to compare values. + */ def assertNotEquals[A, B]( obtained: A, expected: B, clue: => Any = "values are the same" - )(implicit loc: Location, ev: A =:= B): Unit = { + )(implicit loc: Location, compare: Compare[A, B]): Unit = { StackTraces.dropInside { - if (obtained == expected) { + if (compare.isEqual(obtained, expected)) { failComparison( s"${munitPrint(clue)} expected same: $expected was not: $obtained", obtained, @@ -91,32 +83,17 @@ trait Assertions extends MacroCompat.CompileErrorMacro { } /** - * Asserts that two elements are equal using `==` equality. - * - * The "expected" value (second argument) must have the same type or be a - * subtype of the "obtained" value (first argument). For example: - * {{{ - * assertEquals(Option(1), Some(1)) // OK - * assertEquals(Some(1), Option(1)) // Error: Option[Int] is not a subtype of Some[Int] - * }}} + * Asserts that two elements are equal according to the `Compare[A, B]` type-class. * - * Use `assertEquals[Any, Any](a, b)` as an escape hatch to compare two - * values of different types. For example: - * {{{ - * val a: Either[List[String], Int] = Right(42) - * val b: Either[String, Int] = Right(42) - * assertEquals[Any, Any](a, b) // OK - * assertEquals(a, b) // Error: Either[String, Int] is not a subtype of Either[List[String], Int] - * }}} + * By default, uses `==` to compare values. */ def assertEquals[A, B]( obtained: A, expected: B, clue: => Any = "values are not the same" - )(implicit loc: Location, ev: B <:< A): Unit = { + )(implicit loc: Location, compare: Compare[A, B]): Unit = { StackTraces.dropInside { - if (obtained != expected) { - + if (!compare.isEqual(obtained, expected)) { (obtained, expected) match { case (a: Array[_], b: Array[_]) if a.sameElements(b) => // Special-case error message when comparing arrays. See @@ -137,34 +114,7 @@ trait Assertions extends MacroCompat.CompileErrorMacro { ) case _ => } - - Diffs.assertNoDiff( - munitPrint(obtained), - munitPrint(expected), - munitComparisonHandler(obtained, expected), - munitPrint(clue), - printObtainedAsStripMargin = false - ) - // try with `.toString` in case `munitPrint()` produces identical formatting for both values. - Diffs.assertNoDiff( - obtained.toString(), - expected.toString(), - munitComparisonHandler(obtained, expected), - munitPrint(clue), - printObtainedAsStripMargin = false - ) - if (obtained.toString() == expected.toString()) - failComparison( - s"values are not equal even if they have the same `toString()`: $obtained", - obtained, - expected - ) - else - failComparison( - s"values are not equal, even if their text representation only differs in leading/trailing whitespace and ANSI escape characters: $obtained", - obtained, - expected - ) + compare.failEqualsComparison(obtained, expected, clue, loc, this) } } } @@ -320,7 +270,8 @@ trait Assertions extends MacroCompat.CompileErrorMacro { munitFilterAnsi(munitLines.formatLine(loc, message, clues)), obtained, expected, - loc + loc, + isStackTracesEnabled = false ) } diff --git a/munit/shared/src/main/scala/munit/Clue.scala b/munit/shared/src/main/scala/munit/Clue.scala index 898c6362..cb905bee 100644 --- a/munit/shared/src/main/scala/munit/Clue.scala +++ b/munit/shared/src/main/scala/munit/Clue.scala @@ -10,5 +10,7 @@ class Clue[+T]( override def toString(): String = s"Clue($source, $value)" } object Clue extends MacroCompat.ClueMacro { - def empty[T](value: T): Clue[T] = new Clue("", value, "") + @deprecated("use fromValue instead", "1.0.0") + def empty[T](value: T): Clue[T] = fromValue(value) + def fromValue[T](value: T): Clue[T] = new Clue("", value, "") } diff --git a/munit/shared/src/main/scala/munit/Clues.scala b/munit/shared/src/main/scala/munit/Clues.scala index 11eb04cc..8659a06a 100644 --- a/munit/shared/src/main/scala/munit/Clues.scala +++ b/munit/shared/src/main/scala/munit/Clues.scala @@ -5,3 +5,7 @@ import munit.internal.console.Printers class Clues(val values: List[Clue[_]]) { override def toString(): String = Printers.print(this) } +object Clues { + def empty: Clues = new Clues(List()) + def fromValue[T](value: T): Clues = new Clues(List(Clue.fromValue(value))) +} diff --git a/munit/shared/src/main/scala/munit/Compare.scala b/munit/shared/src/main/scala/munit/Compare.scala new file mode 100644 index 00000000..15745d7a --- /dev/null +++ b/munit/shared/src/main/scala/munit/Compare.scala @@ -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 +} diff --git a/munit/shared/src/main/scala/munit/ComparisonFailException.scala b/munit/shared/src/main/scala/munit/ComparisonFailException.scala index 42191d80..74eb75c7 100644 --- a/munit/shared/src/main/scala/munit/ComparisonFailException.scala +++ b/munit/shared/src/main/scala/munit/ComparisonFailException.scala @@ -11,22 +11,54 @@ import org.junit.ComparisonFailure * * @param message the exception message. * @param obtained the obtained value from this comparison. + * @param obtainedString the pretty-printed representation of the obtained value. + * This string is displayed in the IntelliJ diff viewer. * @param expected the expected value from this comparison. + * @param expectedString the pretty-printed representation of the obtained value. + * This string is displayed in the IntelliJ diff viewer. * @param location the source location where this exception was thrown. */ class ComparisonFailException( val message: String, val obtained: Any, + val obtainedString: String, val expected: Any, - val location: Location -) extends ComparisonFailure(message, s"$expected", s"$obtained") + val expectedString: String, + val location: Location, + val isStackTracesEnabled: Boolean +) extends ComparisonFailure(message, expectedString, obtainedString) with FailExceptionLike[ComparisonFailException] { + def this( + message: String, + obtained: Any, + expected: Any, + location: Location, + isStackTracesEnabled: Boolean + ) = this( + message, + obtained, + s"$obtained", + expected, + s"$expected", + location, + isStackTracesEnabled + ) override def getMessage: String = message def withMessage(newMessage: String): ComparisonFailException = new ComparisonFailException( newMessage, obtained, + obtainedString, expected, - location + expectedString, + location, + isStackTracesEnabled ) + override def fillInStackTrace(): Throwable = { + val result = super.fillInStackTrace() + if (!isStackTracesEnabled) { + result.setStackTrace(result.getStackTrace().slice(0, 1)) + } + result + } } diff --git a/munit/shared/src/main/scala/munit/FailException.scala b/munit/shared/src/main/scala/munit/FailException.scala index 8000f7db..67414893 100644 --- a/munit/shared/src/main/scala/munit/FailException.scala +++ b/munit/shared/src/main/scala/munit/FailException.scala @@ -6,26 +6,25 @@ class FailException( val isStackTracesEnabled: Boolean, val location: Location ) extends AssertionError(message, cause) - with FailExceptionLike[FailException] - with Serializable { + with FailExceptionLike[FailException] { def this(message: String, location: Location) = - this(message, null, true, location) + this(message, null, isStackTracesEnabled = true, location) def this(message: String, cause: Throwable, location: Location) = this( message, cause, - true, + isStackTracesEnabled = true, location ) def withMessage(newMessage: String): FailException = copy(message = newMessage) - def copy( + + private[munit] def copy( message: String = this.message, cause: Throwable = this.cause, isStackTracesEnabled: Boolean = this.isStackTracesEnabled, location: Location = this.location ): FailException = new FailException(message, cause, isStackTracesEnabled, location) - override def fillInStackTrace(): Throwable = { val result = super.fillInStackTrace() if (!isStackTracesEnabled) { @@ -33,4 +32,5 @@ class FailException( } result } + } diff --git a/munit/shared/src/main/scala/munit/FailExceptionLike.scala b/munit/shared/src/main/scala/munit/FailExceptionLike.scala index 6c3289eb..c7bfa19c 100644 --- a/munit/shared/src/main/scala/munit/FailExceptionLike.scala +++ b/munit/shared/src/main/scala/munit/FailExceptionLike.scala @@ -13,7 +13,9 @@ package munit * `org.junit.ComparisonFailure` and this base trait. Internally, MUnit should * match against `FailExceptionLike[_]` instead of `munit.FailException` directly. */ -trait FailExceptionLike[T <: AssertionError] { self: AssertionError => +trait FailExceptionLike[T <: AssertionError] extends Serializable { + self: AssertionError => def withMessage(message: String): T def location: Location + def isStackTracesEnabled: Boolean } diff --git a/munit/shared/src/main/scala/munit/internal/difflib/ComparisonFailExceptionHandler.scala b/munit/shared/src/main/scala/munit/internal/difflib/ComparisonFailExceptionHandler.scala index bb761560..5b51cbf8 100644 --- a/munit/shared/src/main/scala/munit/internal/difflib/ComparisonFailExceptionHandler.scala +++ b/munit/shared/src/main/scala/munit/internal/difflib/ComparisonFailExceptionHandler.scala @@ -1,6 +1,8 @@ package munit.internal.difflib import munit.Location +import munit.Assertions +import munit.Clues trait ComparisonFailExceptionHandler { def handle( @@ -10,3 +12,19 @@ trait ComparisonFailExceptionHandler { location: Location ): Nothing } +object ComparisonFailExceptionHandler { + def fromAssertions( + assertions: Assertions, + clues: => Clues + ): ComparisonFailExceptionHandler = + new ComparisonFailExceptionHandler { + def handle( + message: String, + obtained: String, + expected: String, + loc: Location + ): Nothing = { + assertions.failComparison(message, obtained, expected, clues)(loc) + } + } +} diff --git a/tests/shared/src/test/scala/munit/BaseFrameworkSuite.scala b/tests/shared/src/main/scala/munit/BaseFrameworkSuite.scala similarity index 100% rename from tests/shared/src/test/scala/munit/BaseFrameworkSuite.scala rename to tests/shared/src/main/scala/munit/BaseFrameworkSuite.scala diff --git a/tests/shared/src/test/scala/munit/BaseSuite.scala b/tests/shared/src/main/scala/munit/BaseSuite.scala similarity index 78% rename from tests/shared/src/test/scala/munit/BaseSuite.scala rename to tests/shared/src/main/scala/munit/BaseSuite.scala index 94f22ee5..ea2289a5 100644 --- a/tests/shared/src/test/scala/munit/BaseSuite.scala +++ b/tests/shared/src/main/scala/munit/BaseSuite.scala @@ -4,15 +4,16 @@ import munit.internal.PlatformCompat class BaseSuite extends FunSuite { + def isDotty: Boolean = + !BuildInfo.scalaVersion.startsWith("2.") + def is213: Boolean = + BuildInfo.scalaVersion.startsWith("2.13") || isDotty + override def munitTestTransforms: List[TestTransform] = super.munitTestTransforms ++ List( new TestTransform( "BaseSuite", { test => - def isDotty: Boolean = - !BuildInfo.scalaVersion.startsWith("2.") - def is213: Boolean = - BuildInfo.scalaVersion.startsWith("2.13") || isDotty if (test.tags(NoDotty) && isDotty) { test.tag(Ignore) } else if (test.tags(Only213) && !is213) { diff --git a/tests/shared/src/main/scala/munit/CustomCompare.scala b/tests/shared/src/main/scala/munit/CustomCompare.scala new file mode 100644 index 00000000..0cd62cdd --- /dev/null +++ b/tests/shared/src/main/scala/munit/CustomCompare.scala @@ -0,0 +1,22 @@ +package munit + +trait CustomCompare[A, B] { + def isEqual(a: A, b: B): Boolean +} + +object CustomCompare { + implicit val optionEquality: CustomCompare[Some[Int], Option[Int]] = + new CustomCompare[Some[Int], Option[Int]] { + def isEqual(a: Some[Int], b: Option[Int]): Boolean = { + if (a.contains(42)) sys.error("boom") + else a == b + } + } + implicit def fromCustomEquality[A, B](implicit + my: CustomCompare[A, B] + ): Compare[A, B] = { + new Compare[A, B] { + def isEqual(a: A, b: B) = my.isEqual(a, b) + } + } +} diff --git a/tests/shared/src/main/scala/munit/StackTraceFrameworkSuite.scala b/tests/shared/src/main/scala/munit/StackTraceFrameworkSuite.scala index f6171a05..a2071c5d 100644 --- a/tests/shared/src/main/scala/munit/StackTraceFrameworkSuite.scala +++ b/tests/shared/src/main/scala/munit/StackTraceFrameworkSuite.scala @@ -28,9 +28,6 @@ object FullStackTraceFrameworkSuite extends BaseStackTraceFrameworkSuite( Array("-F"), """|at munit.Assertions:failComparison - | at munit.Assertions:failComparison$ - | at munit.FunSuite:failComparison - | at munit.Assertions$$anon$1:handle |==> failure munit.StackTraceFrameworkSuite.fail - /scala/munit/StackTraceFrameworkSuite.scala:5 |4: test("fail") { |5: assertNoDiff("a", "b") @@ -47,9 +44,7 @@ object FullStackTraceFrameworkSuite object SmallStackTraceFrameworkSuite extends BaseStackTraceFrameworkSuite( Array(), - """|at munit.FunSuite:assertNoDiff - | at munit.StackTraceFrameworkSuite:$anonfun$new$1 - | at scala.runtime.java8.JFunction0$mcV$sp:apply + """|at munit.Assertions:failComparison |==> failure munit.StackTraceFrameworkSuite.fail - /scala/munit/StackTraceFrameworkSuite.scala:5 |4: test("fail") { |5: assertNoDiff("a", "b") diff --git a/tests/shared/src/test/scala/munit/AssertionsSuite.scala b/tests/shared/src/test/scala/munit/AssertionsSuite.scala index 38a855e0..0f4035c8 100644 --- a/tests/shared/src/test/scala/munit/AssertionsSuite.scala +++ b/tests/shared/src/test/scala/munit/AssertionsSuite.scala @@ -50,12 +50,133 @@ class AssertionsSuite extends BaseSuite { test("subtype".tag(NoDotty)) { assertEquals(Option(1), Some(1)) + assertEquals(Some(1), Option(1)) + assertEquals(Option(1), Option(1)) + } + + test("false-negative") { assertNoDiff( - compileErrors("assertEquals(Some(1), Option(1))"), - """|error: Cannot prove that Option[Int] <:< Some[Int]. - |assertEquals(Some(1), Option(1)) - | ^ - |""".stripMargin + compileErrors("assertEquals(List(1), Vector(1))"), + if (isDotty) + """|error: + |Can't compare these two types: + | First type: List[Int] + | Second type: Vector[Int] + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[List[Int], Vector[Int]] + | Alternative 2: upcast either type into `Any` or a shared supertype. + |I found: + | + | munit.Compare.compareSubtypeWithSupertype[List[Int], Vector[Int]]( + | /* missing */summon[List[Int] <:< Vector[Int]] + | ) + | + |But no implicit values were found that match type List[Int] <:< Vector[Int]. + | + |The following import might make progress towards fixing the problem: + | + | import munit.CustomCompare.fromCustomEquality + | + |assertEquals(List(1), Vector(1)) + | ^ + |""".stripMargin + else + """|error: + |Can't compare these two types: + | First type: List[Int] + | Second type: scala.collection.immutable.Vector[Int] + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[List[Int], scala.collection.immutable.Vector[Int]] + | Alternative 2: upcast either type into `Any` or a shared supertype + |assertEquals(List(1), Vector(1)) + | ^ + |""".stripMargin + ) + } + + test("unrelated") { + assertNoDiff( + compileErrors(""" +class A { + override def equals(x: Any): Boolean = true +} +class B { + override def equals(x: Any): Boolean = true +} +assertEquals(new A, new B) + """), + if (isDotty) + """|error: + |Can't compare these two types: + | First type: A + | Second type: B + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[A, B] + | Alternative 2: upcast either type into `Any` or a shared supertype. + |I found: + | + | munit.Compare.compareSubtypeWithSupertype[A, B](/* missing */summon[A <:< B]) + | + |But no implicit values were found that match type A <:< B. + | + |The following import might make progress towards fixing the problem: + | + | import munit.CustomCompare.fromCustomEquality + | + |assertEquals(new A, new B) + | ^ + |""".stripMargin + else + """|error: + |Can't compare these two types: + | First type: A + | Second type: B + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[A, B] + | Alternative 2: upcast either type into `Any` or a shared supertype + |assertEquals(new A, new B) + | ^ + |""".stripMargin + ) + } + + test("char-int-nok") { + assertNoDiff( + compileErrors("assertEquals('a', 'a'.toInt)"), + if (isDotty) + """|error: + |Can't compare these two types: + | First type: Char + | Second type: Int + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[Char, Int] + | Alternative 2: upcast either type into `Any` or a shared supertype. + |I found: + | + | munit.Compare.compareSubtypeWithSupertype[Char, Int]( + | /* missing */summon[Char <:< Int] + | ) + | + |But no implicit values were found that match type Char <:< Int. + | + |The following import might make progress towards fixing the problem: + | + | import munit.CustomCompare.fromCustomEquality + | + |assertEquals('a', 'a'.toInt) + | ^ + |""".stripMargin + else + """|error: + |Can't compare these two types: + | First type: Char + | Second type: Int + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[Char, Int] + | Alternative 2: upcast either type into `Any` or a shared supertype + |assertEquals('a', 'a'.toInt) + | ^ + |""".stripMargin ) } test("array-sameElements") { @@ -68,4 +189,45 @@ class AssertionsSuite extends BaseSuite { ) ) } + + test("some-none-nokj") { + assertNoDiff( + compileErrors("assertEquals(None, Some(1))"), + if (isDotty) + """|error: + |Can't compare these two types: + | First type: None.type + | Second type: Some[Int] + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[None.type, Some[Int]] + | Alternative 2: upcast either type into `Any` or a shared supertype. + |I found: + | + | munit.Compare.compareSubtypeWithSupertype[None.type, Some[Int]]( + | /* missing */summon[None.type <:< Some[Int]] + | ) + | + |But no implicit values were found that match type None.type <:< Some[Int]. + | + |The following import might make progress towards fixing the problem: + | + | import munit.CustomCompare.fromCustomEquality + | + |assertEquals(None, Some(1)) + | ^ + |""".stripMargin + else + """|error: + |Can't compare these two types: + | First type: None.type + | Second type: Some[Int] + |Possible ways to fix this error: + | Alternative 1: provide an implicit instance for Compare[None.type, Some[Int]] + | Alternative 2: upcast either type into `Any` or a shared supertype + |assertEquals(None, Some(1)) + | ^ + |""".stripMargin + ) + } + } diff --git a/tests/shared/src/test/scala/munit/ComparisonFailExceptionSuite.scala b/tests/shared/src/test/scala/munit/ComparisonFailExceptionSuite.scala index 36c1e697..b929bf41 100644 --- a/tests/shared/src/test/scala/munit/ComparisonFailExceptionSuite.scala +++ b/tests/shared/src/test/scala/munit/ComparisonFailExceptionSuite.scala @@ -15,6 +15,7 @@ class ComparisonFailExceptionSuite extends BaseSuite { assertEquals[Any, Any](List("1", "2", "3"), List(1, 2)) } assert(clue(e).isInstanceOf[ComparisonFailure]) + assert(clue(e).isInstanceOf[Serializable]) // NOTE: assert that we use the `toString` of values in the // `org.junit.ComparisionFailure` exception. The stdout message in the // console still uses `munitPrint()`, which would have displayed `List("1", @@ -60,10 +61,10 @@ class ComparisonFailExceptionSuite extends BaseSuite { } assertNoDiff( e.getMessage(), - """|ComparisonFailExceptionSuite.scala:59 - |58: val e = intercept[ComparisonFailException] { - |59: assertNoDiff("", "Lorem ipsum") - |60: } + """|ComparisonFailExceptionSuite.scala:60 + |59: val e = intercept[ComparisonFailException] { + |60: assertNoDiff("", "Lorem ipsum") + |61: } |Obtained empty output! |=> Expected: |Lorem ipsum diff --git a/tests/shared/src/test/scala/munit/CustomCompareSuite.scala b/tests/shared/src/test/scala/munit/CustomCompareSuite.scala new file mode 100644 index 00000000..3e7bf43b --- /dev/null +++ b/tests/shared/src/test/scala/munit/CustomCompareSuite.scala @@ -0,0 +1,21 @@ +package munit + +class CustomCompareSuite extends BaseSuite { + import CustomCompare.fromCustomEquality + test("ok") { + assertEquals(Some(1), Option(1)) + } + + test("boom") { + interceptMessage[RuntimeException]("boom")( + assertEquals(Some(42), Option(42)) + ) + } + + test("fallback-to-default") { + // NOTE: Users who rely on custom equality won't get a compile error for + // comparisons between supertype/subtype relationships. They only get a + // compile error when comparing unrelated types (same as default behavior). + assertEquals(List(1), collection.Seq(1)) + } +} diff --git a/tests/shared/src/test/scala/munit/FailExceptionSuite.scala b/tests/shared/src/test/scala/munit/FailExceptionSuite.scala index a6c2c2af..0fcfcfae 100644 --- a/tests/shared/src/test/scala/munit/FailExceptionSuite.scala +++ b/tests/shared/src/test/scala/munit/FailExceptionSuite.scala @@ -2,8 +2,9 @@ package munit class FailExceptionSuite extends BaseSuite { test("assertion-error") { - intercept[AssertionError] { + val e = intercept[AssertionError] { fail("hello world!") } + assert(clue(e).isInstanceOf[Serializable]) } }