diff --git a/.travis.yml b/.travis.yml
index f84b68f..463c3f1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,25 +1,27 @@
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
-after_success:
- - bash <(curl -s https://codecov.io/bash)
+
+jobs:
+ include:
+ # run tests against all Scala versions & JDKs
+ - stage: Test
+ script: sbt +test
+ # 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)
+
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/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")
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
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..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
@@ -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,29 @@ object XmlCompare {
}
}
+ private def compareAttributes(left: Node, right: Node, options: DiffOptions, path: Seq[String]): XmlDiff = {
+ 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 (leftKeys, leftMap) = extractAttributes(left)
+ val (rightKeys, rightMap) = extractAttributes(right)
+
+ 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 {
+ 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
+ }
+ }
+
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/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala b/xml-compare/src/main/scala/software/purpledragon/xml/compare/options/DiffOption.scala
index bdc8090..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
@@ -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 19de2a9..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}
@@ -80,6 +96,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",
+ Seq("value"),
+ Seq("cost"),
+ Seq("test")
+ )
+ }
+
+ it should "not-match with different attribute count" in {
+ XmlCompare.compare(, ) shouldBe XmlDiffers(
+ "different attribute names",
+ Seq("first", "second"),
+ Seq("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,
@@ -115,4 +166,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")
+ )
+ }
}