Skip to content

Commit 15d4ecf

Browse files
author
Ronald Holshausen
committed
Display a smaller diff for some of the body mismatches #370
Conflicts: pact-jvm-matchers/build.gradle
1 parent a65ea49 commit 15d4ecf

File tree

14 files changed

+94
-107
lines changed

14 files changed

+94
-107
lines changed

pact-jvm-matchers/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ dependencies {
22
compile project(":pact-jvm-model"),
33
"org.apache.commons:commons-lang3:${project.commonsLang3Version}",
44
"io.gatling:jsonpath_${project.scalaVersion}:0.6.4",
5-
project(":pact-jvm-logging_${project.scalaVersion}")
5+
project(":pact-jvm-logging_${project.scalaVersion}"),
6+
'com.googlecode.java-diff-utils:diffutils:1.3.0'
67

78
testCompile "ch.qos.logback:logback-classic:${project.logbackVersion}"
89
testCompile('org.spockframework:spock-core:1.0-groovy-2.4') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package au.com.dius.pact.matchers
2+
3+
import difflib.Delta
4+
import difflib.Patch
5+
import groovy.json.JsonOutput
6+
7+
/**
8+
* Utility methods for generating diffs
9+
*/
10+
class DiffUtils {
11+
12+
private static final String NL = '\n'
13+
14+
static generateDiff(String expectedBodyString, String actualBodyString) {
15+
def expectedLines = expectedBodyString.split(NL) as List
16+
def actualLines = actualBodyString.split(NL) as List
17+
Patch<String> patch = difflib.DiffUtils.diff(expectedLines, actualLines)
18+
19+
def diff = []
20+
21+
patch.deltas.each { Delta<String> delta ->
22+
if (diff.empty || delta.original.position > 1 && expectedLines[delta.original.position - 1] != diff.last()) {
23+
diff << expectedLines[delta.original.position - 1]
24+
}
25+
26+
delta.original.lines.each {
27+
diff << "-$it"
28+
}
29+
delta.revised.lines.each {
30+
diff << "+$it"
31+
}
32+
33+
int pos = delta.original.position + delta.original.lines.size()
34+
if (pos < expectedLines.size()) {
35+
diff << expectedLines[pos]
36+
}
37+
}
38+
diff
39+
}
40+
41+
static String generateObjectDiff(expected, actual) {
42+
def actualJson = ''
43+
if (actual) {
44+
actualJson = JsonOutput.prettyPrint(JsonOutput.toJson(actual))
45+
}
46+
47+
def expectedJson = ''
48+
if (expected) {
49+
expectedJson = JsonOutput.prettyPrint(JsonOutput.toJson(expected))
50+
}
51+
52+
generateDiff(expectedJson, actualJson).join(NL)
53+
}
54+
}

pact-jvm-matchers/src/main/scala/au/com/dius/pact/matchers/JsonBodyMatcher.scala

+16-9
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,18 @@ class JsonBodyMatcher extends BodyMatcher with StrictLogging {
5151
(expected.isInstanceOf[List[Any]] && !actual.isInstanceOf[List[Any]])) {
5252
List(BodyMismatch(expected, actual,
5353
Some(s"Type mismatch: Expected ${typeOf(expected)} ${valueOf(expected)} but received ${typeOf(actual)} ${valueOf(actual)}"),
54-
path.mkString(".")))
54+
path.mkString("."), generateObjectDiff(expected, actual)))
5555
} else {
5656
compareValues(path, expected, actual, matchers)
5757
}
5858
}
5959
}
6060

