module CompareXML

Constants

DEFAULTS_OPTS

default options used by the module; all of these can be overridden

EQUIVALENT

used internally only in order to differentiate equivalence for inequivalence

MISSING_ATTRIBUTE

a list of all possible inequivalence types for nodes these are returned in the differences array to differentiate error types.

MISSING_NODE
UNEQUAL_ATTRIBUTES
UNEQUAL_COMMENTS
UNEQUAL_DOCUMENTS
UNEQUAL_ELEMENTS
UNEQUAL_NODES_TYPES
UNEQUAL_TEXT_CONTENTS
VERSION

Public Class Methods

equivalent?(n1, n2, opts = {}, childopts = {}, diffchildren = false) click to toggle source

Determines whether two XML documents or fragments are equal to each other. The two parameters could be any type of XML documents, or fragments or node sets or even text nodes - any subclass of Nokogiri::XML::Node.

@param [Nokogiri::XML::Element] n1 left node element
@param [Nokogiri::XML::Element] n2 right node element
@param [Hash] opts user-overridden options
@param [Hash] childopts user-overridden options used for the child nodes
@param [Bool] diffchildren use different options for the child nodes

@return true if equal, [Array] differences otherwise
# File lib/compare-xml.rb, line 80
def equivalent?(n1, n2, opts = {}, childopts = {}, diffchildren = false)
  opts = DEFAULTS_OPTS.merge(opts)
  childopts = DEFAULTS_OPTS.merge(childopts)
  differences = []
  result = diffchildren ? compareNodes(n1, n2, opts, differences, childopts, diffchildren) : compareNodes(n1, n2, opts, differences)
  opts[:verbose] ? differences : result == EQUIVALENT
end

Private Class Methods

addDifference(node1, node2, diff1, diff2, opts, differences) click to toggle source

Strips the whitespace (from beginning and end) and collapses it, i.e. multiple spaces, new lines and tabs are all collapsed to a single space.

@param [Nokogiri::XML::Node] node1 left node
@param [Nokogiri::XML::Node] node2 right node
@param [String] diff1 left diffing value
@param [String] diff2 right diffing value
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages

@return collapsed string
# File lib/compare-xml.rb, line 497
def addDifference(node1, node2, diff1, diff2, opts, differences)
  differences << { node1: node1, node2: node2, diff1: diff1, diff2: diff2 } if opts[:verbose]
end
attrContentExcluded?(a1, a2, opts) click to toggle source

Checks whether two given attributes should be excluded, based on their content. Checks whether both attributes contain content that should be excluded, and returns true only if an excluded string is contained in both attribute values.

@param [Nokogiri::XML::Attr] a1 left attribute
@param [Nokogiri::XML::Attr] a2 right attribute
@param [Hash] opts user-overridden options

@return true if excluded, false otherwise
# File lib/compare-xml.rb, line 451
def attrContentExcluded?(a1, a2, opts)
  a1_excluded = false
  a2_excluded = false
  opts[:ignore_attr_content].each do |content|
    a1_excluded ||= a1.value.include?(content)
    a2_excluded ||= a2.value.include?(content)
    return true if a1_excluded && a2_excluded
  end
  false
end
attrNameExcluded?(a1, a2, opts) click to toggle source

Checks whether two given attributes should be excluded, based on their content. Checks whether both attributes contain content that should be excluded, and returns true only if an excluded string is contained in both attribute values.

@param [Nokogiri::XML::Attr] a1 left attribute
@param [Nokogiri::XML::Attr] a2 right attribute
@param [Hash] opts user-overridden options

@return true if excluded, false otherwise
# File lib/compare-xml.rb, line 473
def attrNameExcluded?(a1, a2, opts)
  a1_excluded = false
  a2_excluded = false
  opts[:ignore_attrs_by_name].each do |name|
    a1_excluded ||= a1.to_s.include?(name)
    a2_excluded ||= a2.to_s.include?(name)
    return true if a1_excluded && a2_excluded
  end
  false
end
attrsExcluded?(a1, a2, opts) click to toggle source

Checks whether two given attributes should be excluded, based on a user-specified css rule. If true, only the specified attributes are ignored; all remaining attributes are still compared. The CSS rule is used to locate the node that contains the attributes to be excluded. The CSS rule MUST contain the name of the attribute to be ignored.

@param [Nokogiri::XML::Attr] a1 left attribute
@param [Nokogiri::XML::Attr] a2 right attribute
@param [Hash] opts user-overridden options

