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

Close #273: YAML diff #285

Merged
merged 5 commits into from
Oct 25, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ lazy val vm = (project in file("vm"))
.settings(
sources in doc := Seq.empty,
publishArtifact in packageDoc := false,
testFrameworks := Seq(new TestFramework("pravda.common.PreserveColoursFramework"))
)
.dependsOn(`vm-api`, `vm-asm` % "compile->test")
.dependsOn(common % "compile->compile;test->test")
Expand Down Expand Up @@ -148,7 +149,8 @@ lazy val dotnet = (project in file("dotnet"))
"org.typelevel" %% "cats-core" % "1.0.1",
"com.lihaoyi" %% "fastparse-byte" % "1.0.0",
"com.lihaoyi" %% "pprint" % "0.5.3" % "test"
)
),
testFrameworks := Seq(new TestFramework("pravda.common.PreserveColoursFramework"))
)
.dependsOn(`vm-asm`)
.dependsOn(common % "test->test")
Expand Down Expand Up @@ -297,7 +299,8 @@ lazy val testkit = (project in file("testkit"))
.settings(
skip in publish := true,
normalizedName := "pravda-testkit",
unmanagedResourceDirectories in Test += dotnetTests
unmanagedResourceDirectories in Test += dotnetTests,
testFrameworks := Seq(new TestFramework("pravda.common.PreserveColoursFramework"))
)
.dependsOn(common % "test->test")
.dependsOn(vm % "compile->compile;test->test")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package pravda.common
import utest.ufansi.Attrs