61+
private def generateObjectDiff(expected: Any, actual: Any) = {
62+
Some(DiffUtils.generateObjectDiff(JsonUtils.scalaObjectGraphToJavaObjectGraph(expected),
63+
JsonUtils.scalaObjectGraphToJavaObjectGraph(actual)))
64+
}
65+
6166
def compareListContent(expectedValues: List[Any], actualValues: List[Any], path: Seq[String],
6267
diffConfig: DiffConfig, matchers: Option[Map[String, Map[String, Any]]]) = {
6368
var result = List[BodyMismatch]()
@@ -66,7 +71,7 @@ class JsonBodyMatcher extends BodyMatcher with StrictLogging {
6671
result = result ++: compare(path :+ index.toString, value, actualValues(index), diffConfig, matchers)
6772
} else if (!Matchers.matcherDefined(path, matchers)) {
6873
result = result :+ BodyMismatch(expectedValues, actualValues, Some(s"Expected ${valueOf(value)} but was missing"),
69-
path.mkString("."))
74+
path.mkString("."), generateObjectDiff(expectedValues, actualValues))
7075
}
7176
}
7277
result
@@ -84,13 +89,14 @@ class JsonBodyMatcher extends BodyMatcher with StrictLogging {
8489
result
8590
} else {
8691
if (expectedValues.isEmpty && actualValues.nonEmpty) {
87-
List(BodyMismatch(a, b, Some(s"Expected an empty List but received ${valueOf(actualValues)}"), path.mkString(".")))
92+
List(BodyMismatch(a, b, Some(s"Expected an empty List but received ${valueOf(actualValues)}"),
93+
path.mkString("."), generateObjectDiff(expectedValues, actualValues)))
8894
} else {
8995
var result = compareListContent(expectedValues, actualValues, path, diffConfig, matchers)
9096
if (expectedValues.size != actualValues.size) {
9197
result = result :+ BodyMismatch(a, b,
9298
Some(s"Expected a List with ${expectedValues.size} elements but received ${actualValues.size} elements"),
93-
path.mkString("."))
99+
path.mkString("."), generateObjectDiff(expectedValues, actualValues))
94100
}
95101
result
96102
}
@@ -100,17 +106,18 @@ class JsonBodyMatcher extends BodyMatcher with StrictLogging {
100106
def compareMaps(expectedValues: Map[String, Any], actualValues: Map[String, Any], a: Any, b: Any, path: Seq[String],
101107
diffConfig: DiffConfig, matchers: Option[Map[String, Map[String, Any]]]): List[BodyMismatch] = {
102108
if (expectedValues.isEmpty && actualValues.nonEmpty) {
103-
List(BodyMismatch(a, b, Some(s"Expected an empty Map but received ${valueOf(actualValues)}"), path.mkString(".")))
109+
List(BodyMismatch(a, b, Some(s"Expected an empty Map but received ${valueOf(actualValues)}"), path.mkString("."),
110+
generateObjectDiff(expectedValues, actualValues)))
104111
} else {
105112
var result = List[BodyMismatch]()
106113
if (diffConfig.allowUnexpectedKeys && expectedValues.size > actualValues.size) {
107114
result = result :+ BodyMismatch(a, b,
108115
Some(s"Expected a Map with at least ${expectedValues.size} elements but received ${actualValues.size} elements"),
109-
path.mkString("."))
116+
path.mkString("."), generateObjectDiff(expectedValues, actualValues))
110117
} else if (!diffConfig.allowUnexpectedKeys && expectedValues.size != actualValues.size) {
111118
result = result :+ BodyMismatch(a, b,
112119
Some(s"Expected a Map with ${expectedValues.size} elements but received ${actualValues.size} elements"),
113-
path.mkString("."))
120+
path.mkString("."), generateObjectDiff(expectedValues, actualValues))
114121
}
115122
if (Matchers.wildcardMatcherDefined(path :+ "any", matchers)) {
116123
actualValues.foreach(entry => {
@@ -126,7 +133,7 @@ class JsonBodyMatcher extends BodyMatcher with StrictLogging {
126133
result = result ++: compare(path :+ entry._1, entry._2, actualValues(entry._1), diffConfig, matchers)
127134
} else {
128135
result = result :+ BodyMismatch(a, b, Some(s"Expected ${entry._1}=${valueOf(entry._2)} but was missing"),
129-
path.mkString("."))
136+
path.mkString("."), generateObjectDiff(expectedValues, actualValues))
130137
}
131138
})
132139
}
@@ -144,7 +151,7 @@ class JsonBodyMatcher extends BodyMatcher with StrictLogging {
144151
List[BodyMismatch]()
145152
} else {
146153
List(BodyMismatch(expected, actual, Some(s"Expected ${valueOf(expected)} but received ${valueOf(actual)}"),
147-
path.mkString(".")))
154+
path.mkString("."), None))
148155
}
149156
}
150157
}