@return true if excluded, false otherwise
# File lib/compare-xml.rb, line 431
def attrsExcluded?(a1, a2, opts)
  opts[:ignore_attrs].each do |css|
    if css.include?(a1.name) && css.include?(a2.name)
      return true if a1.parent.xpath('../*').css(css).include?(a1.parent) && a2.parent.xpath('../*').css(css).include?(a2.parent)
    end
  end
  false
end
collapse(text) click to toggle source

Strips the whitespace (from beginning and end) and collapses it, i.e. multiple spaces, new lines and tabs are all collapsed to a single space.

@param [String] text string to collapse

@return collapsed string
# File lib/compare-xml.rb, line 509
def collapse(text)
  text = text.to_s unless text.is_a? String
  text.strip.gsub(/\s+/, ' ')
end
compareAttributeSets(n1, n2, a1_set, a2_set, opts, differences) click to toggle source

Compares two sets of Nokogiri::XML::Element attributes.

@param [Nokogiri::XML::Element] n1 left node element
@param [Nokogiri::XML::Element] n2 right node element
@param [Array] a1_set left attribute set
@param [Array] a2_set right attribute set
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 288
def compareAttributeSets(n1, n2, a1_set, a2_set, opts, differences)
  return false unless a1_set.length == a2_set.length || opts[:verbose]
  if opts[:ignore_attr_order]
    compareSortedAttributeSets(n1, n2, a1_set, a2_set, opts, differences)
  else
    compareUnsortedAttributeSets(n1, n2, a1_set, a2_set, opts, differences)
  end
end
compareAttributes(n1, n2, a1, a2, opts, differences, status = EQUIVALENT) click to toggle source

Compares two attributes by name and value.

@param [Nokogiri::XML::Element] n1 left node element
@param [Nokogiri::XML::Element] n2 right node element
@param [Nokogiri::XML::Attr] a1 left attribute
@param [Nokogiri::XML::Attr] a2 right attribute
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 375
def compareAttributes(n1, n2, a1, a2, opts, differences, status = EQUIVALENT)
  if a1.nil?
    status = MISSING_ATTRIBUTE
    addDifference(n1, n2, nil, "#{a2.name}=\"#{a2.value}\"", opts, differences)
  elsif a2.nil?
    status = MISSING_ATTRIBUTE
    addDifference(n1, n2, "#{a1.name}=\"#{a1.value}\"", nil, opts, differences)
  elsif a1.name == a2.name
    return status if attrNameExcluded?(a1.name, a2.name, opts)
    return status if attrsExcluded?(a1, a2, opts)
    return status if attrContentExcluded?(a1, a2, opts)
    if a1.value != a2.value
      status = UNEQUAL_ATTRIBUTES
      addDifference(n1, n2, "#{a1.name}=\"#{a1.value}\"", "#{a2.name}=\"#{a2.value}\"", opts, differences)
    end
  else
    status = UNEQUAL_ATTRIBUTES
    addDifference(n1, n2, "#{a1.name}=\"#{a1.value}\"", "#{a2.name}=\"#{a2.value}\"", opts, differences)
  end
  status
end
compareChildren(n1_set, n2_set, opts, differences, diffchildren = false, status = EQUIVALENT) click to toggle source

Compares two sets of Nokogiri::XML::NodeSet elements.

@param [Nokogiri::XML::NodeSet] n1_set left set of Nokogiri::XML::Node elements
@param [Nokogiri::XML::NodeSet] n2_set right set of Nokogiri::XML::Node elements
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [Bool] diffchildren use different options for the child nodes
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 196
def compareChildren(n1_set, n2_set, opts, differences, diffchildren = false, status = EQUIVALENT)
  i = 0; j = 0
  return if opts[:ignore_children]
  while i < n1_set.length || j < n2_set.length
    if !n1_set[i].nil? && nodeExcluded?(n1_set[i], opts)
      i += 1 # increment counter if left node is excluded
    elsif !n2_set[j].nil? && nodeExcluded?(n2_set[j], opts)
      j += 1 # increment counter if right node is excluded
    else
      result = diffchildren ? compareNodes(n1_set[i], n2_set[j], opts, differences, opts, diffchildren) : compareNodes(n1_set[i], n2_set[j], opts, differences)
      status = result unless result == EQUIVALENT

      # return false so that this subtree could halt comparison on error
      # but neighbours of parents' subtrees could still be compared (in verbose mode)
      return false if [UNEQUAL_NODES_TYPES, UNEQUAL_ELEMENTS].include? status

      # stop execution if a single error is found (unless in verbose mode)
      break unless status == EQUIVALENT || opts[:verbose]

      # increment both counters when both nodes have been compared
      i += 1; j += 1
    end
    status
  end
end
compareCommentNodes(n1, n2, opts, differences, status = EQUIVALENT) click to toggle source

