diff --git a/munit/shared/src/main/scala-2/munit/internal/MacroCompat.scala b/munit/shared/src/main/scala-2/munit/internal/MacroCompat.scala index 1c15595a..594abdb7 100644 --- a/munit/shared/src/main/scala-2/munit/internal/MacroCompat.scala +++ b/munit/shared/src/main/scala-2/munit/internal/MacroCompat.scala @@ -1,6 +1,6 @@ package munit.internal -import munit.diff.Clue +import munit.Clue import munit.Location import scala.language.experimental.macros import scala.reflect.macros.blackbox.Context diff --git a/munit/shared/src/main/scala-3/munit/internal/MacroCompat.scala b/munit/shared/src/main/scala-3/munit/internal/MacroCompat.scala index a4482204..1e5b3ce3 100644 --- a/munit/shared/src/main/scala-3/munit/internal/MacroCompat.scala +++ b/munit/shared/src/main/scala-3/munit/internal/MacroCompat.scala @@ -1,6 +1,6 @@ package munit.internal -import munit.diff.Clue +import munit.Clue import munit.Location import scala.quoted._ import scala.language.experimental.macros diff --git a/munit/shared/src/main/scala/munit/Assertions.scala b/munit/shared/src/main/scala/munit/Assertions.scala index 970d151f..19a5201e 100644 --- a/munit/shared/src/main/scala/munit/Assertions.scala +++ b/munit/shared/src/main/scala/munit/Assertions.scala @@ -1,10 +1,10 @@ package munit import munit.internal.console.{Lines, StackTraces} -import munit.diff.console.Printers +import munit.internal.console.Printers import munit.diff.Printer -import munit.diff.Clue -import munit.diff.Clues +import munit.Clue +import munit.Clues import munit.diff.EmptyPrinter import scala.reflect.ClassTag diff --git a/munit/shared/src/main/scala/munit/Clue.scala b/munit/shared/src/main/scala/munit/Clue.scala index d81e322e..028c1aba 100644 --- a/munit/shared/src/main/scala/munit/Clue.scala +++ b/munit/shared/src/main/scala/munit/Clue.scala @@ -2,16 +2,16 @@ package munit import munit.internal.MacroCompat -// class Clue[+T]( -// val source: String, -// val value: T, -// val valueType: String -// ) extends Serializable { -// override def toString(): String = s"Clue($source, $value)" -// } +class Clue[+T]( + val source: String, + val value: T, + val valueType: String +) extends Serializable { + override def toString(): String = s"Clue($source, $value)" +} object Clue extends MacroCompat.ClueMacro { @deprecated("use fromValue instead", "1.0.0") - def empty[T](value: T): munit.diff.Clue[T] = fromValue(value) - def fromValue[T](value: T): munit.diff.Clue[T] = - new munit.diff.Clue("", value, "") + 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 ae64576a..8659a06a 100644 --- a/munit/shared/src/main/scala/munit/Clues.scala +++ b/munit/shared/src/main/scala/munit/Clues.scala @@ -2,10 +2,10 @@ package munit 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))) -// } +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/MUnitRunner.scala b/munit/shared/src/main/scala/munit/MUnitRunner.scala index 47fa8ec6..348d79b3 100644 --- a/munit/shared/src/main/scala/munit/MUnitRunner.scala +++ b/munit/shared/src/main/scala/munit/MUnitRunner.scala @@ -80,7 +80,7 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) descriptions.getOrElseUpdate( test, { val escapedName = Printers.escapeNonVisible(test.name) - val testName = munit.diff.internal.Compat.LazyList + val testName = munit.internal.Compat.LazyList .from(0) .map { case 0 => escapedName diff --git a/munit/shared/src/main/scala/munit/internal/MacroCompatScala2.scala b/munit/shared/src/main/scala/munit/internal/MacroCompatScala2.scala index 05f171b5..90c298db 100644 --- a/munit/shared/src/main/scala/munit/internal/MacroCompatScala2.scala +++ b/munit/shared/src/main/scala/munit/internal/MacroCompatScala2.scala @@ -1,6 +1,6 @@ package munit.internal -import munit.diff.Clue +import munit.Clue import munit.Location import scala.reflect.macros.blackbox.Context import scala.reflect.macros.TypecheckException diff --git a/munit/shared/src/main/scala/munit/internal/console/Lines.scala b/munit/shared/src/main/scala/munit/internal/console/Lines.scala index 5dc3ca84..440a55dc 100644 --- a/munit/shared/src/main/scala/munit/internal/console/Lines.scala +++ b/munit/shared/src/main/scala/munit/internal/console/Lines.scala @@ -6,7 +6,7 @@ import munit.Location import scala.collection.JavaConverters._ import scala.collection.mutable import scala.util.control.NonFatal -import munit.diff.Clues +import munit.Clues class Lines extends Serializable { private val filecache = mutable.Map.empty[Path, Array[String]] diff --git a/munit/shared/src/main/scala/munit/internal/console/Printers.scala b/munit/shared/src/main/scala/munit/internal/console/Printers.scala index 7b382bcb..b952de6c 100644 --- a/munit/shared/src/main/scala/munit/internal/console/Printers.scala +++ b/munit/shared/src/main/scala/munit/internal/console/Printers.scala @@ -5,17 +5,195 @@ import munit.Location import munit.diff.Printer import munit.diff.EmptyPrinter import munit.diff.console.{Printers => DiffPrinters} +import munit.Clues +import munit.Printable +import munit.internal.Compat object Printers { + + import DiffPrinters._ + def log(any: Any, printer: Printer = EmptyPrinter)(implicit loc: Location ): Unit = { println(loc.toString) - println(DiffPrinters.print(any, printer)) + println(print(any, printer)) + } + + /** Pretty-prints the value in a format that's optimized for producing diffs */ + def print(any: Any, printer: Printer = EmptyPrinter): String = { + var height = printer.height + val out = new StringBuilder() + val indentStep = 2 + def loop(a: Any, indent: Int): Unit = { + height -= 1 + if (height < 0) { + out.append("...") + return + } + val nextIndent = indent + indentStep + val isDone = printer.print(a, out, indent) + if (!isDone) { + a match { + case null => out.append("null") + case x: Printable => x.print(out, indent) + case x: Char => + out.append('\'') + if (x == '\'') out.append("\\'") + else printChar(x, out) + out.append('\'') + case x: Byte => out.append(x.toString()) + case x: Short => out.append(x.toString()) + case x: Int => out.append(x.toString()) + case x: Long => out.append(x.toString()) + case x: Float => out.append(x.toString()) + case x: Double => out.append(x.toString()) + case x: String => printString(x, out, printer) + case x: Clues => + printApply( + "Clues", + x.values.iterator, + out, + indent, + nextIndent, + open = " {", + close = "}", + comma = "" + ) { clue => + if (clue.source.nonEmpty) { + out.append(clue.source) + } + if (clue.valueType.nonEmpty) { + out.append(": ").append(clue.valueType) + } + out.append(" = ") + loop(clue.value, nextIndent) + } + case None => + out.append("None") + case Nil => + out.append("Nil") + case x: Map[_, _] => + printApply( + Compat.collectionClassName(x), + x.iterator, + out, + indent, + nextIndent + ) { case (key, value) => + loop(key, nextIndent) + out.append(" -> ") + loop(value, nextIndent) + } + case x: Iterable[_] => + printApply( + Compat.collectionClassName(x), + x.iterator, + out, + indent, + nextIndent + ) { value => loop(value, nextIndent) } + case x: Array[_] => + printApply( + "Array", + x.iterator, + out, + indent, + nextIndent + ) { value => loop(value, nextIndent) } + case it: Iterator[_] => + if (it.isEmpty) out.append("empty iterator") + else out.append("non-empty iterator") + case p: Product => + val elementNames = Compat.productElementNames(p) + val infiniteElementNames = Iterator.continually { + if (elementNames.hasNext) elementNames.next() + else "" + } + printApply( + p.productPrefix, + p.productIterator.zip(infiniteElementNames), + out, + indent, + nextIndent + ) { case (value, key) => + if (key.nonEmpty) { + out.append(key).append(" = ") + } + loop(value, nextIndent) + } + case _ => + out.append(a.toString()) + } + } + } + loop(any, indent = 0) + munit.diff.console.AnsiColors.filterAnsi(out.toString()) + } + + private def printApply[T]( + prefix: String, + it: Iterator[T], + out: StringBuilder, + indent: Int, + nextIndent: Int, + open: String = "(", + close: String = ")", + comma: String = "," + )(fn: T => Unit): Unit = { + out.append(prefix) + out.append(open) + if (it.hasNext) { + printNewline(out, nextIndent) + while (it.hasNext) { + val value = it.next() + fn(value) + if (it.hasNext) { + out.append(comma) + printNewline(out, nextIndent) + } else { + printNewline(out, indent) + } + } + } + out.append(close) + } + + private def printNewline(out: StringBuilder, indent: Int): Unit = { + out.append("\n") + var i = 0 + while (i < indent) { + out.append(' ') + i += 1 + } + } + + /** + * Pretty-prints this string with non-visible characters escaped. + * + * The exact definition of "non-visible" is fuzzy and is subject to change. + * The original motivation for this method was to fix + * https://github.com/scalameta/munit/issues/258 related to escaping \r in + * test names. + * + * The spirit of this method is to preserve "visible" characters like emojis + * and double quotes and escape "non-visible" characters like newlines and + * ANSI escape codes. A non-goal of this method is to make the output + * copy-pasteable back into source code unlike the `printChar` method, which + * escapes for example double-quote characters. + */ + def escapeNonVisible(string: String): String = { + val out = new StringBuilder() + var i = 0 + while (i < string.length()) { + val ch = string.charAt(i) + ch match { + case '"' | '\'' => out.append(ch) + case _ => printChar(ch, out, isEscapeUnicode = false) + } + i += 1 + } + out.toString() } - def print(any: Any, printer: Printer = EmptyPrinter): String = - DiffPrinters.print(any, printer) - def escapeNonVisible(string: String): String = - DiffPrinters.escapeNonVisible(string) } diff --git a/scala-diff/shared/src/main/scala-2.13/munit/diff/internal/Compat.scala b/scala-diff/shared/src/main/scala-2.13/munit/diff/internal/Compat.scala deleted file mode 100644 index e110dc12..00000000 --- a/scala-diff/shared/src/main/scala-2.13/munit/diff/internal/Compat.scala +++ /dev/null @@ -1,10 +0,0 @@ -package munit.diff.internal - -object Compat { - type LazyList[+T] = scala.LazyList[T] - val LazyList = scala.LazyList - def productElementNames(p: Product): Iterator[String] = - p.productElementNames - def collectionClassName(i: Iterable[_]): String = - i.asInstanceOf[{ def collectionClassName: String }].collectionClassName -} diff --git a/scala-diff/shared/src/main/scala-3/munit/diff/internal/Compat.scala b/scala-diff/shared/src/main/scala-3/munit/diff/internal/Compat.scala deleted file mode 100644 index 26a438d2..00000000 --- a/scala-diff/shared/src/main/scala-3/munit/diff/internal/Compat.scala +++ /dev/null @@ -1,13 +0,0 @@ -package munit.diff.internal - -import scala.reflect.Selectable.reflectiveSelectable - -object Compat { - type LazyList[+T] = scala.LazyList[T] - val LazyList = scala.LazyList - def productElementNames(p: Product): Iterator[String] = - p.productElementNames - def collectionClassName(i: Iterable[_]): String = { - i.asInstanceOf[{ def collectionClassName: String }].collectionClassName - } -} diff --git a/scala-diff/shared/src/main/scala-pre-2.13/munit/diff/internal/Compat.scala b/scala-diff/shared/src/main/scala-pre-2.13/munit/diff/internal/Compat.scala deleted file mode 100644 index 06d54f3d..00000000 --- a/scala-diff/shared/src/main/scala-pre-2.13/munit/diff/internal/Compat.scala +++ /dev/null @@ -1,10 +0,0 @@ -package munit.diff.internal - -object Compat { - type LazyList[+T] = Stream[T] - val LazyList = scala.Stream - def productElementNames(p: Product): Iterator[String] = - Iterator.continually("") - def collectionClassName(i: Iterable[_]): String = - i.stringPrefix -} diff --git a/scala-diff/shared/src/main/scala/munit/diff/Clue.scala b/scala-diff/shared/src/main/scala/munit/diff/Clue.scala deleted file mode 100644 index 9f6702d3..00000000 --- a/scala-diff/shared/src/main/scala/munit/diff/Clue.scala +++ /dev/null @@ -1,13 +0,0 @@ -package munit.diff - -class Clue[+T]( - val source: String, - val value: T, - val valueType: String -) extends Serializable { - override def toString(): String = s"Clue($source, $value)" -} - -object Clue { - def fromValue[T](value: T): Clue[T] = new Clue("", value, "") -} diff --git a/scala-diff/shared/src/main/scala/munit/diff/Clues.scala b/scala-diff/shared/src/main/scala/munit/diff/Clues.scala deleted file mode 100644 index 9616c21d..00000000 --- a/scala-diff/shared/src/main/scala/munit/diff/Clues.scala +++ /dev/null @@ -1,11 +0,0 @@ -package munit.diff - -import munit.diff.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/scala-diff/shared/src/main/scala/munit/diff/Printable.scala b/scala-diff/shared/src/main/scala/munit/diff/Printable.scala deleted file mode 100644 index 44ceb06b..00000000 --- a/scala-diff/shared/src/main/scala/munit/diff/Printable.scala +++ /dev/null @@ -1,8 +0,0 @@ -package munit.diff - -/** - * Override this class to customize the default pretty-printer. - */ -trait Printable { - def print(out: StringBuilder, indent: Int): Unit -} diff --git a/scala-diff/shared/src/main/scala/munit/diff/console/Printers.scala b/scala-diff/shared/src/main/scala/munit/diff/console/Printers.scala index b6235712..4e9c9ed6 100644 --- a/scala-diff/shared/src/main/scala/munit/diff/console/Printers.scala +++ b/scala-diff/shared/src/main/scala/munit/diff/console/Printers.scala @@ -1,163 +1,18 @@ -// Adaptation of https://github.com/lihaoyi/PPrint/blob/e6a918c259ed7ae1998bbf58c360334a3f0157ca/pprint/src/pprint/Walker.scala package munit.diff.console -import munit.diff.{EmptyPrinter, Printable, Printer} +import munit.diff.{EmptyPrinter, Printer} import scala.annotation.switch -import munit.diff.Clues -import munit.diff.internal.Compat object Printers { - /** Pretty-prints the value in a format that's optimized for producing diffs */ - def print(any: Any, printer: Printer = EmptyPrinter): String = { - var height = printer.height + def print(input: String): String = { val out = new StringBuilder() - val indentStep = 2 - def loop(a: Any, indent: Int): Unit = { - height -= 1 - if (height < 0) { - out.append("...") - return - } - val nextIndent = indent + indentStep - val isDone = printer.print(a, out, indent) - if (!isDone) { - a match { - case null => out.append("null") - case x: Printable => x.print(out, indent) - case x: Char => - out.append('\'') - if (x == '\'') out.append("\\'") - else printChar(x, out) - out.append('\'') - case x: Byte => out.append(x.toString()) - case x: Short => out.append(x.toString()) - case x: Int => out.append(x.toString()) - case x: Long => out.append(x.toString()) - case x: Float => out.append(x.toString()) - case x: Double => out.append(x.toString()) - case x: String => printString(x, out, printer) - case x: Clues => - printApply( - "Clues", - x.values.iterator, - out, - indent, - nextIndent, - open = " {", - close = "}", - comma = "" - ) { clue => - if (clue.source.nonEmpty) { - out.append(clue.source) - } - if (clue.valueType.nonEmpty) { - out.append(": ").append(clue.valueType) - } - out.append(" = ") - loop(clue.value, nextIndent) - } - case None => - out.append("None") - case Nil => - out.append("Nil") - case x: Map[_, _] => - printApply( - Compat.collectionClassName(x), - x.iterator, - out, - indent, - nextIndent - ) { case (key, value) => - loop(key, nextIndent) - out.append(" -> ") - loop(value, nextIndent) - } - case x: Iterable[_] => - printApply( - Compat.collectionClassName(x), - x.iterator, - out, - indent, - nextIndent - ) { value => loop(value, nextIndent) } - case x: Array[_] => - printApply( - "Array", - x.iterator, - out, - indent, - nextIndent - ) { value => loop(value, nextIndent) } - case it: Iterator[_] => - if (it.isEmpty) out.append("empty iterator") - else out.append("non-empty iterator") - case p: Product => - val elementNames = Compat.productElementNames(p) - val infiniteElementNames = Iterator.continually { - if (elementNames.hasNext) elementNames.next() - else "" - } - printApply( - p.productPrefix, - p.productIterator.zip(infiniteElementNames), - out, - indent, - nextIndent - ) { case (value, key) => - if (key.nonEmpty) { - out.append(key).append(" = ") - } - loop(value, nextIndent) - } - case _ => - out.append(a.toString()) - } - } - } - loop(any, indent = 0) + printString(input, out, EmptyPrinter) munit.diff.console.AnsiColors.filterAnsi(out.toString()) } - private def printApply[T]( - prefix: String, - it: Iterator[T], - out: StringBuilder, - indent: Int, - nextIndent: Int, - open: String = "(", - close: String = ")", - comma: String = "," - )(fn: T => Unit): Unit = { - out.append(prefix) - out.append(open) - if (it.hasNext) { - printNewline(out, nextIndent) - while (it.hasNext) { - val value = it.next() - fn(value) - if (it.hasNext) { - out.append(comma) - printNewline(out, nextIndent) - } else { - printNewline(out, indent) - } - } - } - out.append(close) - } - - private def printNewline(out: StringBuilder, indent: Int): Unit = { - out.append("\n") - var i = 0 - while (i < indent) { - out.append(' ') - i += 1 - } - } - - private def printString( + def printString( string: String, out: StringBuilder, printer: Printer @@ -182,35 +37,7 @@ object Printers { } } - /** - * Pretty-prints this string with non-visible characters escaped. - * - * The exact definition of "non-visible" is fuzzy and is subject to change. - * The original motivation for this method was to fix - * https://github.com/scalameta/munit/issues/258 related to escaping \r in - * test names. - * - * The spirit of this method is to preserve "visible" characters like emojis - * and double quotes and escape "non-visible" characters like newlines and - * ANSI escape codes. A non-goal of this method is to make the output - * copy-pasteable back into source code unlike the `printChar` method, which - * escapes for example double-quote characters. - */ - def escapeNonVisible(string: String): String = { - val out = new StringBuilder() - var i = 0 - while (i < string.length()) { - val ch = string.charAt(i) - ch match { - case '"' | '\'' => out.append(ch) - case _ => printChar(ch, out, isEscapeUnicode = false) - } - i += 1 - } - out.toString() - } - - private def printChar( + def printChar( c: Char, sb: StringBuilder, isEscapeUnicode: Boolean = true @@ -229,5 +56,4 @@ object Printers { sb.append("\\u%04x".format(c.toInt)) else sb.append(c) } - } diff --git a/tests/shared/src/test/scala/munit/TypeCheckSuite.scala b/tests/shared/src/test/scala/munit/TypeCheckSuite.scala index 7bece05e..3b92ca84 100644 --- a/tests/shared/src/test/scala/munit/TypeCheckSuite.scala +++ b/tests/shared/src/test/scala/munit/TypeCheckSuite.scala @@ -41,7 +41,7 @@ class TypeCheckSuite extends FunSuite { | |The following import might make progress towards fixing the problem: | - | import munit.diff.Clue.generate + | import munit.Clue.generate | |msg.foobar | ^ @@ -84,7 +84,7 @@ class TypeCheckSuite extends FunSuite { | |The following import might make progress towards fixing the problem: | - | import munit.diff.Clue.generate + | import munit.Clue.generate | |val n: Int = msg | ^