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") + ) + } }