Skip to content

Commit

Permalink
DiffOptions: allow controlling use of ANSI markers
Browse files Browse the repository at this point in the history
Also, exclude `munit.internal.` from MIMA.
  • Loading branch information
kitbellew committed Jan 21, 2025
1 parent 86fff35 commit 80f980b
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 63 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import scala.collection.mutable

import sbtcrossproject.CrossPlugin.autoImport.CrossType
import sbtcrossproject.CrossPlugin.autoImport.crossProject
def previousVersion = "1.0.0-RC1"

Expand Down Expand Up @@ -59,7 +58,8 @@ def isScala3(v: Option[(Long, Long)]): Boolean = v.exists(_._1 == 3)
lazy val skipIdeaSettings = SettingKey[Boolean]("ide-skip-project")
.withRank(KeyRanks.Invisible) := true
lazy val mimaEnable: List[Def.Setting[_]] = List(
mimaBinaryIssueFilters ++= List.empty,
mimaBinaryIssueFilters +=
_root_.munit.build.Mima.languageAgnosticCompatibilityPolicy,
mimaPreviousArtifacts := {
if (crossPaths.value)
Set("org.scalameta" %% moduleName.value % previousVersion)
Expand Down
45 changes: 25 additions & 20 deletions munit-diff/shared/src/main/scala/munit/diff/Diff.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,34 @@ class Diff(val obtained: String, val expected: String, val options: DiffOptions)
title: String,
printObtainedAsStripMargin: Boolean = true,
): String = {
val sb = new StringBuilder
implicit val sb: StringBuilder = new StringBuilder
if (title.nonEmpty) sb.append(title).append("\n")
if (obtainedClean.length < 1000) {
header("Obtained", sb).append("\n")
if (printObtainedAsStripMargin) sb.append(asStripMargin(obtainedClean))
else sb.append(obtainedClean)
header("Obtained")
sb.append("\n")
sb.append(asStripMargin(obtainedClean, printObtainedAsStripMargin))
sb.append("\n")
}
appendDiffOnlyReport(sb)
appendDiffOnlyReport
sb.toString()
}

def createReport(title: String): String =
createReport(title, options.obtainedAsStripMargin)

def createDiffOnlyReport(): String = {
val out = new StringBuilder
appendDiffOnlyReport(out)
implicit val out: StringBuilder = new StringBuilder
appendDiffOnlyReport
out.toString()
}

private def appendDiffOnlyReport(sb: StringBuilder): Unit = {
header("Diff", sb)
val red = AnsiColors.use(AnsiColors.LightRed)
val reset = AnsiColors.use(AnsiColors.Reset)
val green = AnsiColors.use(AnsiColors.LightGreen)
sb.append(s" ($red- expected$reset, $green+ obtained$reset)")
sb.append("\n")
private def appendDiffOnlyReport(implicit sb: StringBuilder): Unit = {
header("Diff")
sb.append(" (")
AnsiColors.c(AnsiColors.LightRed, options.ansi)(_.append("- expected"))
sb.append(", ")
AnsiColors.c(AnsiColors.LightGreen, options.ansi)(_.append("+ obtained"))
sb.append(")\n")
sb.append(unifiedDiff)
}

Expand All @@ -76,8 +76,9 @@ object Diff {
options: DiffOptions
): String = apply(obtained, expected).unifiedDiff

private def asStripMargin(obtained: String): String =
if (!obtained.contains("\n")) Printers.print(obtained)
private def asStripMargin(obtained: String, flag: Boolean): String =
if (!flag) obtained
else if (!obtained.contains("\n")) Printers.print(obtained)
else {
val out = new StringBuilder
val lines = obtained.trim.linesIterator
Expand All @@ -88,8 +89,10 @@ object Diff {
out.toString()
}

private def header(t: String, sb: StringBuilder): StringBuilder = sb
.append(AnsiColors.c(s"=> $t", AnsiColors.Bold))
private def header(
t: String
)(implicit sb: StringBuilder, options: DiffOptions): Unit = AnsiColors
.c(AnsiColors.Bold, options.ansi)(_.append("=> ").append(t))

private def createUnifiedDiff(original: Seq[String], revised: Seq[String])(
implicit options: DiffOptions
Expand All @@ -107,8 +110,10 @@ object Diff {
.map(line => if (line.lastOption.contains(' ')) line + "" else line)
.map(line =>
line.headOption match {
case Some('-') => AnsiColors.c(line, AnsiColors.LightRed)
case Some('+') => AnsiColors.c(line, AnsiColors.LightGreen)
case Some('-') if options.ansi =>
AnsiColors.c(line, AnsiColors.LightRed)
case Some('+') if options.ansi =>
AnsiColors.c(line, AnsiColors.LightGreen)
case _ => line
}
).mkString("\n")
Expand Down
9 changes: 9 additions & 0 deletions munit-diff/shared/src/main/scala/munit/diff/DiffOptions.scala
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package munit.diff

class DiffOptions private (
val forceAnsi: Option[Boolean],
val contextSize: Int,
val showLines: Boolean,
val obtainedAsStripMargin: Boolean,
val printer: Option[Printer],
) {
private def privateCopy(
forceAnsi: Option[Boolean] = this.forceAnsi,
contextSize: Int = this.contextSize,
showLines: Boolean = this.showLines,
obtainedAsStripMargin: Boolean = this.obtainedAsStripMargin,
printer: Option[Printer] = this.printer,
): DiffOptions = new DiffOptions(
forceAnsi = forceAnsi,
contextSize = contextSize,
showLines = showLines,
obtainedAsStripMargin = obtainedAsStripMargin,
printer = printer,
)

def withForceAnsi(value: Option[Boolean]): DiffOptions =
privateCopy(forceAnsi = value)
def ansi(orElse: => Boolean): Boolean = forceAnsi.getOrElse(orElse)
def ansi: Boolean = ansi(true)

def withContextSize(value: Int): DiffOptions = privateCopy(contextSize = value)
def withShowLines(value: Boolean): DiffOptions = privateCopy(showLines = value)
def withObtainedAsStripMargin(value: Boolean): DiffOptions =
Expand All @@ -28,6 +36,7 @@ class DiffOptions private (

object DiffOptions
extends DiffOptions(
forceAnsi = None,
contextSize = 1,
showLines = false,
obtainedAsStripMargin = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ object AnsiColors {

def c(s: String, colorSequence: String): String =
if (colorSequence == null || noColor) s else colorSequence + s + Reset
def c(colorSequence: String, flag: Boolean = false)(
f: StringBuilder => Unit
)(implicit sb: StringBuilder): Unit =
if (!flag || colorSequence == null || noColor) f(sb)
else { sb.append(colorSequence); f(sb); sb.append(Reset) }

def filterAnsi(s: String): String =
if (s == null) null
Expand Down
96 changes: 72 additions & 24 deletions munit/shared/src/main/scala/munit/Assertions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package munit
import munit.diff.DiffOptions
import munit.diff.EmptyPrinter
import munit.diff.Printer
import munit.diff.console.AnsiColors
import munit.internal.MacroCompat
import munit.internal.console.Lines
import munit.internal.console.Printers
Expand All @@ -27,9 +26,8 @@ trait Assertions extends MacroCompat.CompileErrorMacro {
val munitLines = new Lines

def munitAnsiColors: Boolean = true

private def munitFilterAnsi(message: String): String =
if (munitAnsiColors) message else AnsiColors.filterAnsi(message)
private def useAnsiColors(implicit diffOptions: DiffOptions): Boolean =
diffOptions.ansi(munitAnsiColors)

def assert(cond: => Boolean, clue: => Any = "assertion failed")(implicit
loc: Location
Expand Down Expand Up @@ -234,27 +232,61 @@ trait Assertions extends MacroCompat.CompileErrorMacro {
}
}

// for MIMA compatibility
@deprecated("Use version with implicit DiffOptions", "1.0.4")
protected def fail(
message: String,
cause: Throwable,
loc: Location,
): Nothing = {
implicit val _loc: Location = loc
fail(message, cause)
}

/**
* Unconditionally fails this test with the given message and exception marked as the cause.
*/
def fail(message: String, cause: Throwable)(implicit loc: Location): Nothing =
throw new FailException(
munitFilterAnsi(munitLines.formatLine(loc, message)),
cause,
isStackTracesEnabled = true,
location = loc,
)
def fail(message: String, cause: Throwable)(implicit
loc: Location,
diffOptions: DiffOptions,
): Nothing = throw new FailException(
munitLines.formatLine(loc, message, ansi = useAnsiColors),
cause,
isStackTracesEnabled = true,
location = loc,
)

// for MIMA compatibility
@deprecated("Use version with implicit DiffOptions", "1.0.4")
protected def fail(message: String, clues: Clues, loc: Location): Nothing = {
implicit val _loc: Location = loc
fail(message, clues)
}

/**
* Unconditionally fails this test with the given message and optional clues.
*/
def fail(message: String, clues: Clues = new Clues(Nil))(implicit
loc: Location
loc: Location,
diffOptions: DiffOptions,
): Nothing = throw new FailException(
munitFilterAnsi(munitLines.formatLine(loc, message, clues)),
munitLines.formatLine(loc, message, clues, ansi = useAnsiColors),
loc,
)

// for MIMA compatibility
@deprecated("Use version with implicit DiffOptions", "1.0.4")
protected def failComparison(
message: String,
obtained: Any,
expected: Any,
clues: Clues,
loc: Location,
): Nothing = {
implicit val _loc: Location = loc
failComparison(message, obtained, expected, clues)
}

/**
* Unconditionally fails this test due to result of comparing two values.
*
Expand All @@ -267,29 +299,45 @@ trait Assertions extends MacroCompat.CompileErrorMacro {
obtained: Any,
expected: Any,
clues: Clues = new Clues(Nil),
)(implicit loc: Location): Nothing = throw new ComparisonFailException(
munitFilterAnsi(munitLines.formatLine(loc, message, clues)),
obtained,
expected,
loc,
isStackTracesEnabled = true,
)
)(implicit loc: Location, diffOptions: DiffOptions): Nothing =
throw new ComparisonFailException(
munitLines.formatLine(loc, message, clues, ansi = useAnsiColors),
obtained,
expected,
loc,
isStackTracesEnabled = true,
)

// for MIMA compatibility
@deprecated("Use version with implicit DiffOptions", "1.0.4")
protected def failSuite(
message: String,
clues: Clues,
loc: Location,
): Nothing = {
implicit val _loc: Location = loc
failSuite(message, clues)
}

/**
* Unconditionally fail this test case and cancel all the subsequent tests in this suite.
*/
def failSuite(message: String, clues: Clues = new Clues(Nil))(implicit
loc: Location
loc: Location,
diffOptions: DiffOptions,
): Nothing = throw new FailSuiteException(
munitFilterAnsi(munitLines.formatLine(loc, message, clues)),
munitLines.formatLine(loc, message, clues, ansi = useAnsiColors),
loc,
)

private def exceptionHandlerFromAssertions(
assertions: Assertions,
clues: => Clues,
): ComparisonFailExceptionHandler =
assertions.failComparison(_, _, _, clues)(_)
)(implicit diffOptions: DiffOptions): ComparisonFailExceptionHandler = {
(message: String, obtained: String, expected: String, location: Location) =>
implicit val loc = location
assertions.failComparison(message, obtained, expected, clues)
}

private val munitCapturedClues: mutable.ListBuffer[Clue[_]] =
mutable.ListBuffer.empty
Expand Down
7 changes: 4 additions & 3 deletions munit/shared/src/main/scala/munit/SuiteTransforms.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ trait SuiteTransforms {
if (onlySuite.nonEmpty)
if (!isCI) onlySuite
else onlySuite.map(t =>
if (t.tags(Only)) t.withBody(() =>
fail("'Only' tag is not allowed when `isCI=true`")(t.location)
)
if (t.tags(Only)) t.withBody { () =>
implicit val loc = t.location
fail("'Only' tag is not allowed when `isCI=true`")
}
else t
)
else tests
Expand Down
29 changes: 15 additions & 14 deletions munit/shared/src/main/scala/munit/internal/console/Lines.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package munit.internal.console

import munit.Clues
import munit.Location
import munit.diff.console.AnsiColors
import munit.internal.io.PlatformIO.Files
import munit.internal.io.PlatformIO.Path
import munit.internal.io.PlatformIO.Paths
Expand All @@ -13,8 +14,6 @@ import scala.util.control.NonFatal
class Lines extends Serializable {
private val filecache = mutable.Map.empty[Path, Array[String]]

def formatLine(location: Location, message: String): String =
formatLine(location, message, new Clues(Nil))
def formatPath(location: Location): String = location.path

def findPath(cwd: String, path: String, max: Int): Path = {
Expand All @@ -32,28 +31,30 @@ class Lines extends Serializable {
else findPath(getParentPath(cwd, "/"), path, max - 1)
}

def formatLine(location: Location, message: String, clues: Clues): String =
def formatLine(
location: Location,
message: String,
clues: Clues = Clues.empty,
ansi: Boolean = true,
): String =
try {
val path = findPath(Path.workingDirectory.toString, location.path, 3)
val lines = filecache
.getOrElseUpdate(path, Files.readAllLines(path).asScala.toArray)
val slice = lines.slice(location.line - 2, location.line + 1)
val out = new StringBuilder()
implicit val out: StringBuilder = new StringBuilder()
if (slice.length >= 2) {
val width = (location.line + 1).toString().length() + 1
val width = (location.line + 1).toString.length() + 1
def format(n: Int): String = s"$n:".padTo(width, ' ')
val isMultilineMessage = message.contains('\n')
out.append(formatPath(location)).append(':')
.append(location.line.toString())
if (message.length() > 0 && !isMultilineMessage) out.append(" ")
out.append(formatPath(location)).append(':').append(location.line)
if (message.nonEmpty && !isMultilineMessage) out.append(" ")
.append(message)
out.append('\n').append(format(location.line - 1)).append(slice(0))
.append('\n').append(munit.diff.console.AnsiColors.use(
munit.diff.console.AnsiColors.Reversed
)).append(format(location.line)).append(slice(1))
.append(munit.diff.console.AnsiColors.use(
munit.diff.console.AnsiColors.Reset
))
.append('\n')
AnsiColors.c(AnsiColors.Reversed, ansi)(
_.append(format(location.line)).append(slice(1))
)
if (slice.length >= 3) out.append('\n').append(format(location.line + 1))
.append(slice(2))
if (isMultilineMessage) out.append('\n').append(message)
Expand Down
20 changes: 20 additions & 0 deletions project/Mima.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package munit.build

import com.typesafe.tools.mima.core.*

// More details about Mima:
// https://github.com/typesafehub/migration-manager/wiki/sbt-plugin#basic-usage
object Mima {
val languageAgnosticCompatibilityPolicy: ProblemFilter = (problem: Problem) => {
val fullName = problem match {
case problem: TemplateProblem =>
val ref = problem.ref
ref.fullName
case problem: MemberProblem =>
val ref = problem.ref
ref.fullName
}

!fullName.startsWith("munit.internal.")
}
}
Loading

0 comments on commit 80f980b

Please sign in to comment.