Skip to content

Commit

Permalink
chore: update the XML matching to handle when child elements have dif…
Browse files Browse the repository at this point in the history
…ferent types of children
  • Loading branch information
Ronald Holshausen committed Jun 19, 2020
1 parent 755a968 commit 89e0db2
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 57 deletions.
@@ -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 ->
Expand All @@ -20,14 +19,41 @@ class PactXmlBuilderSpec extends Specification {
}

when:
def result = builder.toString()
def result = new XmlSlurper().parseText(builder.toString())

then:
result == '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|<projects id="1234">
|<project id="12" name=" Project 1 " type="activity"/>
|<project id="12" name=" Project 1 " type="activity"/>
|</projects>
|'''.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
}
}
Expand Up @@ -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
Expand All @@ -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
}
}
Expand All @@ -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()
}
Expand Down Expand Up @@ -139,6 +143,8 @@ fun <M : Mismatch> 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" }
Expand Down Expand Up @@ -183,7 +189,8 @@ fun <M : Mismatch> 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 &&
Expand Down Expand Up @@ -217,7 +224,8 @@ fun <M : Mismatch> 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))
}
Expand Down Expand Up @@ -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" }
Expand All @@ -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" }
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -134,4 +135,9 @@ object Matchers : KLogging() {
matcherCategory.matchingRules.values.first()
}
}

fun typeMatcherDefined(category: String, path: List<String>, matchingRules: MatchingRules): Boolean {
val resolvedMatchers = resolveMatchers(matchingRules, category, path, Comparator.naturalOrder())
return resolvedMatchers.allMatchingRules().any { it is TypeMatcher }
}
}
@@ -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
Expand Down Expand Up @@ -140,46 +139,64 @@ object XmlBodyMatcher : BodyMatcher, KLogging() {
allowUnexpectedKeys: Boolean,
matchers: MatchingRules
): List<BodyMismatch> {
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<BodyMismatch>()
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<BodyMismatch>()
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(
Expand Down Expand Up @@ -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<QualifiedName, String> {
private fun attributesToMap(attributes: NamedNodeMap?): Map<QualifiedName, Node> {
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()
}
}
Expand Down

0 comments on commit 89e0db2

Please sign in to comment.