class PreserveColoursFramework extends utest.runner.Framework {
override def exceptionMsgColor: Attrs = Attrs.Empty
}
9 changes: 4 additions & 5 deletions plaintest/src/main/scala/pravda/plaintest/Plaintest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,10 @@ abstract class Plaintest[Input: Manifest, Output: Manifest] extends TestSuite {
Predef.assert(
res == output,
s"""
|Expected:
|${yaml4s.renderYaml(Extraction.decompose(res)(formats))}
|Actual output:
|${yaml4s.renderYaml(Extraction.decompose(output)(formats))}
""".stripMargin
|Produced:
|${yaml4s.renderYamlDiff(Extraction.decompose(output)(formats),
Extraction.decompose(res)(formats))}
""".stripMargin
)
case Left(err) => Predef.assert(false, s"${f.getName}: $err")
}
Expand Down
85 changes: 85 additions & 0 deletions yaml4s/src/main/scala/pravda/yaml4s/YamlMethods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,91 @@ object YamlMethods {
}
}

/**
* Render diff of two JValues as yaml.
* Changed parts are coloured yellow, added parts -- green, deleted parts -- red.
* The style is fixed, and produced yaml can be wrong, it should be used only in demonstrating purposes.
*
* @param node Obtained JValue.
* @param expected Expected JValue.
* @return Yaml with colours.
*/
def renderDiff(node: JValue, expected: JValue): String = {

def print(j: JValue) = yaml.dump(jvalue2java(j)).trim
def withColour(s: String, colour: String) = s"$colour$s${Console.RESET}"
def printWithColour(j: JValue, colour: String) = withColour(print(j), colour)

def withNewLine(s: String) = if (s.nonEmpty) "\n" + s else s

def diff(j1: JValue, j2: JValue): String = (j1, j2) match {
case (x, y) if x == y => print(x)
case (JObject(xs), JObject(ys)) => diffFields(xs, ys)
case (JArray(xs), JArray(ys)) => diffVals(xs, ys)
case (x: JInt, y: JInt) if x != y => printWithColour(y, Console.YELLOW)
case (x: JDouble, y: JDouble) if x != y => printWithColour(y, Console.YELLOW)
case (x: JDecimal, y: JDecimal) if x != y => printWithColour(y, Console.YELLOW)
case (x: JString, y: JString) if x != y => printWithColour(y, Console.YELLOW)
case (x: JBool, y: JBool) if x != y => printWithColour(y, Console.YELLOW)
case (JNothing, x) => printWithColour(x, Console.GREEN)
case (x, JNothing) => printWithColour(x, Console.RED)
case (x, y) => printWithColour(y, Console.YELLOW)
}

def diffFields(xs: List[JField], ys: List[JField]): String = {
def formatElem(name: String, elem: String) = {
val lines = elem.lines.toList
if (lines.length <= 1) {
s"$name: $elem"
} else {
s"""$name:
| ${lines.mkString("\n ")}
""".stripMargin
}
}

xs match {
case (xname, xvalue) :: xtail if ys.exists(_._1 == xname) =>
val (_, yvalue) = ys.find(_._1 == xname).get
formatElem(xname, diff(xvalue, yvalue)) + withNewLine(diffFields(xtail, ys.filterNot(_ == xname -> yvalue)))
case (xname, xvalue) :: xtail =>
withColour(formatElem(xname, print(xvalue)), Console.RED) + withNewLine(diffFields(xtail, ys))
case Nil =>
ys match {
case (yname, yvalue) :: ytail =>
withColour(formatElem(yname, print(yvalue)), Console.GREEN) + withNewLine(diffFields(Nil, ytail))
case Nil => ""
}
}
}

def diffVals(xs: List[JValue], ys: List[JValue]): String = {
def formatElem(elem: String) = {
val lines = elem.lines.toList
lines match {
case Nil => s"-"
case head :: tail =>
val h = s"- $head"
val t = if (tail.nonEmpty) {
s"\n${tail.map(s => s" $s").mkString("\n")}"
} else {
""
}
h + t
}
}

(xs, ys) match {
case (x :: xtail, y :: ytail) => formatElem(diff(x, y)) + withNewLine(diffVals(xtail, ytail))
case (Nil, y :: ytail) => withColour(formatElem(print(y)), Console.GREEN) + withNewLine(diffVals(Nil, ytail))
case (x :: xtail, Nil) => withColour(formatElem(print(x)), Console.RED) + withNewLine(diffVals(xtail, Nil))
case (Nil, Nil) => ""
}
}

diff(node, expected)
}

def parseOpt(in: JsonInput, useBigDecimalForDouble: Boolean): Option[JValue] =
Try { parseUnsafe(in, useBigDecimalForDouble) }.toOption

Expand Down
2 changes: 2 additions & 0 deletions yaml4s/src/main/scala/pravda/yaml4s/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ package object yaml4s {

def renderYaml(node: JValue): String = YamlMethods.render(node)

def renderYamlDiff(node: JValue, expected: JValue): String = YamlMethods.renderDiff(node, expected)

def parseYamlOpt(in: JsonInput, useBigDecimalForDouble: Boolean): Option[JValue] =
YamlMethods.parseOpt(in, useBigDecimalForDouble)

Expand Down
150 changes: 150 additions & 0 deletions yaml4s/src/test/scala/pravda/yaml4s/YamlDiffSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package pravda.yaml4s

import org.json4s._
import org.json4s.native.JsonMethods._
import utest._

import scala.io.AnsiColor

object YamlDiffSuite extends TestSuite {

private val coloursLabels = Map(
AnsiColor.BLACK -> "black",
AnsiColor.RED -> "red",
AnsiColor.GREEN -> "green",
AnsiColor.YELLOW -> "yellow",
AnsiColor.BLUE -> "blue",
AnsiColor.MAGENTA -> "magenta",
AnsiColor.CYAN -> "cyan",
AnsiColor.WHITE -> "white",
AnsiColor.BLACK_B -> "black_b",
AnsiColor.RED_B -> "red_b",
AnsiColor.GREEN_B -> "green_b",
AnsiColor.YELLOW_B -> "yellow_b",
AnsiColor.BLUE_B -> "blue_b",
AnsiColor.MAGENTA_B -> "magenta_b",
AnsiColor.CYAN_B -> "cyan_b",
AnsiColor.WHITE_B -> "white_b",
AnsiColor.RESET -> "reset",
AnsiColor.BOLD -> "bold",
AnsiColor.UNDERLINED -> "underlined",
AnsiColor.BLINK -> "blink",
AnsiColor.REVERSED -> "reversed",
AnsiColor.INVISIBLE -> "invisible"
)

private def escapeColours(s: String): String = {
var res: String = s
for {
(colour, label) <- coloursLabels
} {
res = res.replace(colour, s"[$label]")
}

res
}

def tests = Tests {
"simple object diff" - {
val json1 = parse("""
{
"first body": 1,
"second body": 2,
"third body": 3
}
""")
val json2 = parse("""
{
"first body": "one",
"second body": "two",
"third body": "three"
}
""")

escapeColours(YamlMethods.renderDiff(json1, json2)) ==>
"""first body: [yellow]one[reset]
|second body: [yellow]two[reset]
|third body: [yellow]three[reset]""".stripMargin
}

"simple object deletion diff" - {
val json1 = parse("""
{
"first body": 1,
"second body": "two",
"third body": "three",
"forth body": "four"
}
""")
val json2 = parse("""
{
"first body": "one",
"second body": "two",
"third body": "three"
}
""")

escapeColours(YamlMethods.renderDiff(json1, json2)) ==>
"""first body: [yellow]one[reset]
|second body: two
|third body: three
|[red]forth body: four[reset]""".stripMargin
}

"simple object addition diff" - {
val json1 = parse("""
{
"first body": 1,
"second body": "two",
"third body": "three",
}
""")
val json2 = parse("""
{
"first body": "one",
"second body": "two",
"third body": "three",
"forth body": "four"
}
""")

escapeColours(YamlMethods.renderDiff(json1, json2)) ==>
"""first body: [yellow]one[reset]
|second body: two
|third body: three
|[green]forth body: four[reset]""".stripMargin
}

"simple object permutation diff" - {
val json1 = parse("""
{
"second body": 2,
"first body": 1,
"third body": 3
}
""")
val json2 = parse("""
{
"first body": "one",
"second body": "two",
"third body": "three"
}
""")

escapeColours(YamlMethods.renderDiff(json1, json2)) ==>
"""second body: [yellow]two[reset]
|first body: [yellow]one[reset]
|third body: [yellow]three[reset]""".stripMargin
}

"simple array diff" - {
val json1 = parse("[1, 2, 3]")
val json2 = parse("[\"one\", \"two\", \"three\"]")

escapeColours(YamlMethods.renderDiff(json1, json2)) ==>
"""- [yellow]one[reset]
|- [yellow]two[reset]
|- [yellow]three[reset]""".stripMargin
}
}
}