json-deep-compare 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,114 +1,201 @@
1
1
  module JsonDeepCompare
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
+
4
+ class DocumentComparison
5
+ def initialize(lval, rval, options = {})
6
+ if exclusions = options[:exclusions]
7
+ options[:exclusions] = [exclusions] unless exclusions.is_a?(Array)
8
+ else
9
+ options[:exclusions] = []
10
+ end
11
+ @root_comparisons = []
12
+ @root_comparisons << NodeComparison.new(lval, rval, ":root", options)
13
+ @root_comparisons << NodeComparison.new(rval, lval, ":root", options)
14
+ end
15
+
16
+ def difference_messages
17
+ ldiffs = @root_comparisons.first.differences
18
+ rdiffs = @root_comparisons.last.differences
19
+ differences = ldiffs.dup
20
+ ldiff_selectors = ldiffs.map { |ldiff| ldiff.selector }
21
+ rdiffs.each do |rdiff|
22
+ unless ldiff_selectors.include?(rdiff.selector)
23
+ differences << rdiff.reverse
24
+ end
25
+ end
26
+ differences = differences.sort_by &:selector
27
+ differences.map(&:message).join("\n")
28
+ end
29
+
30
+ def equal?
31
+ @root_comparisons.all?(&:equal?)
32
+ end
33
+ end
3
34
 
4
35
  class NodeComparison
5
36
  ExcerptPadding = 15
6
- attr_reader :left_value, :right_value, :selector
37
+ attr_reader :lval, :rval, :selector
7
38
 
8
- def initialize(left_value, right_value, options = {})
9
- @left_value, @right_value = left_value, right_value
10
- @selector = options[:selector] || ':root'
11
- @exclusions = options[:exclusions]
12
- @exclusions = [@exclusions] unless @exclusions.is_a?(Array)
39
+ def initialize(lval, rval, selector, options = {})
40
+ @lval, @rval, @selector, @options = lval, rval, selector, options
13
41
  @children = []
14
- if left_value.is_a?(Hash)
15
- if right_value.is_a?(Hash)
16
- left_value.each do |key, left_sub_value|
42
+ if lval.is_a?(Hash)
43
+ if rval.is_a?(Hash)
44
+ lval.each do |key, left_sub_value|
17
45
  @children << NodeComparison.new(
18
- left_sub_value, right_value[key],
19
- selector: "#{selector} > .#{key}", exclusions: @exclusions
46
+ left_sub_value,
47
+ rval[key],
48
+ "#{selector} > .#{key}",
49
+ options
20
50
  )
21
51
  end
22
52
  end
23
- elsif left_value.is_a?(Array)
24
- if right_value.is_a?(Array)
25
- left_value.each_with_index do |left_sub_value, i|
53
+ elsif lval.is_a?(Array)
54
+ if rval.is_a?(Array)
55
+ lval.each_with_index do |left_sub_value, i|
26
56
  @children << NodeComparison.new(
27
- left_sub_value, right_value[i],
28
- selector: "#{selector} :nth-child(#{i+1})",
29
- exclusions: @exclusions
57
+ left_sub_value,
58
+ rval[i],
59
+ "#{selector} :nth-child(#{i+1})",
60
+ options
30
61
  )
31
62
  end
32
63
  end
33
64
  end
34
65
  end
35
66
 
36
- def value_inspect(value)
37
- str = value.inspect
38
- if str.length >= 40
39
- "#{value.class.name} #{str[0..37]}..."
40
- else
41
- str
42
- end
67
+ def blank?(value)
68
+ value.respond_to?(:empty?) ? value.empty? : !value
43
69
  end
44
70
 
45
- def difference_message
46
- unless equal?
71
+ def blank_equality?
72
+ @options[:blank_equality]
73
+ end
74
+
75
+ def differences
76
+ if equal?
77
+ []
78
+ else
47
79
  if leaf?
48
80
  if excerptable_difference?
49
- excerpted_difference
81
+ [excerpted_difference]
50
82
  else