Compares two nodes of type Nokogiri::HTML::Comment.

@param [Nokogiri::XML::Comment] n1 left comment
@param [Nokogiri::XML::Comment] n2 right comment
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 146
def compareCommentNodes(n1, n2, opts, differences, status = EQUIVALENT)
  return true if opts[:ignore_comments]
  t1 = n1.content
  t2 = n2.content
  if opts[:collapse_whitespace]
    t1 = collapse(t1)
    t2 = collapse(t2)
  end
  unless t1 == t2
    status = UNEQUAL_COMMENTS
    addDifference(n1.parent, n2.parent, t1, t2, opts, differences)
  end
  status
end
compareDocumentNodes(n1, n2, opts, differences, childopts = {}, diffchildren = false, status = EQUIVALENT) click to toggle source

Compares two nodes of type Nokogiri::HTML::Document.

@param [Nokogiri::XML::Document] n1 left document
@param [Nokogiri::XML::Document] n2 right document
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [Hash] childopts user-overridden options used for the child nodes
@param [Bool] diffchildren use different options for the child nodes
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 174
def compareDocumentNodes(n1, n2, opts, differences, childopts = {}, diffchildren = false, status = EQUIVALENT)
  if n1.name == n2.name
    status = diffchildren ? compareChildren(n1.children, n2.children, childopts, differences, diffchildren) : compareChildren(n1.children, n2.children, opts, differences)
  else
    status = UNEQUAL_DOCUMENTS
    addDifference(n1, n2, n1, n2, opts, differences)
  end
  status
end
compareElementNodes(n1, n2, opts, differences, childopts = {}, diffchildren = false, status = EQUIVALENT) click to toggle source

Compares two nodes of type Nokogiri::XML::Element.

  • compares element attributes

  • recursively compares element children

    @param [Nokogiri::XML::Element] n1 left node element @param [Nokogiri::XML::Element] n2 right node element @param [Hash] opts user-overridden options @param [Array] differences inequivalence messages @param [Hash] childopts user-overridden options used for the child nodes @param [Bool] diffchildren use different options for the child nodes @param [int] status comparison status code (EQUIVALENT by default)

    @return type of equivalence (from equivalence constants)

# File lib/compare-xml.rb, line 237
def compareElementNodes(n1, n2, opts, differences, childopts = {}, diffchildren = false, status = EQUIVALENT)
  if n1.name == n2.name
    result = compareAttributeSets(n1, n2, n1.attribute_nodes, n2.attribute_nodes, opts, differences)
    return result unless result == EQUIVALENT || opts[:force_children] == true
    result = diffchildren ? compareChildren(n1.children, n2.children, childopts, differences, diffchildren) : compareChildren(n1.children, n2.children, opts, differences)
    status = result unless result == EQUIVALENT
  else
    status = UNEQUAL_ELEMENTS
    addDifference(n1, n2, n1.name, n2.name, opts, differences)
  end
  status
end
compareNodes(n1, n2, opts, differences, childopts = {}, diffchildren = false, status = EQUIVALENT) click to toggle source

Compares two nodes for equivalence. The nodes could be any subclass of Nokogiri::XML::Node including node sets and document fragments.

@param [Nokogiri::XML::Node] n1 left node
@param [Nokogiri::XML::Node] n2 right node
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [Hash] childopts user-overridden options used for the child nodes
@param [Bool] diffchildren use different options for the child nodes
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 104
def compareNodes(n1, n2, opts, differences, childopts = {}, diffchildren = false, status = EQUIVALENT)
  if n1.class == n2.class
    case n1
    when Nokogiri::XML::Comment
      status = compareCommentNodes(n1, n2, opts, differences)
    when Nokogiri::HTML::Document
      status = diffchildren ? compareDocumentNodes(n1, n2, opts, differences, childopts, diffchildren) : compareDocumentNodes(n1, n2, opts, differences)
    when Nokogiri::XML::Element
      status = diffchildren ? compareElementNodes(n1, n2, opts, differences, childopts, diffchildren) : compareElementNodes(n1, n2, opts, differences)
    when Nokogiri::XML::Text
      status = compareTextNodes(n1, n2, opts, differences)
    else
      raise 'Comparison only allowed between objects of type Nokogiri::XML::Node and Nokogiri::XML::NodeSet.' unless n1.is_a?(Nokogiri::XML::Node) || n1.is_a?(Nokogiri::XML::NodeSet)
      status = compareChildren(n1.children, n2.children, opts, differences)
    end
  elsif n1.nil? || n2.nil?
    status = MISSING_NODE
    addDifference(n1, n2, n1, n2, opts, differences)
  else
    status = UNEQUAL_NODES_TYPES
    if n1.is_a? Nokogiri::XML::Text
      addDifference(n1.parent, n2, n1, n2, opts, differences)
    elsif n2.is_a? Nokogiri::XML::Text
      addDifference(n1, n2.parent, n1, n2, opts, differences)
    else
      addDifference(n1, n2, n1, n2, opts, differences)
    end
  end
  status
