xmatch 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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