xmatch 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Peter Moran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,53 @@
1
+ XMatch
2
+ ==============
3
+
4
+ XMatch is a Ruby library for comparing two XML documents and reporting on mismatches. An XML document will match another if:
5
+
6
+ * elements have the same name
7
+ * elements have the same number of children
8
+ * elements have the same number of attributes
9
+ * attributes have the same value
10
+ * text elements have the same content
11
+
12
+ Blank elements are ignored.
13
+ XMatch uses Nokogiri for xml parsing.
14
+
15
+ Matching XML
16
+ ------------
17
+ Given two XML documents as strings, XMatch is run by:
18
+
19
+ xml = Matcher::Xml.new(lhs)
20
+ xml.match(rhs)
21
+
22
+ A matcher provides access to the match information by xpath values:
23
+
24
+ xml.matches
25
+ xml.mismatches
26
+
27
+ Custom matchers
28
+ ---------------
29
+ The actual value of some xml elements are hard to know in advance, timestamps and ids being typical examples. XMatch allows custom matches to be applied
30
+ to provide a good guess at a match in advance of the match being run. Custom matchers are Ruby procs.
31
+
32
+ custom_matchers = "/bookstore/@id" => lambda {|elem| elem.value == '2'}
33
+ xml = Matcher::Xml.new("<bookstore id='1'></bookstore>", custom_matchers)
34
+ xml.match("<bookstore id='2'></bookstore>") # ==> true
35
+
36
+ Formatting match results
37
+ ------------------------
38
+
39
+ The HTML formatter provides a cucumber-inspired report of the match results:
40
+
41
+ Matcher::HtmlFormatter.new(xml).format
42
+
43
+ Installation
44
+ ------------
45
+
46
+ XMatch is packaged as a Gem. Install with:
47
+
48
+ gem install xmatch
49
+
50
+ Copyright
51
+ ---------
52
+
53
+ Copyright (c) 2009 Peter Moran. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+ require "rubygems"
3
+ require "rake"
4
+ require "rake/clean"
5
+
6
+ require 'xmatch'
7
+
8
+ require "spec/rake/spectask"
9
+
10
+ Spec::Rake::SpecTask.new(:spec) do |spec|
11
+ spec.libs << 'lib' << 'spec'
12
+ spec.spec_files = FileList['spec/**/*_spec.rb']
13
+ # spec.spec_opts << "-Du"
14
+ spec.spec_opts << "--color"
15
+ end
16
+
17
+ task :default => :spec
18
+
19
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
20
+ spec.libs << 'lib' << 'spec'
21
+ spec.spec_files = FileList['spec/**/*_spec.rb']
22
+ spec.rcov = true
23
+ end
24
+
25
+ desc "Match two xml documents"
26
+ task :match, :lhs, :rhs do |t, args|
27
+ puts "** Matching #{args[:lhs]} with #{args[:rhs]}"
28
+ xml = Matcher::Xml.new(File.read(args[:lhs]))
29
+ xml.match(File.read(args[:rhs]))
30
+ Matcher::HtmlFormatter.new(xml).format
31
+ end
@@ -0,0 +1,47 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+
4
+ module Matcher
5
+
6
+ class HtmlFormatter
7
+
8
+ TEMPLATE = File.dirname(__FILE__) + '/xmatch.html.erb'
9
+
10
+ def initialize(matcher, args = {})
11
+ @matcher = matcher
12
+ @report_dir = args[:report_dir] || File.dirname(__FILE__) + '/../../reports'
13
+ end
14
+
15
+ def format
16
+ FileUtils.mkdir_p(@report_dir)
17
+ File.open(File.join(@report_dir, "xmatch.html"), 'w') { |f| f.write(generate_html) }
18
+ end
19
+
20
+ private
21
+
22
+ def generate_html
23
+ actual_filename = create_actual_file
24
+ expected_filename = create_expected_file
25
+ xml = @matcher
26
+ html = ERB.new(File.read(TEMPLATE))
27
+ html.result(binding)
28
+ end
29
+
30
+ def create_expected_file
31
+ write_xml("expected.xml", @matcher.lhs)
32
+ end
33
+
34
+ def create_actual_file
35
+ write_xml("actual.xml", @matcher.rhs)
36
+ end
37
+
38
+ def write_xml(name, xml)
39
+ File.open(File.join(@report_dir, name), 'w') { |f| f.write(xml)}
40
+ name
41
+ end
42
+
43
+
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,76 @@
1
+ require 'nokogiri'
2
+
3
+ module Nokogiri
4
+
5
+ module XML
6
+
7
+ class Element
8
+
9
+ def match?(other, matcher)
10
+ @matcher = matcher
11
+ children_match?(other) &&
12
+ name_matches?(other) &&
13
+ attributes_match?(other)
14
+ end
15
+
16
+ private
17
+
18
+ def children_match?(other)
19
+ match = children.size == other.children.size
20
+ @matcher.record(path, match, "expected #{children.size} children, got #{other.children.size}")
21
+ match
22
+ end
23
+
24
+ def name_matches?(other)
25
+ match = name == other.name
26
+ @matcher.record(path, match, "expected element '#{name}', got '#{other.name}'")
27
+ match
28
+ end
29
+
30
+ def attributes_match?(other)
31
+ match = attributes.size == other.attributes.size
32
+ if match
33
+ attributes.each_pair do |name, lhs|
34
+ match = match && lhs.match?(other.attributes[name], @matcher)
35
+ end
36
+ else
37
+ @matcher.record(path, match, "expected #{attributes.size} attributes, got #{other.attributes.size}")
38
+ end
39
+ match
40
+ end
41
+
42
+ end
43
+
44
+ class Document
45
+ def match?(other, matcher = nil)
46
+ true
47
+ end
48
+ end
49
+
50
+ class Text
51
+ def match?(other, matcher)
52
+ custom_matcher = matcher.custom_matchers[path]
53
+ match = custom_matcher ? custom_matcher.call(other) : (content == other.content)
54
+ matcher.record(path, match, "expected '#{content}', got '#{other.content}'")
55
+ match
56
+ end
57
+ end
58
+
59
+ class Attr
60
+ def match?(other, matcher)
61
+ if other.nil?
62
+ # record parent's path: nokogiri's traverse won't find attrs as children, so formatter won't report on them
63
+ matcher.record(parent.path, false, "expected attribute missing")
64
+ return false
65
+ end
66
+
67
+ custom_matcher = matcher.custom_matchers[path]
68
+ match = custom_matcher ? custom_matcher.call(other) : (value == other.value)
69
+ matcher.record(parent.path, match, "attribute '#{name}' expected '#{value}', got '#{other.value}'")
70
+ match
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,26 @@
1
+ module Matcher
2
+
3
+ class TextFormatter
4
+
5
+ def format(matcher)
6
+ if matcher.mismatches.empty?
7
+ puts "Documents matched"
8
+ else
9
+ puts "Documents didn't match:"
10
+ puts matcher.mismatches.to_a.join(' : ')
11
+ puts
12
+ matcher.lhs.traverse do |e|
13
+ print e.path
14
+ mismatch = matcher.mismatches[e.path]
15
+ if mismatch
16
+ puts " <====== #{mismatch}"
17
+ else
18
+ puts
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,3 @@
1
+ module Representative
2
+ VERSION = "0.1.1".freeze
3
+ end
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+
4
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
+ <head>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
7
+
8
+ <title>XMatch Report</title>
9
+
10
+ <style type="text/css">
11
+ body { background-color: #fff; color: #333; }
12
+
13
+ body, p, ol, ul, td {
14
+ font-family: verdana, arial, helvetica, sans-serif;
15
+ font-size: 12px;
16
+ line-height: 18px;
17
+ }
18
+
19
+ .matched { background-color: #DBFFB4 }
20
+ .mismatched { background-color: #FA5858 }
21
+ .unmatched { background-color: #FFFBD3 }
22
+ </style>
23
+
24
+ </head>
25
+
26
+ <body>
27
+
28
+ <h2>XMatch Report</h2>
29
+ <p><%= xml.mismatches.size %> mismatches found</p>
30
+ <p><a href='<%=expected_filename%>'>Expected xml</a> | <a href='<%=actual_filename%>'>Actual xml</a></p>
31
+
32
+ <table border="0" cellspacing="2" cellpadding="5">
33
+ <tr>
34
+ <th>Line</th>
35
+ <th>Path</th>
36
+ <th>Mismatch</th>
37
+ </tr>
38
+ <% xml.lhs.traverse do | elem | %>
39
+ <% next if elem.xml? %>
40
+ <tr class=<%= xml.result_for(elem.path) %> >
41
+ <td><%= elem.line %></td>
42
+ <td><%= elem.path %></td>
43
+ <td><%= xml.mismatches[elem.path] || '&nbsp;' %></td>
44
+ </tr>
45
+ <% end %>
46
+ </table>
47
+
48
+ </body>
49
+ </html>
@@ -0,0 +1,62 @@
1
+ require 'matcher/nokogiri_extensions'
2
+ require 'ostruct'
3
+
4
+ module Matcher
5
+
6
+ class Xml
7
+
8
+ attr_reader :lhs, :rhs, :custom_matchers
9
+
10
+ def initialize(lhs, custom_matchers = {})
11
+ @lhs = parse(lhs)
12
+ @custom_matchers = custom_matchers
13
+ @results = {}
14
+ end
15
+
16
+ def match(actual)
17
+ @results.clear
18
+ @rhs = parse(actual)
19
+ compare(@lhs, @rhs)
20
+ end
21
+
22
+ def record(path, result, message = nil)
23
+ @results[path] = OpenStruct.new(:result => result, :message => message)
24
+ end
25
+
26
+ def result_for(path)
27
+ return "matched" if matches[path]
28
+ return "mismatched" if mismatches[path]
29
+ "unmatched"
30
+ end
31
+
32
+ def matches
33
+ match_info = {}
34
+ @results.each_pair { |k, v| match_info[k] = '' if v.result }
35
+ match_info
36
+ end
37
+
38
+ def mismatches
39
+ match_info = {}
40
+ @results.each_pair { |k, v| match_info[k] = v.message unless v.result }
41
+ match_info
42
+ end
43
+
44
+ private
45
+
46
+ def parse(xml)
47
+ xml_as_string = xml.instance_of?(Nokogiri::XML::Document) ? xml.to_xml : xml
48
+ Nokogiri::XML(xml_as_string) { |config| config.noblanks }
49
+ end
50
+
51
+ def compare(lhs, rhs)
52
+ return false unless lhs && rhs
53
+ match = lhs.match?(rhs, self)
54
+ lhs.children.each_with_index do |child, i|
55
+ match = match & compare(child, rhs.children[i])
56
+ end
57
+ match
58
+ end
59
+
60
+ end
61
+
62
+ end
data/lib/xmatch.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'matcher/nokogiri_extensions'
2
+ require 'matcher/xml'
3
+ require 'matcher/text_formatter'
4
+ require 'matcher/html_formatter'
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XMLxxx</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <authorxxx>James McGovern</authorxxx>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XMLx</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEBx">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book categoryx="WEB">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ <book category="foo">
38
+ <title lang="en">Learning XML</title>
39
+ <author>Erik T. Ray</author>
40
+ <year>2003</year>
41
+ <price>39.95</price>
42
+ </book>
43
+
44
+ </bookstore>
@@ -0,0 +1,40 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+
38
+
39
+
40
+ </bookstore>
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+
3
+ <bookstore>
4
+
5
+ <book category="COOKING">
6
+ <title lang="en">Everyday Italian</title>
7
+ <author>Giada De Laurentiis</author>
8
+ <year>2005</year>
9
+ <price>30.00</price>
10
+ </book>
11
+
12
+ <book category="CHILDREN">
13
+ <title lang="en">Harry Potter</title>
14
+ <author>J K. Rowling</author>
15
+ <year>2005</year>
16
+ <price>29.99</price>
17
+ </book>
18
+
19
+ <book category="WEB">
20
+ <title lang="en">XQuery Kick Start</title>
21
+ <author>James McGovern</author>
22
+ <author>Per Bothner</author>
23
+ <author>Kurt Cagle</author>
24
+ <author>James Linn</author>
25
+ <author>Vaidyanathan Nagarajan</author>
26
+ <year>2003</year>
27
+ <price>49.99</price>
28
+ </book>
29
+
30
+ <book category="WEB" foo="bar">
31
+ <title lang="en">Learning XML</title>
32
+ <author>Erik T. Ray</author>
33
+ <year>2003</year>
34
+ <price>39.95</price>
35
+ </book>
36
+
37
+ </bookstore>
@@ -0,0 +1,25 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'matcher/xml'
3
+
4
+ describe Matcher::Xml do
5
+
6
+ it "should compare files" do
7
+ 1.upto(10) do |i|
8
+ instance_variable_set("@xml#{i}", open(File.expand_path(File.join(__FILE__, "../../fixtures/books#{i}.xml"))).read)
9
+ end
10
+
11
+ xml = Matcher::Xml.new(@xml1)
12
+ xml.match(@xml2).should be_true
13
+ xml.match(@xml3).should be_false
14
+ xml.match(@xml4).should be_false
15
+ xml.match(@xml5).should be_false
16
+ xml.match(@xml6).should be_false
17
+ xml.match(@xml7).should be_false
18
+ xml.match(@xml9).should be_false
19
+ xml.match(@xml10).should be_false
20
+ xml.match(@xml8).should be_true
21
+
22
+ Matcher::Xml.new(@xml7).match(@xml1).should be_false
23
+ end
24
+
25
+ end
@@ -0,0 +1,328 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ require 'nokogiri'
4
+
5
+ describe Matcher::Xml do
6
+
7
+ def verify_mismatch(path, message)
8
+ @xml.match(@rhs).should be_false
9
+ @xml.mismatches.should have(1).mismatch
10
+ @xml.mismatches[path].should == message
11
+ end
12
+
13
+ context "attributes" do
14
+
15
+ before(:each) do
16
+ @xml = Matcher::Xml.new("<foo></foo>")
17
+ end
18
+
19
+ it "should initialise with a string" do
20
+ @xml.lhs.should be_a_kind_of(Nokogiri::XML::Document)
21
+ end
22
+
23
+ it "should initialise with a document" do
24
+ @xml.lhs.should be_a_kind_of(Nokogiri::XML::Document)
25
+ end
26
+
27
+ it "should provide the rhs after a match" do
28
+ @xml.match("<bar></bar>")
29
+ @xml.rhs.should be_a_kind_of(Nokogiri::XML::Document)
30
+ end
31
+
32
+ end
33
+
34
+ before(:each) do
35
+ @lhs = <<-eos
36
+ <bookstore>
37
+ <book category="COOKING">
38
+ <title lang="en">Everyday Italian</title>
39
+ </book>
40
+ </bookstore>
41
+ eos
42
+ @xml = Matcher::Xml.new(@lhs)
43
+ end
44
+
45
+ context "matching" do
46
+
47
+ it "should be true when documents match" do
48
+ Matcher::Xml.new(@lhs).match(@lhs.clone).should be_true
49
+ end
50
+
51
+ it "should provide empty mismatches on match" do
52
+ @xml.match(@lhs.clone).should be_true
53
+ @xml.mismatches.should be_empty
54
+ end
55
+
56
+ it "should ignore blank elements" do
57
+ rhs = <<-eos
58
+ <bookstore>
59
+ <book category="COOKING">
60
+ <title lang="en">Everyday Italian</title>
61
+ </book>
62
+
63
+
64
+ </bookstore>
65
+ eos
66
+ @xml.match(rhs).should be_true
67
+ end
68
+
69
+ it "should be true when a string is matched with a document" do
70
+ rhs = <<-eos
71
+ <bookstore>
72
+ <book category="COOKING">
73
+ <title lang="en">Everyday Italian</title>
74
+ </book>
75
+
76
+
77
+ </bookstore>
78
+ eos
79
+
80
+ Matcher::Xml.new(@lhs).match(Nokogiri::XML(rhs)).should be_true
81
+ end
82
+
83
+ context "elements" do
84
+
85
+ it "should not match when rhs has an extra element" do
86
+ @rhs = <<-eos
87
+ <bookstore>
88
+ <book category="COOKING">
89
+ <title lang="en">Everyday Italian</title>
90
+ </book>
91
+ <book></book>
92
+ </bookstore>
93
+ eos
94
+ verify_mismatch("/bookstore", "expected 1 children, got 2")
95
+ end
96
+
97
+ it "should not match when rhs has a missing element" do
98
+ @lhs = <<-eos
99
+ <bookstore>
100
+ <book category="COOKING">
101
+ <title lang="en">Everyday Italian</title>
102
+ </book>
103
+ </bookstore>
104
+ eos
105
+
106
+ @rhs = <<-eos
107
+ <bookstore>
108
+ <book category="COOKING">
109
+ </book>
110
+ </bookstore>
111
+ eos
112
+ verify_mismatch("/bookstore/book/title", "expected 1 children, got 0")
113
+ end
114
+
115
+ end
116
+
117
+ context "names" do
118
+
119
+ it "should not match when rhs has a different element name" do
120
+ @rhs = <<-eos
121
+ <bookstore>
122
+ <bookx category="COOKING">
123
+ <title lang="en">Everyday Italian</title>
124
+ </bookx>
125
+ </bookstore>
126
+ eos
127
+ verify_mismatch("/bookstore/book", "expected element 'book', got 'bookx'")
128
+ end
129
+
130
+ end
131
+
132
+ context "attributes" do
133
+
134
+ it "should not match when an attribute names don't match" do
135
+ @rhs = <<-eos
136
+ <bookstore>
137
+ <book categoryx="COOKING">
138
+ <title lang="en">Everyday Italian</title>
139
+ </book>
140
+ </bookstore>
141
+ eos
142
+ verify_mismatch("/bookstore/book", "expected attribute missing")
143
+ end
144
+
145
+ it "should not match when an attribute value doesn't match" do
146
+ @rhs = <<-eos
147
+ <bookstore>
148
+ <book category="COOKINGx">
149
+ <title lang="en">Everyday Italian</title>
150
+ </book>
151
+ </bookstore>
152
+ eos
153
+ verify_mismatch("/bookstore/book", "attribute 'category' expected 'COOKING', got 'COOKINGx'")
154
+ end
155
+
156
+ it "should not match when rhs has an extra attribute" do
157
+ @rhs = <<-eos
158
+ <bookstore>
159
+ <book category="COOKING" foo="bar">
160
+ <title lang="en">Everyday Italian</title>
161
+ </book>
162
+ </bookstore>
163
+ eos
164
+ verify_mismatch("/bookstore/book", "expected 1 attributes, got 2")
165
+ end
166
+
167
+ it "should not match when rhs has less attributes" do
168
+ @rhs = <<-eos
169
+ <bookstore>
170
+ <book category="COOKING">
171
+ <title>Everyday Italian</title>
172
+ </book>
173
+ </bookstore>
174
+ eos
175
+ verify_mismatch("/bookstore/book/title", "expected 1 attributes, got 0")
176
+ end
177
+
178
+ it "should not match when rhs has more attributes" do
179
+ @rhs = <<-eos
180
+ <bookstore>
181
+ <book category="COOKING">
182
+ <title lang="en" foo="bar">Everyday Italian</title>
183
+ </book>
184
+ </bookstore>
185
+ eos
186
+ verify_mismatch("/bookstore/book/title", "expected 1 attributes, got 2")
187
+ end
188
+
189
+ end
190
+
191
+ context "contents" do
192
+
193
+ it "should not match when inner text contents don't match" do
194
+ @rhs = <<-eos
195
+ <bookstore>
196
+ <book category="COOKING">
197
+ <title lang="en">Everyday Italianx</title>
198
+ </book>
199
+ </bookstore>
200
+ eos
201
+ verify_mismatch("/bookstore/book/title/text()", "expected 'Everyday Italian', got 'Everyday Italianx'")
202
+ end
203
+
204
+ end
205
+
206
+ end
207
+
208
+ context "mismatches" do
209
+
210
+ it "should be empty to start with" do
211
+ Matcher::Xml.new(@lhs).mismatches.should be_empty
212
+ end
213
+
214
+ it "should be reset when rematching" do
215
+ rhs = <<-eos
216
+ <bookstore>
217
+ <book category="COOKING">
218
+ </book>
219
+ </bookstore>
220
+ eos
221
+ @xml.match(rhs)
222
+ @xml.mismatches.should_not be_empty
223
+ @xml.match(@lhs)
224
+ @xml.mismatches.should be_empty
225
+ end
226
+
227
+ it 'can have multiple mismatches' do
228
+ rhs = <<-eos
229
+ <bookstorex>
230
+ <book>
231
+ <title lang="en">Everyday Italian</title>
232
+ </book>
233
+ </bookstore>
234
+ eos
235
+ @xml.match(rhs)
236
+ @xml.mismatches.should have(2).mismatches
237
+ end
238
+
239
+ it "should contain parent's path when an attribute doesn't match" do
240
+
241
+ lhs = <<-eos
242
+ <bookstore>
243
+ <book category="COOKING">
244
+ <title lang="en">Everyday Italian</title>
245
+ </book>
246
+ <book category="FOO">
247
+ <title lang="en">Everyday French</title>
248
+ </book>
249
+ </bookstore>
250
+ eos
251
+
252
+
253
+ @rhs = <<-eos
254
+ <bookstore>
255
+ <book category="COOKING">
256
+ <title lang="en">Everyday Italian</title>
257
+ </book>
258
+ <book foo="bar">
259
+ <title lang="en">Everyday French</title>
260
+ </book>
261
+ </bookstore>
262
+ eos
263
+
264
+ @xml = Matcher::Xml.new(lhs)
265
+ verify_mismatch("/bookstore/book[2]", "expected attribute missing")
266
+ end
267
+
268
+ context 'matches' do
269
+
270
+ it "should be provided with no message" do
271
+ lhs = "<bookstore><book></book></bookstore>"
272
+ xml = Matcher::Xml.new(lhs)
273
+ xml.match(lhs)
274
+ xml.matches.should have(2).matches
275
+ xml.matches.should include("/bookstore")
276
+ xml.matches.values.all? {|m| m == ''}.should be_true
277
+ end
278
+
279
+ end
280
+
281
+ end
282
+
283
+ context "match results" do
284
+
285
+ it "provides all results"
286
+
287
+ it "returns 'matched' for a path that matched correctly" do
288
+ xml = Matcher::Xml.new("<bookstore></bookstore>")
289
+ xml.match("<bookstore></bookstore>")
290
+ xml.result_for("/bookstore").should == "matched"
291
+ end
292
+
293
+ it "returns 'mismatched' for a path that was mismatched" do
294
+ xml = Matcher::Xml.new("<bookstore></bookstore>")
295
+ xml.match("<bookstorex></bookstorex>")
296
+ xml.result_for("/bookstore").should == "mismatched"
297
+ end
298
+
299
+ it "returns 'unmatched' for a path that was not matched at all" do
300
+ xml = Matcher::Xml.new("<bookstore><foo></foo></bookstore>")
301
+ xml.match("<bookstorex></bookstorex>")
302
+ xml.result_for("/bookstore/foo").should == "unmatched"
303
+ end
304
+
305
+ end
306
+
307
+ context "custom matchers" do
308
+
309
+ it "can be provided" do
310
+ xml = Matcher::Xml.new("<bookstore id='1'></bookstore>", {"my path" => "my predicate"})
311
+ xml.custom_matchers.should have(1).matcher
312
+ end
313
+
314
+ it "can be used on an attribute value" do
315
+ custom_matchers = { "/bookstore/@id" => lambda {|elem| elem.value == '2'} }
316
+ xml = Matcher::Xml.new("<bookstore id='1'></bookstore>", custom_matchers)
317
+ xml.match("<bookstore id='2'></bookstore>").should be_true
318
+ end
319
+
320
+ it "can be used on an element value" do
321
+ custom_matchers = { "/bookstore/book/text()" => lambda {|elem| elem.content == 'bar'} }
322
+ xml = Matcher::Xml.new("<bookstore><book>foo</book></bookstore", custom_matchers)
323
+ xml.match("<bookstore><book>bar</book></bookstore").should be_true
324
+ end
325
+
326
+ end
327
+
328
+ end
@@ -0,0 +1,8 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "nokogiri"
4
+ require 'xmatch'
5
+
6
+ Spec::Runner.configure do |config|
7
+
8
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xmatch
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - Peter Moran
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-18 00:00:00 +11:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: nokogiri
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 1
32
+ - 4
33
+ - 2
34
+ version: 1.4.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: bundler
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 15
46
+ segments:
47
+ - 1
48
+ - 0
49
+ version: "1.0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 27
61
+ segments:
62
+ - 1
63
+ - 3
64
+ - 0
65
+ version: 1.3.0
66
+ type: :development
67
+ version_requirements: *id003
68
+ description:
69
+ email: workingpeter@gmail.com
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files: []
75
+
76
+ files:
77
+ - lib/matcher/html_formatter.rb
78
+ - lib/matcher/nokogiri_extensions.rb
79
+ - lib/matcher/text_formatter.rb
80
+ - lib/matcher/version.rb
81
+ - lib/matcher/xmatch.html.erb
82
+ - lib/matcher/xml.rb
83
+ - lib/xmatch.rb
84
+ - README.markdown
85
+ - LICENSE
86
+ - spec/fixtures/books1.xml
87
+ - spec/fixtures/books10.xml
88
+ - spec/fixtures/books2.xml
89
+ - spec/fixtures/books3.xml
90
+ - spec/fixtures/books4.xml
91
+ - spec/fixtures/books5.xml
92
+ - spec/fixtures/books6.xml
93
+ - spec/fixtures/books7.xml
94
+ - spec/fixtures/books8.xml
95
+ - spec/fixtures/books9.xml
96
+ - spec/matcher/smoke_spec.rb
97
+ - spec/matcher/xml_spec.rb
98
+ - spec/spec_helper.rb
99
+ - Rakefile
100
+ has_rdoc: true
101
+ homepage: http://github.com/pmoran/xmatch
102
+ licenses: []
103
+
104
+ post_install_message:
105
+ rdoc_options: []
106
+
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ hash: 3
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ hash: 3
124
+ segments:
125
+ - 0
126
+ version: "0"
127
+ requirements: []
128
+
129
+ rubyforge_project:
130
+ rubygems_version: 1.3.7
131
+ signing_key:
132
+ specification_version: 3
133
+ summary: A Ruby library for comparing XML documents and reporting on mismatches
134
+ test_files:
135
+ - spec/fixtures/books1.xml
136
+ - spec/fixtures/books10.xml
137
+ - spec/fixtures/books2.xml
138
+ - spec/fixtures/books3.xml
139
+ - spec/fixtures/books4.xml
140
+ - spec/fixtures/books5.xml
141
+ - spec/fixtures/books6.xml
142
+ - spec/fixtures/books7.xml
143
+ - spec/fixtures/books8.xml
144
+ - spec/fixtures/books9.xml
145
+ - spec/matcher/smoke_spec.rb
146
+ - spec/matcher/xml_spec.rb
147
+ - spec/spec_helper.rb
148
+ - Rakefile