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

Use munit diff logic #25

Merged
merged 10 commits into from Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 5 additions & 9 deletions modules/core/shared/src/main/scala/weaver/Comparison.scala
Expand Up @@ -2,7 +2,6 @@ package weaver

import cats.Eq
import cats.Show
import com.eed3si9n.expecty._
import scala.annotation.implicitNotFound

/**
Expand Down Expand Up @@ -44,14 +43,11 @@ object Comparison {
if (eqv.eqv(found, expected)) {
Result.Success
} else {
val expectedLines = showA.show(expected).linesIterator.toSeq
val foundLines = showA.show(found).linesIterator.toSeq
val report = DiffUtil
.mkColoredLineDiff(expectedLines, foundLines)
.linesIterator
.toSeq
.map(str => Console.RESET.toString + str)
.mkString("\n")
val diff = new weaver.diff.Diff(
obtained = showA.show(found),
expected = showA.show(expected)
)
val report = diff.createReport("", printObtainedAsStripMargin = false)
Result.Failure(report)
}
}
Expand Down
12 changes: 12 additions & 0 deletions modules/core/shared/src/main/scala/weaver/diff/Chunk.scala
@@ -0,0 +1,12 @@
package weaver.diff

import java.util

class Chunk[T](position: Int, lines: util.List[T]) {

def getPosition: Int = position
def getLines: util.List[T] = lines
def size: Int = lines.size()

override def toString: String = s"Chunk($getPosition, $getLines, $size)"
}
28 changes: 28 additions & 0 deletions modules/core/shared/src/main/scala/weaver/diff/Delta.scala
@@ -0,0 +1,28 @@
package weaver.diff

sealed abstract class Delta[T](original: Chunk[T], revised: Chunk[T]) {

sealed abstract class TYPE
object TYPE {
case object CHANGE extends TYPE
case object DELETE extends TYPE
case object INSERT extends TYPE
}
def getType: TYPE
def getOriginal: Chunk[T] = original
def getRevised: Chunk[T] = revised

override def toString: String = s"Delta($getType, $getOriginal, $getRevised)"
}
class ChangeDelta[T](original: Chunk[T], revised: Chunk[T])
extends Delta(original, revised) {
override def getType: TYPE = TYPE.CHANGE
}
class InsertDelta[T](original: Chunk[T], revised: Chunk[T])
extends Delta(original, revised) {
override def getType: TYPE = TYPE.INSERT
}
class DeleteDelta[T](original: Chunk[T], revised: Chunk[T])
extends Delta(original, revised) {
override def getType: TYPE = TYPE.DELETE
}
105 changes: 105 additions & 0 deletions modules/core/shared/src/main/scala/weaver/diff/Diff.scala
@@ -0,0 +1,105 @@
package weaver.diff

import weaver.diff.console.Printers
import weaver.diff.console.AnsiColors

import scala.jdk.CollectionConverters._

class Diff(val obtained: String, val expected: String) extends Serializable {
val obtainedClean: String = AnsiColors.filterAnsi(obtained)
val expectedClean: String = AnsiColors.filterAnsi(expected)
val obtainedLines: Seq[String] = splitIntoLines(obtainedClean)
val expectedLines: Seq[String] = splitIntoLines(expectedClean)
val unifiedDiff: String = createUnifiedDiff(obtainedLines, expectedLines)

def createReport(
title: String,
printObtainedAsStripMargin: Boolean = true
): String = {
val sb = 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)
}
sb.append("\n")
}
appendDiffOnlyReport(sb)
sb.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}- obtained${reset}, ${green}+ expected${reset})")
sb.append("\n")
sb.append(unifiedDiff)
}

private def asStripMargin(obtained: String): String = {
if (!obtained.contains("\n")) Printers.print(obtained)
else {
val out = new StringBuilder
val lines = obtained.trim.linesIterator
val head = if (lines.hasNext) lines.next() else ""
out.append(" \"\"\"|" + head + "\n")
lines.foreach(line => {
out.append(" |").append(line).append("\n")
})
out.append(" |\"\"\".stripMargin")
out.toString()
}
}

private def header(t: String, sb: StringBuilder): StringBuilder = {
sb.append(AnsiColors.c(s"=> $t", AnsiColors.Bold))
}

private def createUnifiedDiff(
original: Seq[String],
revised: Seq[String]
): String = {
val diff = DiffUtils.diff(original.asJava, revised.asJava)
val result =
if (diff.getDeltas.isEmpty) ""
else {
DiffUtils
.generateUnifiedDiff(
"obtained",
"expected",
original.asJava,
diff,
1
)
.asScala
.iterator
.drop(2)
.filterNot(_.startsWith("@@"))
.map { line =>
if (line.isEmpty()) line
else if (line.last == ' ') line + "∙"
else line
}
.map { line =>
if (line.startsWith("-")) AnsiColors.c(line, AnsiColors.LightRed)
else if (line.startsWith("+"))
AnsiColors.c(line, AnsiColors.LightGreen)
else line
}
.mkString("\n")
}
result
}

private def splitIntoLines(string: String): Seq[String] = {
string.trim().replace("\r\n", "\n").split("\n").toIndexedSeq
}
}
@@ -0,0 +1,7 @@
package weaver.diff

import java.util

trait DiffAlgorithm[T] {
def diff(original: util.List[T], revised: util.List[T]): Patch[T]
}
162 changes: 162 additions & 0 deletions modules/core/shared/src/main/scala/weaver/diff/DiffUtils.scala
@@ -0,0 +1,162 @@
package weaver.diff

import java.util

object DiffUtils {
def generateUnifiedDiff(
original: String,
revised: String,
originalLines: util.List[String],
patch: Patch[String],
contextSize: Int
): util.List[String] = {
if (!patch.getDeltas.isEmpty) {
val ret: util.List[String] = new util.ArrayList()
ret.add("--- " + original)
ret.add("+++ " + revised)
val patchDeltas: util.List[Delta[String]] =
new util.ArrayList(patch.getDeltas)
val deltas: util.List[Delta[String]] = new util.ArrayList()

var delta = patchDeltas.get(0)
deltas.add(delta)

if (patchDeltas.size() > 1) {
for (i <- 1 until patchDeltas.size) {
val position = delta.getOriginal.getPosition // store
// the
// current
// position
// of
// the first Delta
// Check if the next Delta is too close to the current
// position.
// And if it is, add it to the current set
val nextDelta = patchDeltas.get(i)
if ((position + delta.getOriginal.size + contextSize) >=
(nextDelta.getOriginal.getPosition - contextSize)) {
deltas.add(nextDelta)
} else { // if it isn't, output the current set,
// then create a new set and add the current Delta to
// it.
val curBlock = processDeltas(originalLines, deltas, contextSize)
ret.addAll(curBlock)
deltas.clear()
deltas.add(nextDelta)
}
delta = nextDelta
}
}
val curBlock = processDeltas(originalLines, deltas, contextSize)
ret.addAll(curBlock)
ret
} else {
new util.ArrayList[String]()
}
}
def diff(
original: util.List[String],
revised: util.List[String]
): Patch[String] =
new MyersDiff[String]().diff(original, revised)

private def processDeltas(
origLines: util.List[String],
deltas: util.List[Delta[String]],
contextSize: Int
) = {
val buffer = new util.ArrayList[String]
var origTotal = 0 // counter for total lines output from Original
var revTotal = 0
var line = 0
var curDelta = deltas.get(0)
// NOTE: +1 to overcome the 0-offset Position
var origStart = curDelta.getOriginal.getPosition + 1 - contextSize
if (origStart < 1) origStart = 1
var revStart = curDelta.getRevised.getPosition + 1 - contextSize
if (revStart < 1) revStart = 1
// find the start of the wrapper context code
var contextStart = curDelta.getOriginal.getPosition - contextSize
if (contextStart < 0) contextStart = 0 // clamp to the start of the file
// output the context before the first Delta
line = contextStart
while ({
line < curDelta.getOriginal.getPosition
}) { //
buffer.add(" " + origLines.get(line))
origTotal += 1
revTotal += 1

line += 1
}
// output the first Delta
buffer.addAll(getDeltaText(curDelta))
origTotal += curDelta.getOriginal.getLines.size
revTotal += curDelta.getRevised.getLines.size
var deltaIndex = 1
while ({
deltaIndex < deltas.size
}) { // for each of the other Deltas
val nextDelta = deltas.get(deltaIndex)
val intermediateStart =
curDelta.getOriginal.getPosition + curDelta.getOriginal.getLines.size
line = intermediateStart
while ({
line < nextDelta.getOriginal.getPosition
}) { // output the code between the last Delta and this one
buffer.add(" " + origLines.get(line))
origTotal += 1
revTotal += 1

line += 1
}
buffer.addAll(getDeltaText(nextDelta)) // output the Delta

origTotal += nextDelta.getOriginal.getLines.size
revTotal += nextDelta.getRevised.getLines.size
curDelta = nextDelta
deltaIndex += 1
}
// Now output the post-Delta context code, clamping the end of the file
contextStart =
curDelta.getOriginal.getPosition + curDelta.getOriginal.getLines.size
line = contextStart
while ({
(line < (contextStart + contextSize)) && (line < origLines.size)
}) {
buffer.add(" " + origLines.get(line))
origTotal += 1
revTotal += 1

line += 1
}
// Create and insert the block header, conforming to the Unified Diff
// standard
val header = new StringBuffer
header.append("@@ -")
header.append(origStart)
header.append(",")
header.append(origTotal)
header.append(" +")
header.append(revStart)
header.append(",")
header.append(revTotal)
header.append(" @@")
buffer.add(0, header.toString)
buffer
}

private def getDeltaText(delta: Delta[String]) = {
import scala.jdk.CollectionConverters._

val buffer = new util.ArrayList[String]
for (line <- delta.getOriginal.getLines.asScala) {
buffer.add("-" + line)
}
for (line <- delta.getRevised.getLines.asScala) {
buffer.add("+" + line)
}
buffer
}

}
@@ -0,0 +1,3 @@
package weaver.diff

class DifferentiationFailedException(message: String) extends Exception(message)
12 changes: 12 additions & 0 deletions modules/core/shared/src/main/scala/weaver/diff/Equalizer.scala
@@ -0,0 +1,12 @@
package weaver.diff

trait Equalizer[T] {
def equals(original: T, revised: T): Boolean
}
object Equalizer {
def default[T]: Equalizer[T] = new Equalizer[T] {
override def equals(original: T, revised: T): Boolean = {
original == revised
}
}
}