51
- "#{@selector.inspect} expected to be #{value_inspect(@left_value)} but was #{value_inspect(@right_value)}"
83
+ [Difference.new(
84
+ @selector, "expected to be :lval but was :rval",
85
+ lval: value_inspect(@lval), rval: value_inspect(@rval)
86
+ )]
52
87
  end
53
88
  else
54
- @children.reject(&:equal?).map(&:difference_message).join("\n")
89
+ @children.map(&:differences).compact.flatten
55
90
  end
56
91
  end
57
92
  end
58
93
 
59
94
  def equal?
60
95
  if leaf?
61
- @exclusions.include?(@selector) || @left_value == @right_value
96
+ if selector_excluded?
97
+ true
98
+ elsif equality_proc
99
+ equality_proc.call(@lval, @rval)
100
+ else
101
+ @lval == @rval || (blank_equality? && blank?(@lval) && blank?(@rval))
102
+ end
62
103
  else
63
104
  @children.all?(&:equal?)
64
105
  end
65
106
  end
66
107
 
108
+ def equality_proc
109
+ @options[:equality]
110
+ end
111
+
67
112
  def excerptable_difference?
68
- @left_value.is_a?(String) and @right_value.is_a?(String) && (
69
- @left_value.size > ExcerptPadding * 2 ||
70
- @right_value.size > ExcerptPadding * 2
113
+ @lval.is_a?(String) and @rval.is_a?(String) && (
114
+ @lval.size > ExcerptPadding * 2 || @rval.size > ExcerptPadding * 2
71
115
  )
72
116
  end
73
117
 
74
118
  def excerpted_difference
75
- difference_start = (0..@left_value.length).detect { |i|
76
- @left_value[i] != @right_value[i]
77
- }
119
+ difference_start = (0..@lval.length).detect { |i| @lval[i] != @rval[i] }
78
120
  range_start = if difference_start > ExcerptPadding
79
121
  difference_start - ExcerptPadding
80
122
  else
81
123
  0
82
124
  end
83
- left_excerpt = @left_value[
84
- range_start..difference_start+ExcerptPadding
85
- ]
86
- right_excerpt = @right_value[
87
- range_start..difference_start+ExcerptPadding
88
- ]
125
+ left_excerpt = @lval[range_start..difference_start+ExcerptPadding]
126
+ right_excerpt = @rval[range_start..difference_start+ExcerptPadding]
89
127
  if difference_start - ExcerptPadding > 0
90
128
  left_excerpt = "..." + left_excerpt
91
129
  right_excerpt = "..." + right_excerpt
92
130
  end
93
- if difference_start + ExcerptPadding < @left_value.length
131
+ if difference_start + ExcerptPadding < @lval.length
94
132
  left_excerpt = left_excerpt + '...'
95
133
  end
96
- if difference_start + ExcerptPadding < @right_value.length
134
+ if difference_start + ExcerptPadding < @rval.length
97
135
  right_excerpt = right_excerpt + '...'
98
136
  end
99
- "#{@selector.inspect} differs starting at char #{difference_start}: #{left_excerpt.inspect} differs from #{right_excerpt.inspect}"
137
+ Difference.new(
138
+ @selector,
139
+ "differs starting at char :difference_start: :lval differs from :rval",
140
+ difference_start: difference_start.to_s,
141
+ lval: left_excerpt.inspect, rval: right_excerpt.inspect
142
+ )
100
143
  end
101
144
 
102
145
  def leaf?
103
146
  @children.empty?
104
147
  end
148
+
149
+ def selector_excluded?
150
+ @options[:exclusions].any? { |exclusion|
151
+ if exclusion.is_a?(String)
152
+ exclusion == @selector
153
+ else
154
+ @selector =~ exclusion
155
+ end
156
+ }
157
+ end
158
+
159
+ def value_inspect(value)
160
+ str = value.inspect
161
+ if str.length >= 40
162
+ "#{value.class.name} #{str[0..37]}..."
163
+ else
164
+ str
165
+ end
166
+ end
167
+
168
+ class Difference
169
+ attr_reader :selector
170
+
171
+ def initialize(selector, msg_template, variables)
172
+ @selector, @msg_template, @variables =
173
+ selector, msg_template, variables
174
+ end
175
+
176
+ def message
177
+ msg = @msg_template
178
+ @variables.each do |name, value|
179
+ msg = msg.gsub(/#{name.inspect}/, value)
180
+ end
181
+ "#{@selector.inspect} #{msg}"
182
+ end
183
+
184
+ def reverse
185
+ reversed_variables = {lval: @variables[:rval], rval: @variables[:lval]}
186
+ (@variables.keys - [:lval, :rval]).each do |other_var|
187
+ reversed_variables[other_var] = @variables[other_var]
188
+ end
189
+ Difference.new(@selector, @msg_template, reversed_variables)
190
+ end
191
+ end
105
192
  end
106
193
 
107
194
  module Assertions
108
195
  def assert_json_equal(expected, actual, exclusions = nil)
109
- comparison = NodeComparison.new(expected, actual, exclusions: exclusions)
196
+ comparison = DocumentComparison.new(expected, actual, exclusions: exclusions)
110
197
  unless comparison.equal?
111
- fail comparison.difference_message
198
+ fail comparison.difference_messages
112
199
  end
113
200
  end
114
201
  end
@@ -2,7 +2,41 @@ require 'test/unit'
2
2
  $: << '.'
3
3
  require 'lib/json-deep-compare'
4
4
 
5
- class NodeComparisonTestCase < Test::Unit::TestCase
5
+ class DocumentComparisonTestCase < Test::Unit::TestCase
6
+ def assert_symmetrically_different(
7
+ lval, rval, message_template = nil, sub_pairs = nil
8
+ )
9
+ sub_pairs = [sub_pairs] if sub_pairs and sub_pairs.first.is_a?(String)
10
+ comparison1 = JsonDeepCompare::DocumentComparison.new(lval, rval)
11
+ assert(
12
+ !comparison1.equal?,
13
+ "Comparison of #{lval.inspect} to #{rval.inspect} should not have been equal"
14
+ )
15
+ if message_template
16
+ message1 = message_template
17
+ sub_pairs.each_with_index do |sub_pair, i|
18
+ message1 = message1.
19
+ sub(/:left#{i}/, sub_pair.first).
20
+ sub(/:right#{i}/, sub_pair.last)
21
+ end
22
+ assert_equal(message1, comparison1.difference_messages)
23
+ end
24
+ comparison2 = JsonDeepCompare::DocumentComparison.new(rval, lval)
25
+ assert(
26
+ !comparison2.equal?,
27
+ "Comparison of #{rval.inspect} to #{lval.inspect} should not have been equal"
28
+ )
29
+ if message_template
30
+ message2 = message_template
31
+ sub_pairs.each_with_index do |sub_pair, i|
32
+ message2 = message2.
33
+ sub(/:left#{i}/, sub_pair.last).
34
+ sub(/:right#{i}/, sub_pair.first)
35
+ end
36
+ assert_equal(message2, comparison2.difference_messages)
37
+ end
38
+ end
39
+
6
40
  def test_detects_difference_in_atoms
7
41
  left_value = {
8
42
  'total_rows' => 2,
@@ -30,29 +64,36 @@ class NodeComparisonTestCase < Test::Unit::TestCase
30
64
  }
31
65
  ]
32
66
  }
33
- comparison = JsonDeepCompare::NodeComparison.new(left_value, right_value)
34
- assert !comparison.equal?
35
- assert_equal(
36
- "\":root > .rows :nth-child(1) > .doc > .sub_document > .one\" expected to be \"two\" but was \"1\"",
37
- comparison.difference_message
67
+ assert_symmetrically_different(
68
+ left_value, right_value,
69
+ "\":root > .rows :nth-child(1) > .doc > .sub_document > .one\" expected to be :left0 but was :right0",
70
+ ["two".inspect, "1".inspect]
38
71
  )
39
72
  end
40
73
 
41
74
  def test_detects_differences_in_keys
42
75
  left_value = {"doc" => {"foo" => "bar"}}
43
76
  right_value = {"doc" => {"biz" => "bang", "bing" => nil}}
44
- comparison = JsonDeepCompare::NodeComparison.new(left_value, right_value)
45
- assert !comparison.equal?
77
+ assert_symmetrically_different(left_value, right_value)
78
+ end
79
+
80
+ def test_missing_keys
81
+ lval = {"outer" => {"key1" => "value1"}}
82
+ rval = {"outer" => {"key1" => "value1", "key2" => {"foo" => "bar"}}}
83
+ assert_symmetrically_different(
84
+ lval, rval,
85
+ "\":root > .outer > .key2\" expected to be :left0 but was :right0",
86
+ ["nil", {"foo" => "bar"}.inspect]
87
+ )
46
88
  end
47
89
 
48
90
  def test_detects_difference_in_type
49
91
  lval = {"foo" => {'bar' => 'bang'}}
50
92
  rval = {"foo" => [3,4,5]}
51
- comparison = JsonDeepCompare::NodeComparison.new(lval, rval)
52
- assert !comparison.equal?
53
- assert_equal(
54
- "\":root > .foo\" expected to be {\"bar\"=>\"bang\"} but was [3, 4, 5]",
55
- comparison.difference_message
93
+ assert_symmetrically_different(
94
+ lval, rval,
95
+ "\":root > .foo\" expected to be :left0 but was :right0",
96
+ [{"bar" => "bang"}.inspect, [3,4,5].inspect]
56
97
  )
57
98
  end
58
99
 
@@ -63,64 +104,98 @@ class NodeComparisonTestCase < Test::Unit::TestCase
63
104
  end
64
105
  lval = {"foo" => lval_hash}
65
106
  rval = {"foo" => [3,4,5]}
66
- comparison = JsonDeepCompare::NodeComparison.new(lval, rval)
67
- assert !comparison.equal?
68
- assert_equal(
69
- "\":root > .foo\" expected to be Hash {\"A\"=>\"A\", \"B\"=>\"B\", \"C\"=>\"C\", \"D\"=>\"D... but was [3, 4, 5]",
70
- comparison.difference_message
107
+ assert_symmetrically_different(
108
+ lval, rval,
109
+ "\":root > .foo\" expected to be :left0 but was :right0",
110
+ [
111
+ "Hash {\"A\"=>\"A\", \"B\"=>\"B\", \"C\"=>\"C\", \"D\"=>\"D...",
112
+ [3,4,5].inspect
113
+ ]
71
114
  )
72
115
  end
73
116
 
74
117
  def test_lists_multliple_differences
75
118
  lval = {'one' => {'two' => 'three', 'four' => 'five'}, 'six' => 'seven'}
76
119
  rval = {'one' => {'two' => 'TWO', 'four' => 'five'}, 'six' => 'SIX'}
77
- comparison = JsonDeepCompare::NodeComparison.new(lval, rval)
78
- assert !comparison.equal?
79
- assert_equal(
80
- "\":root > .one > .two\" expected to be \"three\" but was \"TWO\"\n\":root > .six\" expected to be \"seven\" but was \"SIX\"",
81
- comparison.difference_message
120
+ assert_symmetrically_different(
121
+ lval, rval,
122
+ "\":root > .one > .two\" expected to be :left0 but was :right0\n\":root > .six\" expected to be :left1 but was :right1",
123
+ [["three".inspect, "TWO".inspect], ["seven".inspect, "SIX".inspect]]
82
124
  )
83
125
  end
84
126
 
85
127
  def test_detects_difference_between_array_and_nil
86
128
  lval = {'one' => [1,2,3]}
87
129
  rval = {'one' => nil}
88
- comparison = JsonDeepCompare::NodeComparison.new(lval, rval)
89
- assert !comparison.equal?
90
- assert_equal(
91
- "\":root > .one\" expected to be [1, 2, 3] but was nil",
92
- comparison.difference_message
130
+ assert_symmetrically_different(
131
+ lval, rval,
132
+ "\":root > .one\" expected to be :left0 but was :right0",
133
+ [[1,2,3].inspect, 'nil']
93
134
  )
94
135
  end
95
136
 
96
137
  def test_shows_first_string_difference
97
138
  lval = {'body' => "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."}
98
139
  rval = {'body' => "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in liberty, and dedicated to the proposition that all men are created equal."}
99
- comparison = JsonDeepCompare::NodeComparison.new(lval, rval)
100
- assert !comparison.equal?
101
- assert_equal(
102
- "\":root > .body\" differs starting at char 103: \"..., conceived in Liberty, and ded...\" differs from \"..., conceived in liberty, and ded...\"",
103
- comparison.difference_message
140
+ assert_symmetrically_different(
141
+ lval, rval,
142
+ "\":root > .body\" differs starting at char 103: :left0 differs from :right0",
143
+ [
144
+ "..., conceived in Liberty, and ded...".inspect,
145
+ "..., conceived in liberty, and ded...".inspect
146
+ ]
104
147
  )
105
148
  end
106
149
 
107
150
  def test_shows_string_difference_start
108
151
  lval = {'rev' => "22-23c92a95665bb692313229c8224b7088"}
109
152
  rval = {'rev' => "23-54a5106f8c522a57d6d4c6963bc36611"}
110
- comparison = JsonDeepCompare::NodeComparison.new(lval, rval)
111
- assert !comparison.equal?
112
- assert_equal(
113
- "\":root > .rev\" differs starting at char 1: \"22-23c92a95665bb6...\" differs from \"23-54a5106f8c522a...\"",
114
- comparison.difference_message
153
+ assert_symmetrically_different(
154
+ lval, rval,
155
+ "\":root > .rev\" differs starting at char 1: :left0 differs from :right0",
156
+ ["22-23c92a95665bb6...".inspect, "23-54a5106f8c522a...".inspect]
115
157
  )
116
158
  end
117
159
 
118
160
  def test_simple_exclusion
119
161
  lval = {'one' => 'two', 'three' => 'four'}
120
162
  rval = {'one' => 'two', 'three' => 'THREE'}
121
- comparison = JsonDeepCompare::NodeComparison.new(
163
+ comparison = JsonDeepCompare::DocumentComparison.new(
122
164
  lval, rval, exclusions: [":root > .three"]
123
165
  )
124
166
  assert comparison.equal?
125
167
  end
168
+
169
+ def test_exclusion_regexp
170
+ lval = {'one' => 'two', 'three' => 'four'}
171
+ rval = {'one' => 'two', 'three' => 'THREE'}
172
+ comparison = JsonDeepCompare::DocumentComparison.new(
173
+ lval, rval, exclusions: [/> \.three$/]
174
+ )
175
+ assert comparison.equal?
176
+ end
177
+
178
+ def test_blank_equality_option
179
+ lval = {'one' => 'two', 'three' => ''}
180
+ rval = {'one' => 'two', 'three' => nil}
181
+ comparison = JsonDeepCompare::DocumentComparison.new(
182
+ lval, rval, blank_equality: true
183
+ )
184
+ assert comparison.equal?
185
+ end
186
+
187
+ def test_equality_proc_option
188
+ lval = {'one' => 2, 'three' => "He says 'hi'"}
189
+ rval = {'one' => 2, 'three' => "He says \"hi\""}
190
+ comparison = JsonDeepCompare::DocumentComparison.new(
191
+ lval, rval, equality: Proc.new { |lval, rval|
192
+ if lval.is_a?(String) && rval.is_a?(String)
193
+ lval.gsub(/"/, "'") == rval.gsub(/"/, "'")
194
+ else
195
+ lval == rval
196
+ end
197
+ }
198
+ )
199
+ assert comparison.equal?
200
+ end
126
201
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json-deep-compare
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-03 00:00:00.000000000 Z
12
+ date: 2014-02-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -73,7 +73,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
73
73
  version: '0'
74
74
  segments:
75
75
  - 0
76
- hash: 383603838726367257
76
+ hash: 634763774305586060
77
77
  required_rubygems_version: !ruby/object:Gem::Requirement
78
78
  none: false
79
79
  requirements:
@@ -82,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  version: '0'
83
83
  segments:
84
84
  - 0
85
- hash: 383603838726367257
85
+ hash: 634763774305586060
86
86
  requirements: []
87
87
  rubyforge_project:
88
88
  rubygems_version: 1.8.23