From 024be90aeffe1475224a51d8b4b797b87e940abd Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 13:37:59 +0100 Subject: [PATCH 1/7] Add comparison of attributes --- .../purpledragon/xml/compare/XmlCompare.scala | 25 +++++++++++++ .../xml/compare/XmlCompareSpec.scala | 35 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala index 474605b..fa6e7c7 100644 --- a/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala +++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala @@ -51,6 +51,7 @@ object XmlCompare { private def compareNodes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = { val checks: Seq[Check] = Seq( compareNamespace, + compareAttributes, compareText, compareChildren ) @@ -78,6 +79,30 @@ object XmlCompare { } } + private def compareAttributes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = { + def extractAttributes(node: Node): Map[String, String] = { + node.attributes.foldLeft(Map.empty[String, String]) { (acc, attrib) => + acc + (attrib.key -> attrib.value.text) + } + } + + val leftAttributes: Map[String, String] = extractAttributes(left) + val rightAttributes = extractAttributes(right) + + if (leftAttributes.keySet != rightAttributes.keySet) { + XmlDiffers("different attribute names", leftAttributes.keySet, rightAttributes.keySet, extendPath(path, left)) + } else { + leftAttributes.keySet.toSeq.sorted collectFirst { + case name if leftAttributes(name) != rightAttributes(name) => + XmlDiffers( + s"different value for attribute '$name'", + leftAttributes(name), + rightAttributes(name), + extendPath(path, left)) + } getOrElse XmlEqual + } + } + private def compareText(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = { def extractText(node: Node): String = node.child.collect({ case a: Atom[_] => a }).map(_.text.trim).mkString val leftText = extractText(left) diff --git a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala index 19de2a9..f1c8bbd 100644 --- a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala +++ b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala @@ -80,6 +80,41 @@ class XmlCompareSpec extends FlatSpec with Matchers { ) } + it should "match with same attributes" in { + XmlCompare.compare(, ) shouldBe XmlEqual + } + + it should "match with same attributes in different order" in { + XmlCompare.compare(, ) shouldBe XmlEqual + } + + it should "not-match with different attribute names" in { + XmlCompare.compare(, ) shouldBe XmlDiffers( + "different attribute names", + Set("value"), + Set("cost"), + Seq("test") + ) + } + + it should "not-match with different attribute count" in { + XmlCompare.compare(, ) shouldBe XmlDiffers( + "different attribute names", + Set("first", "second"), + Set("first"), + Seq("test") + ) + } + + it should "not-match with different attribute value" in { + XmlCompare.compare(, ) shouldBe XmlDiffers( + "different value for attribute 'value'", + "a", + "b", + Seq("test") + ) + } + it should "not-match with multiple errors" in { XmlCompare.compare( text-1text-2, From a4f20e4613574ce032a30fd2fad5b864a69c7646 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 13:48:14 +0100 Subject: [PATCH 2/7] Update travis config --- .travis.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index f84b68f..3a5502c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,22 @@ language: scala sudo: false -scala: - - 2.11.11 - - 2.12.7 - - 2.13.0-M5 jdk: - - oraclejdk8 + - openjdk8 + - openjdk11 -# cache config taken from http://www.scala-sbt.org/0.13/docs/Travis-CI-with-sbt.html cache: directories: - - $HOME/.ivy2/cache - - $HOME/.sbt/boot/ + - $HOME/.cache + - $HOME/.sbt before_cache: - - find $HOME/.ivy2 -name "ivydata-*.properties" -delete + - find $HOME/.ivy2 -name "*.lock" -delete - find $HOME/.sbt -name "*.lock" -delete # build with scoverage report and upload to codecov script: - - sbt ++$TRAVIS_SCALA_VERSION coverage test coverageReport coverageAggregate + - sbt coverage test coverageReport coverageAggregate + after_success: - bash <(curl -s https://codecov.io/bash) From 70e979893dc55406bcdf161534dac21040c8ed61 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 14:05:04 +0100 Subject: [PATCH 3/7] Workaround scoverage issue on Scala 2.13.1 --- .travis.yml | 9 +++++++-- project/plugins.sbt | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a5502c..ab31439 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,13 @@ before_cache: - find $HOME/.sbt -name "*.lock" -delete # build with scoverage report and upload to codecov -script: - - sbt coverage test coverageReport coverageAggregate + +jobs: + include: + - stage: Test (all Scala versions) + script: sbt +test + - stage: Coverage (2.12 only) + script: sbt ++2.12.10 coverage test coverageReport coverageAggregate after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7d73889..94168e4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ // code style addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0-M5") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.0") addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.0.0") From 454f265469b3cf22189a6e29fb08700844736056 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 14:19:28 +0100 Subject: [PATCH 4/7] Travis tweaks --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab31439..463c3f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,10 @@ before_cache: jobs: include: - - stage: Test (all Scala versions) + # run tests against all Scala versions & JDKs + - stage: Test script: sbt +test - - stage: Coverage (2.12 only) - script: sbt ++2.12.10 coverage test coverageReport coverageAggregate + # coverage only runs on 2.12 openjdk8 + - stage: Coverage + script: sbt ++2.12.10 coverage test coverageReport coverageAggregate && bash <(curl -s https://codecov.io/bash) -after_success: - - bash <(curl -s https://codecov.io/bash) From c73ac749aa675779950872fdaebfe339c7e03bd2 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 14:53:26 +0100 Subject: [PATCH 5/7] Add option for strict attribute order checks --- .../purpledragon/xml/compare/XmlCompare.scala | 27 +++++++++---------- .../xml/compare/options/DiffOption.scala | 14 ++++++++++ .../xml/compare/XmlCompareSpec.scala | 21 ++++++++++++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala index fa6e7c7..4fe8c16 100644 --- a/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala +++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/XmlCompare.scala @@ -80,25 +80,24 @@ object XmlCompare { } private def compareAttributes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = { - def extractAttributes(node: Node): Map[String, String] = { - node.attributes.foldLeft(Map.empty[String, String]) { (acc, attrib) => - acc + (attrib.key -> attrib.value.text) + def extractAttributes(node: Node): (Seq[String], Map[String, String]) = { + node.attributes.foldLeft(Seq.empty[String], Map.empty[String, String]) { + case ((keys, attribs), attrib) => + (keys :+ attrib.key, attribs + (attrib.key -> attrib.value.text)) } } - val leftAttributes: Map[String, String] = extractAttributes(left) - val rightAttributes = extractAttributes(right) + val (leftKeys, leftMap) = extractAttributes(left) + val (rightKeys, rightMap) = extractAttributes(right) - if (leftAttributes.keySet != rightAttributes.keySet) { - XmlDiffers("different attribute names", leftAttributes.keySet, rightAttributes.keySet, extendPath(path, left)) + if (leftKeys.sorted != rightKeys.sorted) { + XmlDiffers("different attribute names", leftKeys.sorted, rightKeys.sorted, extendPath(path, left)) + } else if (options.contains(StrictAttributeOrdering) && leftKeys != rightKeys) { + XmlDiffers("different attribute ordering", leftKeys, rightKeys, extendPath(path, left)) } else { - leftAttributes.keySet.toSeq.sorted collectFirst { - case name if leftAttributes(name) != rightAttributes(name) => - XmlDiffers( - s"different value for attribute '$name'", - leftAttributes(name), - rightAttributes(name), - extendPath(path, left)) + leftKeys.sorted collectFirst { + case name if leftMap(name) != rightMap(name) => + XmlDiffers(s"different value for attribute '$name'", leftMap(name), rightMap(name), extendPath(path, left)) } getOrElse XmlEqual } } diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala index bdc8090..aaa523d 100644 --- a/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala +++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala @@ -49,4 +49,18 @@ object DiffOption extends Enumeration { * }}} */ val IgnoreNamespace: DiffOption.Value = Value + + /** + * Require element attributes have the same ordering. + * + * Enabling this make this: + * {{{ + * + * }}} + * not equal to: + * {{{ + * + * }}} + */ + val StrictAttributeOrdering: DiffOption.Value = Value } diff --git a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala index f1c8bbd..5dd9389 100644 --- a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala +++ b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala @@ -91,8 +91,8 @@ class XmlCompareSpec extends FlatSpec with Matchers { it should "not-match with different attribute names" in { XmlCompare.compare(, ) shouldBe XmlDiffers( "different attribute names", - Set("value"), - Set("cost"), + Seq("value"), + Seq("cost"), Seq("test") ) } @@ -100,8 +100,8 @@ class XmlCompareSpec extends FlatSpec with Matchers { it should "not-match with different attribute count" in { XmlCompare.compare(, ) shouldBe XmlDiffers( "different attribute names", - Set("first", "second"), - Set("first"), + Seq("first", "second"), + Seq("first"), Seq("test") ) } @@ -150,4 +150,17 @@ class XmlCompareSpec extends FlatSpec with Matchers { , Set(IgnoreNamespace)) shouldBe XmlEqual } + + "compare with StrictAttributeOrder" should "match with same attributes" in { + XmlCompare.compare(, , Set(StrictAttributeOrdering)) shouldBe XmlEqual + } + + it should "not-match with attributes in different order" in { + XmlCompare.compare(, , Set(StrictAttributeOrdering)) shouldBe XmlDiffers( + "different attribute ordering", + Seq("first", "second"), + Seq("second", "first"), + Seq("test") + ) + } } From 03257253645682f95c26f105a1b857cadec930c4 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 15:38:39 +0100 Subject: [PATCH 6/7] Document XML comparison --- build.sbt | 5 +- src/main/paradox/comparing-xml.md | 119 ++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index cfd10df..728f0e4 100644 --- a/build.sbt +++ b/build.sbt @@ -60,9 +60,10 @@ lazy val root = Project("scala-xml-compare", file(".")) // site/paradox siteSubdirName in ScalaUnidoc := "api", addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), siteSubdirName in ScalaUnidoc), - paradoxProperties in Paradox ++= Map( - "scaladoc.software.purpledragon.xml.base_url" -> ".../api" + paradoxProperties in Compile ++= Map( + "scaladoc.base_url" -> ".../api" ), + paradoxNavigationDepth := 3, scalacOptions in Compile in doc ++= Seq( "-doc-root-content", baseDirectory.value + "/root-scaladoc.txt" diff --git a/src/main/paradox/comparing-xml.md b/src/main/paradox/comparing-xml.md index d150a8b..0f82ff6 100644 --- a/src/main/paradox/comparing-xml.md +++ b/src/main/paradox/comparing-xml.md @@ -1,8 +1,127 @@ # Comparing XML +A simple XML comparison can be performed using +@scaladoc[`XmlCompare.compare`](software.purpledragon.xml.compare.XmlCompare$): + +```scala +// using XML literals: +val result = XmlCompare.compare(, ) // == XmlEqual + +// or from a file: +val success = XmlCompare.compare(, XML.loadFile("result.xml")) +``` + +The way that XML is compared can be customised by supplying a `Set` of +@scaladoc[`DiffOption`s](software.purpledragon.xml.compare.options.DiffOption$): + +```scala +// this would result an XmlEqual +XmlCompare.compare( + , + ) + +// this would result an XmlDiffers +XmlCompare.compare( + , + , + Set(DiffOption.StrictAttributeOrdering)) +``` + +@ref:[Comparison Options](#comparison-options) contains details of the supported options. + +## Comparison Results + +`XmlCompare.compare` returns an @scaladoc[`XmlDiff`](software.purpledragon.xml.compare.XmlDiff) that will either be +@scaladoc[`XmlEqual`](software.purpledragon.xml.compare.XmlEqual) or a detailed +@scaladoc[`XmlDiffers`](software.purpledragon.xml.compare.XmlDiffers). If a simple +pass/fail check is required then the `isEqual` method can be called on the result. + +### XmlEqual + +An `XmlEqual` result signifies that no differences in the XML were found. + +### XmlDiffers + +An `XmlDiffers` result will contain the _first_ difference found in the XML. The `reason` property will have a +human-readable reason for the difference, `left` & `right` will have the differences and `failurePath` will have the +path segments to the difference. + +For example: + +```scala +XmlDiffers( + "different attribute ordering", + Seq("first", "second"), + Seq("second", "first"), + Seq("test") +) +``` ## Comparison Options By default the following options are used: * `IgnoreNamespacePrefix` + +### IgnoreNamespacePrefix + +If enabled the prefixes associated with namespaces will be ignored. Differing namespaces will still cause a comparison +error. + +#### Example + +This: +```xml +5 +``` + +would be considered equal to: +```xml +5 +``` + +### IgnoreNamespace + +If enabled then namespaces will be ignored completely. + +#### Example + +This: +```xml +5 +``` + +would be considered equal to: +```xml +5 +``` + +### StrictAttributeOrdering + +This adds an additional comparison on the ordering of element attributes. The presence of attributes will be checked +_before_ the ordering. + +#### Example + +This: + +```xml + +``` + +would not be equal to: + +```xml + +``` + +and would result in the following failure: + +```scala +XmlDiffers( + "different attribute ordering", + Seq("first", "second"), + Seq("second", "first"), + Seq("test") +) +``` \ No newline at end of file From fea58842ed98fc87d92e26b8b7d2194554b68e34 Mon Sep 17 00:00:00 2001 From: Michael Stringer Date: Thu, 26 Sep 2019 15:47:30 +0100 Subject: [PATCH 7/7] Header create/scalafmt --- .../xml/compare/options/DiffOption.scala | 2 +- .../xml/compare/XmlCompareSpec.scala | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala index aaa523d..80960f0 100644 --- a/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala +++ b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala @@ -51,7 +51,7 @@ object DiffOption extends Enumeration { val IgnoreNamespace: DiffOption.Value = Value /** - * Require element attributes have the same ordering. + * Require element attributes have the same ordering. * * Enabling this make this: * {{{ diff --git a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala index 5dd9389..e77f58a 100644 --- a/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala +++ b/xml-compare/src/test/scala/software/purpledragon/xml/compare/XmlCompareSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Michael Stringer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package software.purpledragon.xml.compare import org.scalatest.{FlatSpec, Matchers}