end
compareSortedAttributeSets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT) click to toggle source

Compares two sets of Nokogiri::XML::Node attributes by sorting them first. When the attributes are sorted, only attributes of the same type are compared to each other, and missing attributes can be easily detected.

@param [Nokogiri::XML::Element] n1 left node element
@param [Nokogiri::XML::Element] n2 right node element
@param [Array] a1_set left attribute set
@param [Array] a2_set right attribute set
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 312
def compareSortedAttributeSets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT)
  a1_set = a1_set.sort_by(&:name)
  a2_set = a2_set.sort_by(&:name)
  i = j = 0

  while i < a1_set.length || j < a2_set.length

    if a1_set[i].nil?
      result = compareAttributes(n1, n2, nil, a2_set[j], opts, differences); j += 1
    elsif a2_set[j].nil?
      result = compareAttributes(n1, n2, a1_set[i], nil, opts, differences); i += 1
    elsif a1_set[i].name < a2_set[j].name
      result = compareAttributes(n1, n2, a1_set[i], nil, opts, differences); i += 1
    elsif a1_set[i].name > a2_set[j].name
      result = compareAttributes(n1, n2, nil, a2_set[j], opts, differences); j += 1
    else
      result = compareAttributes(n1, n2, a1_set[i], a2_set[j], opts, differences); i += 1; j += 1
    end

    status = result unless result == EQUIVALENT
    break unless status == EQUIVALENT || opts[:verbose]
  end
  status
end
compareTextNodes(n1, n2, opts, differences, status = EQUIVALENT) click to toggle source

Compares two nodes of type Nokogiri::XML::Text.

@param [Nokogiri::XML::Text] n1 left text node
@param [Nokogiri::XML::Text] n2 right text node
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 261
def compareTextNodes(n1, n2, opts, differences, status = EQUIVALENT)
  return true if opts[:ignore_text_nodes]
  t1 = n1.content
  t2 = n2.content
  if opts[:collapse_whitespace]
    t1 = collapse(t1)
    t2 = collapse(t2)
  end
  unless t1 == t2
    status = UNEQUAL_TEXT_CONTENTS
    addDifference(n1.parent, n2.parent, t1, t2, opts, differences)
  end
  status
end
compareUnsortedAttributeSets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT) click to toggle source

Compares two sets of Nokogiri::XML::Element attributes without sorting them. As a result attributes of different types may be compared, and even if all attributes are identical in both sets, if their order is different, the comparison will stop as soon two unequal attributes are found.

@param [Nokogiri::XML::Element] n1 left node element
@param [Nokogiri::XML::Element] n2 right node element
@param [Array] a1_set left attribute set
@param [Array] a2_set right attribute set
@param [Hash] opts user-overridden options
@param [Array] differences inequivalence messages
@param [int] status comparison status code (EQUIVALENT by default)

@return type of equivalence (from equivalence constants)
# File lib/compare-xml.rb, line 353
def compareUnsortedAttributeSets(n1, n2, a1_set, a2_set, opts, differences, status = EQUIVALENT)
  [a1_set.length, a2_set.length].max.times do |i|
    result = compareAttributes(n1, n2, a1_set[i], a2_set[i], opts, differences)
    status = result unless result == EQUIVALENT
    break unless status == EQUIVALENT
  end
  status
end
nodeExcluded?(n, opts) click to toggle source

Determines if a node should be excluded from the comparison. When a node is excluded, it is completely ignored, as if it did not exist.

Several types of nodes are considered ignored:

  • comments (only in ignore_comments mode)

  • text nodes (only in ignore_text_nodes mode OR when a text node is empty)

  • node matches a user-specified css rule from ignore_nodes

    @param [Nokogiri::XML::Node] n node being tested for exclusion @param [Hash] opts user-overridden options

    @return true if excluded, false otherwise

# File lib/compare-xml.rb, line 411
def nodeExcluded?(n, opts)
  return true if n.is_a?(Nokogiri::XML::DTD)
  return true if n.is_a?(Nokogiri::XML::Comment) && opts[:ignore_comments]
  return true if n.is_a?(Nokogiri::XML::Text) && (opts[:ignore_text_nodes] || collapse(n.content).empty?)
  opts[:ignore_nodes].each { |css| return true if n.parent.css(css).include? n }
  false
end