diff --git a/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/xml/PactXmlBuilderSpec.groovy b/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/xml/PactXmlBuilderSpec.groovy
index 168737d1c3..04b0c21a7b 100644
--- a/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/xml/PactXmlBuilderSpec.groovy
+++ b/consumer/junit5/src/test/groovy/au/com/dius/pact/consumer/xml/PactXmlBuilderSpec.groovy
@@ -1,13 +1,12 @@
package au.com.dius.pact.consumer.xml
-import spock.lang.Ignore
+import groovy.xml.XmlSlurper
import spock.lang.Specification
import static au.com.dius.pact.consumer.dsl.Matchers.integer
import static au.com.dius.pact.consumer.dsl.Matchers.string
class PactXmlBuilderSpec extends Specification {
- @Ignore // fails on travis due to whitespace differences
def 'without a namespace'() {
given:
def builder = new PactXmlBuilder('projects').build { root ->
@@ -20,14 +19,41 @@ class PactXmlBuilderSpec extends Specification {
}
when:
- def result = builder.toString()
+ def result = new XmlSlurper().parseText(builder.toString())
then:
- result == '''
- |
- |
- |
- |
- |'''.stripMargin()
+ result.@id == '1234'
+ result.project.size() == 2
+ result.project.each {
+ assert it.@id == '12'
+ assert it.@name == ' Project 1 '
+ assert it.@type == 'activity'
+ }
+ }
+
+ def 'elements with mutiple different types'() {
+ given:
+ def builder = new PactXmlBuilder('animals').build { root ->
+ root.eachLike('dog', 2, [
+ id: integer(1),
+ name: string('Canine')
+ ])
+ root.eachLike('cat', 3, [
+ id: integer(2),
+ name: string('Feline')
+ ])
+ root.eachLike('wolf', 1, [
+ id: integer(3),
+ name: string('Canine')
+ ])
+ }
+
+ when:
+ def result = new XmlSlurper().parseText(builder.toString())
+
+ then:
+ result.dog.size() == 2
+ result.cat.size() == 3
+ result.wolf.size() == 1
}
}
diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt
index 6fc0e4623d..8dbec61e5f 100755
--- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt
+++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/MatcherExecutor.kt
@@ -22,7 +22,9 @@ import org.apache.commons.lang3.time.DateUtils
import org.apache.tika.config.TikaConfig
import org.apache.tika.io.TikaInputStream
import org.apache.tika.metadata.Metadata
+import org.w3c.dom.Attr
import org.w3c.dom.Element
+import org.w3c.dom.Node
import org.w3c.dom.Text
import java.math.BigDecimal
import java.math.BigInteger
@@ -49,6 +51,7 @@ fun typeOf(value: Any?): String {
return when (value) {
null -> "Null"
is JsonValue -> value.type()
+ is Attr -> "XmlAttr"
else -> value.javaClass.simpleName
}
}
@@ -58,6 +61,7 @@ fun safeToString(value: Any?): String {
null -> ""
is Text -> value.wholeText
is Element -> value.textContent
+ is Attr -> value.nodeValue
is JsonValue -> value.asString()
else -> value.toString()
}
@@ -139,6 +143,8 @@ fun matchEquality(
val matches = when {
(actual == null || actual is JsonValue.Null) && (expected == null || expected is JsonValue.Null) -> true
actual is Element && expected is Element -> QualifiedName(actual) == QualifiedName(expected)
+ actual is Attr && expected is Attr -> QualifiedName(actual) == QualifiedName(expected) &&
+ actual.nodeValue == expected.nodeValue
else -> actual != null && actual == expected
}
logger.debug { "comparing ${valueOf(actual)} to ${valueOf(expected)} at $path -> $matches" }
@@ -183,7 +189,8 @@ fun matchType(
expected is JsonValue.Array && actual is JsonValue.Array ||
expected is Map<*, *> && actual is Map<*, *> ||
expected is JsonValue.Object && actual is JsonValue.Object ||
- expected is Element && actual is Element && QualifiedName(actual) == QualifiedName(expected)
+ expected is Element && actual is Element && QualifiedName(actual) == QualifiedName(expected) ||
+ expected is Attr && actual is Attr && QualifiedName(actual) == QualifiedName(expected)
) {
emptyList()
} else if (expected is JsonValue && actual is JsonValue &&
@@ -217,7 +224,8 @@ fun matchNumber(
when (numberType) {
NumberTypeMatcher.NumberType.NUMBER -> {
logger.debug { "comparing type of ${valueOf(actual)} (${typeOf(actual)}) to a number at $path" }
- if (actual is JsonValue && !actual.isNumber || actual !is JsonValue && actual !is Number) {
+ if (actual is JsonValue && !actual.isNumber || actual is Attr && actual.nodeValue.matches(decimalRegex) ||
+ actual !is JsonValue && actual !is Node && actual !is Number) {
return listOf(mismatchFactory.create(expected, actual,
"Expected ${valueOf(actual)} (${typeOf(actual)}) to be a number", path))
}
@@ -252,6 +260,7 @@ fun matchDecimal(actual: Any?): Boolean {
bigDecimal == BigDecimal.ZERO || bigDecimal.scale() > 0
}
actual is JsonValue.Integer -> decimalRegex.matches(actual.asString())
+ actual is Attr -> decimalRegex.matches(actual.nodeValue)
else -> false
}
logger.debug { "${valueOf(actual)} (${typeOf(actual)}) matches decimal number -> $result" }
@@ -266,6 +275,7 @@ fun matchInteger(actual: Any?): Boolean {
actual is JsonValue.Integer -> true
actual is BigDecimal && actual.scale() == 0 -> true
actual is JsonValue.Decimal -> integerRegex.matches(actual.asString())
+ actual is Attr -> integerRegex.matches(actual.nodeValue)
else -> false
}
logger.debug { "${valueOf(actual)} (${typeOf(actual)}) matches integer -> $result" }
diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt
index aa671c454b..be33286911 100755
--- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt
+++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/Matchers.kt
@@ -5,6 +5,7 @@ import au.com.dius.pact.core.matchers.util.tails
import au.com.dius.pact.core.model.PathToken
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup
import au.com.dius.pact.core.model.matchingrules.MatchingRules
+import au.com.dius.pact.core.model.matchingrules.TypeMatcher
import au.com.dius.pact.core.model.parsePath
import mu.KLogging
import java.util.Comparator
@@ -134,4 +135,9 @@ object Matchers : KLogging() {
matcherCategory.matchingRules.values.first()
}
}
+
+ fun typeMatcherDefined(category: String, path: List, matchingRules: MatchingRules): Boolean {
+ val resolvedMatchers = resolveMatchers(matchingRules, category, path, Comparator.naturalOrder())
+ return resolvedMatchers.allMatchingRules().any { it is TypeMatcher }
+ }
}
diff --git a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/XmlBodyMatcher.kt b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/XmlBodyMatcher.kt
index 70ffbc8949..db5afea845 100755
--- a/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/XmlBodyMatcher.kt
+++ b/core/matchers/src/main/kotlin/au/com/dius/pact/core/matchers/XmlBodyMatcher.kt
@@ -1,6 +1,5 @@
package au.com.dius.pact.core.matchers
-import au.com.dius.pact.core.matchers.util.padTo
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.matchingrules.MatchingRules
import au.com.dius.pact.core.support.zipAll
@@ -140,46 +139,64 @@ object XmlBodyMatcher : BodyMatcher, KLogging() {
allowUnexpectedKeys: Boolean,
matchers: MatchingRules
): List {
- var expectedChildren = asList(expected.childNodes).filter { n -> n.nodeType == ELEMENT_NODE }
+ val expectedChildren = asList(expected.childNodes).filter { n -> n.nodeType == ELEMENT_NODE }
val actualChildren = asList(actual.childNodes).filter { n -> n.nodeType == ELEMENT_NODE }
- val mismatches = if (Matchers.matcherDefined("body", path, matchers)) {
- if (expectedChildren.isNotEmpty()) expectedChildren = expectedChildren.padTo(actualChildren.size, expectedChildren.first())
- emptyList()
- } else if (expectedChildren.isEmpty() && actualChildren.isNotEmpty() && !allowUnexpectedKeys) {
- listOf(BodyMismatch(expected, actual,
+ val mismatches = mutableListOf()
+ if (expectedChildren.isEmpty() && actualChildren.isNotEmpty() && !allowUnexpectedKeys) {
+ mismatches.add(BodyMismatch(expected, actual,
"Expected an empty List but received ${actualChildren.size} child nodes",
path.joinToString(".")))
- } else {
- emptyList()
}
- val actualChildrenByQName = actualChildren.groupBy { QualifiedName(it) }
- return mismatches + expectedChildren
+ val expectedChildrenByQName = expectedChildren.groupBy { QualifiedName(it) }.toMutableMap()
+ mismatches.addAll(actualChildren
.groupBy { QualifiedName(it) }
.flatMap { e ->
- if (actualChildrenByQName.contains(e.key)) {
- e.value.zipAll(actualChildrenByQName.getValue(e.key)).mapIndexed { index, comp ->
- val expectedNode = comp.first
- val actualNode = comp.second
- when {
- expectedNode == null -> if (allowUnexpectedKeys || actualNode == null) {
- emptyList()
- } else {
- listOf(BodyMismatch(expected, actual,
- "Unexpected child <${e.key}/>",
- (path + actualNode.nodeName + index.toString()).joinToString(".")))
- }
- actualNode == null -> listOf(BodyMismatch(expected, actual,
- "Expected child <${e.key}/> but was missing",
- (path + expectedNode.nodeName + index.toString()).joinToString(".")))
- else -> compareNode(path, expectedNode, actualNode, allowUnexpectedKeys, matchers)
+ val childPath = path + e.key.toString()
+ if (expectedChildrenByQName.contains(e.key)) {
+ val expectedChild = expectedChildrenByQName.remove(e.key)!!
+ if (Matchers.matcherDefined("body", childPath, matchers)) {
+ val list = mutableListOf()
+ logger.debug { "compareChild: Matcher defined for path $childPath" }
+ e.value.forEach { actualChild ->
+ list.addAll(Matchers.domatch(matchers, "body", childPath, actualChild, expectedChild.first(),
+ BodyMismatchFactory))
+ list.addAll(compareNode(path, expectedChild.first(), actualChild, allowUnexpectedKeys, matchers))
}
- }.flatten()
- } else {
+ list
+ } else {
+ expectedChild.zipAll(e.value).mapIndexed { index, comp ->
+ val expectedNode = comp.first
+ val actualNode = comp.second
+ when {
+ expectedNode == null -> if (allowUnexpectedKeys || actualNode == null) {
+ emptyList()
+ } else {
+ listOf(BodyMismatch(expected, actual,
+ "Unexpected child <${e.key}/>",
+ (path + actualNode.nodeName + index.toString()).joinToString(".")))
+ }
+ actualNode == null -> listOf(BodyMismatch(expected, actual,
+ "Expected child <${e.key}/> but was missing",
+ (path + expectedNode.nodeName + index.toString()).joinToString(".")))
+ else -> compareNode(path, expectedNode, actualNode, allowUnexpectedKeys, matchers)
+ }
+ }.flatten()
+ }
+ } else if (!allowUnexpectedKeys || Matchers.typeMatcherDefined("body", childPath, matchers)) {
listOf(BodyMismatch(expected, actual,
- "Expected child <${e.key}/> but was missing", path.joinToString(".")))
+ "Unexpected child <${e.key}/>", path.joinToString(".")))
+ } else {
+ emptyList()
}
+ })
+ if (expectedChildrenByQName.isNotEmpty()) {
+ expectedChildrenByQName.keys.forEach {
+ mismatches.add(BodyMismatch(expected, actual, "Expected child <$it/> but was missing",
+ path.joinToString(".")))
}
+ }
+ return mismatches
}
private fun compareAttributes(
@@ -216,27 +233,27 @@ object XmlBodyMatcher : BodyMatcher, KLogging() {
logger.debug { "compareText: Matcher defined for path $attrPath" }
Matchers.domatch(matchers, "body", attrPath, attr.value, actualVal, BodyMismatchFactory)
}
- attr.value != actualVal ->
- listOf(BodyMismatch(expected, actual, "Expected ${attr.key}='${attr.value}' but received $actualVal",
+ attr.value.nodeValue != actualVal?.nodeValue ->
+ listOf(BodyMismatch(expected, actual, "Expected ${attr.key}='${attr.value.nodeValue}' but received ${attr.key}='${actualVal?.nodeValue}'",
attrPath.joinToString(".")))
else -> emptyList()
}
} else {
- listOf(BodyMismatch(expected, actual, "Expected ${attr.key}='${attr.value}' but was missing",
+ listOf(BodyMismatch(expected, actual, "Expected ${attr.key}='${attr.value.nodeValue}' but was missing",
appendAttribute(path, attr.key).joinToString(".")))
}
}
}
}
- private fun attributesToMap(attributes: NamedNodeMap?): Map {
+ private fun attributesToMap(attributes: NamedNodeMap?): Map {
return if (attributes == null) {
emptyMap()
} else {
(0 until attributes.length)
.map { attributes.item(it) }
.filter { it.namespaceURI != XMLConstants.XMLNS_ATTRIBUTE_NS_URI }
- .map { QualifiedName(it) to it.nodeValue }
+ .map { QualifiedName(it) to it }
.toMap()
}
}
diff --git a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/XmlBodyMatcherSpec.groovy b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/XmlBodyMatcherSpec.groovy
index 8b550bb4e1..cb0d4f399d 100644
--- a/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/XmlBodyMatcherSpec.groovy
+++ b/core/matchers/src/test/groovy/au/com/dius/pact/core/matchers/XmlBodyMatcherSpec.groovy
@@ -2,7 +2,9 @@ package au.com.dius.pact.core.matchers
import au.com.dius.pact.core.model.OptionalBody
import au.com.dius.pact.core.model.matchingrules.MatchingRulesImpl
+import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.core.model.matchingrules.RegexMatcher
+import au.com.dius.pact.core.model.matchingrules.TypeMatcher
import spock.lang.Issue
import spock.lang.Specification
import spock.lang.Unroll
@@ -128,8 +130,8 @@ class XmlBodyMatcherSpec extends Specification {
then:
!mismatches.empty
- mismatches*.mismatch == ['Expected an empty List but received 1 child nodes']
- mismatches*.path == ['$.foo']
+ mismatches*.mismatch == ['Expected an empty List but received 1 child nodes', 'Unexpected child ']
+ mismatches*.path == ['$.foo', '$.foo']
}
def 'matching XML bodies - returns a mismatch - when comparing a list to one with with different size'() {
@@ -156,8 +158,8 @@ class XmlBodyMatcherSpec extends Specification {
then:
!mismatches.empty
- mismatches*.mismatch == ['Expected child but was missing']
- mismatches*.path == ['$.foo.three.1']
+ mismatches*.mismatch == ['Expected child but was missing', 'Unexpected child ']
+ mismatches*.path == ['$.foo.three.1', '$.foo']
}
def 'matching XML bodies - returns no mismatch - when comparing a list to one where the items are in the wrong order'() {
@@ -237,7 +239,7 @@ class XmlBodyMatcherSpec extends Specification {
then:
!mismatches.empty
- mismatches*.mismatch == ["Expected something='100' but received 101"]
+ mismatches*.mismatch == ["Expected something='100' but received something='101'"]
mismatches*.path == ['$.foo.@something']
}
@@ -275,7 +277,7 @@ class XmlBodyMatcherSpec extends Specification {
' Manchester\t ').bytes)
expect:
- matcher.matchBody(expectedBody, actualBody, false, matchers).empty
+ matcher.matchBody(expectedBody, actualBody, true, matchers).empty
}
@Issue('#975')
@@ -475,4 +477,92 @@ class XmlBodyMatcherSpec extends Specification {
matcher.matchBody(expectedBody, actualBody, false, matchers).empty
}
+ def 'when an element has different types of children but we allow unexpected keys'() {
+ given:
+ def actual = '''
+
+
+
+
+
+
+
+
+ '''
+ actualBody = OptionalBody.body(actual.bytes)
+
+ def expected = '''
+
+
+
+
+
+ '''
+ expectedBody = OptionalBody.body(expected.bytes)
+
+ expect:
+ matcher.matchBody(expectedBody, actualBody, true, matchers).empty
+ }
+
+ def 'when an element has different types of children but we do not allow unexpected keys'() {
+ given:
+ def actual = '''
+
+
+
+
+
+
+
+
+ '''
+ actualBody = OptionalBody.body(actual.bytes)
+
+ def expected = '''
+
+
+
+
+
+ '''
+ expectedBody = OptionalBody.body(expected.bytes)
+
+ when:
+ def result = matcher.matchBody(expectedBody, actualBody, false, matchers)
+
+ then:
+ result.size() == 3
+ result*.description() == ['BodyMismatch: Unexpected child ', 'BodyMismatch: Unexpected child ',
+ 'BodyMismatch: Unexpected child ']
+ }
+
+ def 'type matcher when an element has different types of children'() {
+ given:
+ def actual = '''
+
+
+
+
+
+
+
+
+ '''
+ actualBody = OptionalBody.body(actual.bytes)
+
+ def expected = '''
+
+
+
+
+
+ '''
+ expectedBody = OptionalBody.body(expected.bytes)
+ matchers.addCategory('body')
+ .addRule("\$.animals.*", TypeMatcher.INSTANCE)
+ .addRule("\$.animals.*['@id']", new NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))
+
+ expect:
+ matcher.matchBody(expectedBody, actualBody, false, matchers).empty
+ }
}
diff --git a/core/model/src/test/groovy/au/com/dius/pact/core/model/json/JsonPerformanceSpec.groovy b/core/model/src/test/groovy/au/com/dius/pact/core/model/json/JsonPerformanceSpec.groovy
index e84fadac6d..9a240ff997 100644
--- a/core/model/src/test/groovy/au/com/dius/pact/core/model/json/JsonPerformanceSpec.groovy
+++ b/core/model/src/test/groovy/au/com/dius/pact/core/model/json/JsonPerformanceSpec.groovy
@@ -8,8 +8,7 @@ import org.junit.Ignore
import org.junit.Test
@CompileStatic
-//@Ignore
-@SuppressWarnings('ExplicitCallToDivMethod')
+@Ignore
class JsonPerformanceSpec {
private final Map jsonFiles = [:]
@@ -55,7 +54,7 @@ class JsonPerformanceSpec {
BigInteger total = 0
result.keySet().toSorted().each { key ->
println("${key.padRight(40)}: ${result[key] / 100}")
- total += result[key].div(100).toBigInteger()
+ total += (result[key] / 100).toBigInteger()
}
println("${'TOTAL'.padRight(40)}: ${total}")
println()
@@ -81,7 +80,7 @@ class JsonPerformanceSpec {
BigInteger total = 0
result.keySet().toSorted().each { key ->
println("${key.padRight(40)}: ${result[key] / 100}")
- total += result[key].div(100).toBigInteger()
+ total += (result[key] / 100).toBigInteger()
}
println("${'TOTAL'.padRight(40)}: ${total}")
println()
@@ -108,7 +107,7 @@ class JsonPerformanceSpec {
BigInteger total = 0
result.keySet().toSorted().each { key ->
println("${key.padRight(40)}: ${result[key] / 100}")
- total += result[key].div(100).toBigInteger()
+ total += (result[key] / 100).toBigInteger()
}
println("${'TOTAL'.padRight(40)}: ${total}")
println()