diff --git a/munit/shared/src/main/scala/munit/MUnitRunner.scala b/munit/shared/src/main/scala/munit/MUnitRunner.scala index e6a3f9c1..82987419 100644 --- a/munit/shared/src/main/scala/munit/MUnitRunner.scala +++ b/munit/shared/src/main/scala/munit/MUnitRunner.scala @@ -24,6 +24,7 @@ import scala.concurrent.Future import scala.concurrent.ExecutionContext import java.util.concurrent.ExecutionException import munit.internal.junitinterface.Settings +import munit.internal.console.Printers class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) extends Runner @@ -62,7 +63,7 @@ class MUnitRunner(val cls: Class[_ <: Suite], newInstance: () => Suite) def createTestDescription(test: suite.Test): Description = { descriptions.getOrElseUpdate( test, { - val escapedName = test.name.replace("\n", "\\n") + val escapedName = Printers.escapeNonVisible(test.name) val testName = munit.internal.Compat.LazyList .from(0) .map { 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 3196f42e..3a867c2f 100644 --- a/munit/shared/src/main/scala/munit/internal/console/Printers.scala +++ b/munit/shared/src/main/scala/munit/internal/console/Printers.scala @@ -188,7 +188,39 @@ object Printers { } } - private def printChar(c: Char, sb: StringBuilder) = + /** + * 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: @switch) match { + case '"' | '\'' => out.append(ch) + case _ => printChar(ch, out, isEscapeUnicode = false) + } + i += 1 + } + out.toString() + } + + private def printChar( + c: Char, + sb: StringBuilder, + isEscapeUnicode: Boolean = true + ): Unit = (c: @switch) match { case '"' => sb.append("\\\"") case '\\' => sb.append("\\\\") @@ -198,9 +230,9 @@ object Printers { case '\r' => sb.append("\\r") case '\t' => sb.append("\\t") case c => - val isUnicode = false - if (c < ' ' || (c > '~' && isUnicode)) - sb.append("\\u%04x" format c.toInt) + val isNonReadableAscii = c < ' ' || (c > '~' && isEscapeUnicode) + if (isNonReadableAscii && !Character.isLetter(c)) + sb.append("\\u%04x".format(c.toInt)) else sb.append(c) } diff --git a/tests/shared/src/main/scala/munit/TestNameFrameworkSuite.scala b/tests/shared/src/main/scala/munit/TestNameFrameworkSuite.scala index b09eaea7..931e83eb 100644 --- a/tests/shared/src/main/scala/munit/TestNameFrameworkSuite.scala +++ b/tests/shared/src/main/scala/munit/TestNameFrameworkSuite.scala @@ -1,22 +1,30 @@ package munit class TestNameFrameworkSuite extends FunSuite { - test("basic") { - // pass - } - test("basic") { - // pass - } - test("newline\n") { - // pass - } + test("basic") {} + test("basic") {} + test("newline\n") {} + test("carriage-return\r\n") {} + test("tab\t") {} + test("return\t") {} + test("form-feed\f") {} + test("substitute\u001az") {} + test("emoji😆") {} + test("red" + Console.RED) {} } object TestNameFrameworkSuite extends FrameworkTest( classOf[TestNameFrameworkSuite], - """|==> success munit.TestNameFrameworkSuite.basic - |==> success munit.TestNameFrameworkSuite.basic-1 - |==> success munit.TestNameFrameworkSuite.newline\n - |""".stripMargin + s"""|==> success munit.TestNameFrameworkSuite.basic + |==> success munit.TestNameFrameworkSuite.basic-1 + |==> success munit.TestNameFrameworkSuite.newline\\n + |==> success munit.TestNameFrameworkSuite.carriage-return\\r\\n + |==> success munit.TestNameFrameworkSuite.tab\\t + |==> success munit.TestNameFrameworkSuite.return\\t + |==> success munit.TestNameFrameworkSuite.form-feed\\f + |==> success munit.TestNameFrameworkSuite.substitute\\u001az + |==> success munit.TestNameFrameworkSuite.emoji😆 + |==> success munit.TestNameFrameworkSuite.red\\u001b[31m + |""".stripMargin )