pact-jvm-matchers/src/main/scala/au/com/dius/pact/matchers/util/JsonUtils.scala

+10
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,14 @@ object JsonUtils {
2121
}
2222
}
2323

24+
def scalaObjectGraphToJavaObjectGraph(value: Any): Any = {
25+
value match {
26+
case map: Map[String, Any] =>
27+
JavaConversions.mapAsJavaMap(map.mapValues(scalaObjectGraphToJavaObjectGraph))
28+
case list: List[Any] =>
29+
JavaConversions.seqAsJavaList(list.map(scalaObjectGraphToJavaObjectGraph))
30+
case _ => value
31+
}
32+
}
33+
2434
}

pact-jvm-matchers/src/main/scala/au/com/dius/pact/model/Matching.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ case class QueryMismatch(queryParameter: String, expected: String, actual: Strin
6060

6161
object BodyMismatchFactory extends MismatchFactory[BodyMismatch] {
6262
def create(expected: scala.Any, actual: scala.Any, message: String, path: Seq[String]) =
63-
BodyMismatch(expected, actual, Some(message), path.mkString("."))
63+
BodyMismatch(expected, actual, Some(message), path.mkString("."), None)
6464
}
6565

6666
object PathMismatchFactory extends MismatchFactory[PathMismatch] {

pact-jvm-provider-gradle/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ The following project properties can be specified with `-Pproperty=value` on the
296296
|Property|Description|
297297
|--------|-----------|
298298
|pact.showStacktrace|This turns on stacktrace printing for each request. It can help with diagnosing network errors|
299+
|pact.showFullDiff|This turns on displaying the full diff of the expected versus actual bodies [version 3.3.6+]|
299300
|pact.filter.consumers|Comma seperated list of consumer names to verify|
300301
|pact.filter.description|Only verify interactions whose description match the provided regular expression|
301302
|pact.filter.providerState|Only verify interactions whose provider state match the provided regular expression. An empty string matches interactions that have no state|

pact-jvm-provider-gradle/src/test/groovy/au/com/dius/pact/provider/gradle/ResponseComparisonTest.groovy

-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ class ResponseComparisonTest {
8585
assert result.comparison == [
8686
'$.body.stuff': [mismatch: "Expected 'is good' but received 'should make the test fail'", diff: '']
8787
]
88-
assert result.diff[0] == '@1'
8988
assert result.diff[1] == '- "stuff": "is good"'
9089
assert result.diff[2] == '+ "stuff": "should make the test fail"'
9190
}

pact-jvm-provider-lein/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ The following plugin options can be specified on the command line:
148148
|Property|Description|
149149
|--------|-----------|
150150
|:pact.showStacktrace|This turns on stacktrace printing for each request. It can help with diagnosing network errors|
151+
|:pact.showFullDiff|This turns on displaying the full diff of the expected versus actual bodies [version 3.3.6+]|
151152
|:pact.filter.consumers|Comma seperated list of consumer names to verify|
152153
|:pact.filter.description|Only verify interactions whose description match the provided regular expression|
153154
|:pact.filter.providerState|Only verify interactions whose provider state match the provided regular expression. An empty string matches interactions that have no state|

pact-jvm-provider-maven/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ The following plugin properties can be specified with `-Dproperty=value` on the
224224
|Property|Description|
225225
|--------|-----------|
226226
|pact.showStacktrace|This turns on stacktrace printing for each request. It can help with diagnosing network errors|
227+
|pact.showFullDiff|This turns on displaying the full diff of the expected versus actual bodies [version 3.3.6+]|
227228
|pact.filter.consumers|Comma seperated list of consumer names to verify|
228229
|pact.filter.description|Only verify interactions whose description match the provided regular expression|
229230
|pact.filter.providerState|Only verify interactions whose provider state match the provided regular expression. An empty string matches interactions that have no state|

pact-jvm-provider-sbt/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ The following project properties can be specified with `-Dproperty=value` on the
161161
|Property|Description|
162162
|--------|-----------|
163163
|pact.showStacktrace|This turns on stacktrace printing for each request. It can help with diagnosing network errors|
164+
|pact.showFullDiff|This turns on displaying the full diff of the expected versus actual bodies [version 3.3.6+]|
164165
|pact.filter.consumers|Comma separated list of consumer names to verify|
165166
|pact.filter.description|Only verify interactions whose description match the provided regular expression|
166167
|pact.filter.providerState|Only verify interactions whose provider state match the provided regular expression. An empty string matches interactions that have no state|

pact-jvm-provider/build.gradle

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ dependencies {
77
"org.fusesource.jansi:jansi:${project.jansiVersion}",
88
"org.codehaus.groovy.modules.http-builder:http-builder:${project.httpBuilderVersion}",
99
"org.apache.httpcomponents:httpclient:${project.httpClientVersion}",
10-
'com.googlecode.java-diff-utils:diffutils:1.3.0',
1110
'org.reflections:reflections:0.9.10',
1211
"net.databinder:unfiltered-netty-server_${project.scalaVersion}:0.8.4",
1312
"net.databinder.dispatch:dispatch-core_${project.scalaVersion}:0.11.2"

pact-jvm-provider/src/main/groovy/au/com/dius/pact/provider/DiffUtils.groovy

-88
This file was deleted.

pact-jvm-provider/src/main/groovy/au/com/dius/pact/provider/ResponseComparison.groovy

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package au.com.dius.pact.provider
22

3+
import au.com.dius.pact.matchers.DiffUtils
34
import au.com.dius.pact.matchers.MatchingConfig
45
import au.com.dius.pact.model.BodyMismatch
56
import au.com.dius.pact.model.BodyTypeMismatch

pact-jvm-provider/src/main/groovy/au/com/dius/pact/provider/reporters/AnsiConsoleReporter.groovy

+6-6
Original file line numberDiff line numberDiff line change
@@ -210,18 +210,18 @@ class AnsiConsoleReporter implements VerifierReporter {
210210
AnsiConsole.out().println()
211211

212212
if (messageAndDiff.diff) {
213-
AnsiConsole.out().println(' Diff:')
213+
AnsiConsole.out().println(' Diff:')
214214
AnsiConsole.out().println()
215215

216-
messageAndDiff.diff.each { delta ->
216+
messageAndDiff.diff.eachLine { delta ->
217217
if (delta.startsWith('@')) {
218-
AnsiConsole.out().println(Ansi.ansi().a(' ').fg(Ansi.Color.CYAN).a(delta).reset())
218+
AnsiConsole.out().println(Ansi.ansi().a(' ').fg(Ansi.Color.CYAN).a(delta).reset())
219219
} else if (delta.startsWith('-')) {
220-
AnsiConsole.out().println(Ansi.ansi().a(' ').fg(Ansi.Color.RED).a(delta).reset())
220+
AnsiConsole.out().println(Ansi.ansi().a(' ').fg(Ansi.Color.RED).a(delta).reset())
221221
} else if (delta.startsWith('+')) {
222-
AnsiConsole.out().println(Ansi.ansi().a(' ').fg(Ansi.Color.GREEN).a(delta).reset())
222+
AnsiConsole.out().println(Ansi.ansi().a(' ').fg(Ansi.Color.GREEN).a(delta).reset())
223223
} else {
224-
AnsiConsole.out().println(" $delta")
224+
AnsiConsole.out().println(" $delta")
225225
}
226226
}
227227
AnsiConsole.out().println()

0 commit comments

Comments
 (0)