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()