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

feat(printer): mk printers more easily configurable #640

Merged
merged 8 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion munit/shared/src/main/scala/munit/Assertions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,12 @@ trait Assertions extends MacroCompat.CompileErrorMacro {
}
def clues(clue: Clue[_]*): Clues = new Clues(clue.toList)

def printer: Printer = EmptyPrinter

def munitPrint(clue: => Any): String = {
clue match {
case message: String => message
case value => Printers.print(value)
case value => Printers.print(value, printer)
}
}

Expand Down
78 changes: 78 additions & 0 deletions munit/shared/src/main/scala/munit/Printer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,84 @@ trait Printer {
def height: Int = 100
def isMultiline(string: String): Boolean =
string.contains('\n')

/**
* Combine two printers into a single printer.
*
* Order is important : this printer will be tried first, then the other printer.
* The new Printer's height will be the max of the two printers' heights.
*
* Comibining two printers can be useful if you want to customize Printer for
* some types somewhere, but want to add more specialized printers for some tests
*
* {{{
* trait MySuites extends FunSuite {
* override val printer = Printer.apply {
* case long => s"${l}L"
* }
* }
*
* trait SomeOtherSuites extends MySuites {
* override val printer = Printer.apply {
* case m: SomeCaseClass => m.someCustomToString
* } orElse super.printer
* }
*
* }}}
*/
def orElse(other: Printer): Printer = {
val h = this.height
val p: (Any, StringBuilder, Int) => Boolean = this.print
new Printer {
def print(value: Any, out: StringBuilder, indent: Int): Boolean =
p.apply(value, out, indent) || other.print(
value,
out,
indent
)
override def height: Int = h.max(other.height)
}
}

}

object Printer {

def apply(
height: Int
)(partialPrint: PartialFunction[Any, String]): Printer = {
val h = height
new Printer {
def print(value: Any, out: StringBuilder, indent: Int): Boolean =
value match {
case simpleValue =>
partialPrint.lift.apply(simpleValue).fold(false) { string =>
out.append(string)
true
}
}
wahtique marked this conversation as resolved.
Show resolved Hide resolved

override def height: Int = h
}
}

/**
* Utiliy constructor defining a printer for some types.
*
* This might be useful for some types which default pretty-printers
* do not output helpful diffs.
*
* {{{
* type ByteArray = Array[Byte]
* val listPrinter = Printer.apply {
* case ll: ByteArray => ll.map(String.format("%02x", b)).mkString(" ")
* }
* val bytes = Array[Byte](1, 5, 8, 24)
* Printers.print(bytes, listPrinter) // "01 05 08 18"
* }}}
*/
def apply(partialPrint: PartialFunction[Any, String]): Printer =
apply(100)(partialPrint)
wahtique marked this conversation as resolved.
Show resolved Hide resolved
}

/** Default printer that does not customize the pretty-printer */
Expand Down
141 changes: 141 additions & 0 deletions tests/shared/src/test/scala/munit/CustomPrinterSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package munit

import org.scalacheck.Prop.forAll
import munit.internal.console.Printers
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Prop

class CustomPrinterSuite extends FunSuite with ScalaCheckSuite {

private case class Foo(i: Int)

private case class Bar(l: List[Int])

private case class FooBar(foo: Foo, bar: Bar)

private val genFoo = arbitrary[Int].map(Foo(_))

// limit size to 10 to have a reasonable number of values
private val genBar = arbitrary[List[Int]].map(l => Bar(l.take(10)))

private val genFooBar = for {
foo <- genFoo
bar <- genBar
} yield FooBar(foo, bar)

private val longPrinter: Printer = Printer.apply { case l: Long =>
s"MoreThanInt($l)"
}

private val fooPrinter: Printer = Printer.apply { case Foo(i) =>
s"Foo(INT($i))"
}

private val listPrinter: Printer = Printer.apply { case l: List[_] =>
l.mkString("[", ",", "]")
}

private val intPrinter: Printer = Printer.apply { case i: Int =>
s"NotNaN($i)"
}

private val isScala213: Boolean = BuildInfo.scalaVersion.startsWith("2.13")

private def checkProp(
options: TestOptions,
isEnabled: Boolean = true
)(p: => Prop): Unit = {
test(options) {
assume(isEnabled, "disabled test")
p
}
}

checkProp("long") {
forAll(arbitrary[Long]) { (l: Long) =>
val obtained = Printers.print(l, longPrinter)
val expected = s"MoreThanInt($l)"
assertEquals(obtained, expected)
}
}

checkProp("list") {
forAll(arbitrary[List[Int]]) { l =>
val obtained = Printers.print(l, listPrinter)
val expected = l.mkString("[", ",", "]")
assertEquals(obtained, expected)
}
}

checkProp("product") {
forAll(genFoo) { foo =>
val obtained = Printers.print(foo, fooPrinter)
val expected = s"Foo(INT(${foo.i}))"
assertEquals(obtained, expected)
}
}

checkProp("int in product", isEnabled = isScala213) {
forAll(genFoo) { foo =>
val obtained = Printers.print(foo, intPrinter)
val expected = s"Foo(\n i = NotNaN(${foo.i})\n)"
assertEquals(obtained, expected)
}
}

checkProp("list in product", isEnabled = isScala213) {
forAll(genBar) { bar =>
val obtained = Printers.print(bar, listPrinter)
val expected = s"Bar(\n l = ${bar.l.mkString("[", ",", "]")}\n)"
assertEquals(obtained, expected)
}
}

checkProp("list and int in product", isEnabled = isScala213) {
forAll(genFooBar) { foobar =>
val obtained = Printers
.print(foobar, listPrinter.orElse(intPrinter))
.stripMargin
.stripLeading()
wahtique marked this conversation as resolved.
Show resolved Hide resolved
.stripTrailing()
val expected =
s"""|FooBar(
| foo = Foo(
| i = NotNaN(${foobar.foo.i})
| ),
| bar = Bar(
| l = ${foobar.bar.l.mkString("[", ",", "]")}
| )
|)
|""".stripMargin.stripLeading().stripTrailing()
assertEquals(obtained, expected)
}
}

checkProp("all ints in product", isEnabled = isScala213) {
forAll(genFooBar) { foobar =>
val obtained = Printers
.print(foobar, intPrinter)
.filterNot(_.isWhitespace)

val expectedbBarList = foobar.bar.l match {
case Nil => "Nil"
case l =>
l.map(i => s"NotNaN($i)").mkString("List(", ",", ")")
}

val expected =
s"""|FooBar(
| foo = Foo(
| i = NotNaN(${foobar.foo.i})
| ),
| bar = Bar(
| l = $expectedbBarList
| )
|)
|""".stripMargin.filterNot(_.isWhitespace)
assertEquals(obtained, expected)
}
}

}