specdiff 0.2.0 → 0.3.0.pre.rc1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +1 -1
- data/README.md +21 -4
- data/assets/webmock_json_with_specdiff.png +0 -0
- data/assets/webmock_text_with_specdiff.png +0 -0
- data/examples/rspec/.rspec +2 -0
- data/examples/rspec/Gemfile +10 -0
- data/examples/rspec/Gemfile.lock +52 -0
- data/examples/rspec/spec/example_spec.rb +678 -0
- data/examples/rspec/spec/spec_helper.rb +68 -0
- data/examples/webmock/Gemfile.lock +2 -2
- data/lib/specdiff/colorize.rb +5 -0
- data/lib/specdiff/diff.rb +3 -1
- data/lib/specdiff/differ/hashdiff.rb +101 -22
- data/lib/specdiff/differ/not_found.rb +1 -1
- data/lib/specdiff/differ/text.rb +21 -7
- data/lib/specdiff/hashprint.rb +154 -0
- data/lib/specdiff/inspect.rb +41 -0
- data/lib/specdiff/rspec.rb +33 -0
- data/lib/specdiff/version.rb +1 -1
- data/lib/specdiff/webmock.rb +41 -1
- data/lib/specdiff.rb +15 -1
- metadata +19 -10
- data/lib/specdiff/webmock/request_body_diff.rb +0 -41
@@ -0,0 +1,68 @@
|
|
1
|
+
Bundler.require
|
2
|
+
require "specdiff/rspec"
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
# rspec-expectations config goes here. You can use an alternate
|
6
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
7
|
+
# assertions if you prefer.
|
8
|
+
config.expect_with :rspec do |expectations|
|
9
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
10
|
+
# and `failure_message` of custom matchers include text for helper methods
|
11
|
+
# defined using `chain`, e.g.:
|
12
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
13
|
+
# # => "be bigger than 2 and smaller than 4"
|
14
|
+
# ...rather than:
|
15
|
+
# # => "be bigger than 2"
|
16
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
17
|
+
|
18
|
+
expectations.syntax = :expect
|
19
|
+
end
|
20
|
+
|
21
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
22
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
23
|
+
config.mock_with :rspec do |mocks|
|
24
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
25
|
+
# a real object. This is generally recommended, and will default to
|
26
|
+
# `true` in RSpec 4.
|
27
|
+
mocks.verify_partial_doubles = true
|
28
|
+
end
|
29
|
+
|
30
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
31
|
+
# have no way to turn it off -- the option exists only for backwards
|
32
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
33
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
34
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
35
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
36
|
+
|
37
|
+
# enable test focusing
|
38
|
+
config.filter_run_when_matching :focus
|
39
|
+
|
40
|
+
# Enable flags like --only-failures and --next-failure
|
41
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
42
|
+
|
43
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
44
|
+
# recommended. For more details, see:
|
45
|
+
# https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
|
46
|
+
config.disable_monkey_patching!
|
47
|
+
|
48
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
49
|
+
# be too noisy due to issues in dependencies.
|
50
|
+
config.warnings = true
|
51
|
+
|
52
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
53
|
+
# file, and it's useful to allow more verbose output when running an
|
54
|
+
# individual spec file.
|
55
|
+
if config.files_to_run.one?
|
56
|
+
# Use the documentation formatter for detailed output,
|
57
|
+
# unless a formatter has already been configured
|
58
|
+
# (e.g. via a command-line flag).
|
59
|
+
config.default_formatter = "doc"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class MyBasicObjectClass < BasicObject
|
64
|
+
end
|
65
|
+
|
66
|
+
class ConstantForTheSolePurposeOfUndefiningInspect
|
67
|
+
undef_method :inspect
|
68
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../..
|
3
3
|
specs:
|
4
|
-
specdiff (0.
|
4
|
+
specdiff (0.3.0.pre.rc1)
|
5
5
|
diff-lcs (~> 1.5)
|
6
6
|
hashdiff (~> 1.0)
|
7
7
|
|
@@ -12,7 +12,7 @@ GEM
|
|
12
12
|
public_suffix (>= 2.0.2, < 6.0)
|
13
13
|
crack (0.4.5)
|
14
14
|
rexml
|
15
|
-
diff-lcs (1.5.
|
15
|
+
diff-lcs (1.5.1)
|
16
16
|
domain_name (0.6.20231109)
|
17
17
|
ffi (1.16.3)
|
18
18
|
ffi-compiler (1.0.1)
|
data/lib/specdiff/colorize.rb
CHANGED
data/lib/specdiff/diff.rb
CHANGED
@@ -3,17 +3,67 @@ require "hashdiff"
|
|
3
3
|
class Specdiff::Differ::Hashdiff
|
4
4
|
extend ::Specdiff::Colorize
|
5
5
|
|
6
|
+
VALUE_CHANGE_PERCENTAGE_THRESHOLD = 0.2
|
7
|
+
TOTAL_CHANGES_FOR_GROUPING_THRESHOLD = 9
|
8
|
+
|
9
|
+
NEWLINE = "\n"
|
10
|
+
|
6
11
|
def self.diff(a, b)
|
7
12
|
# array_path: true returns the path as an array, which differentiates
|
8
13
|
# between symbol keys and string keys in hashes, while the string
|
9
14
|
# representation does not.
|
10
15
|
# hmm it really seems like use_lcs: true gives much less human-readable
|
11
16
|
# (human-comprehensible) output when arrays are involved.
|
12
|
-
::Hashdiff.diff(
|
17
|
+
hashdiff_diff = ::Hashdiff.diff(
|
13
18
|
a.value, b.value,
|
14
19
|
array_path: true,
|
15
20
|
use_lcs: false,
|
16
21
|
)
|
22
|
+
|
23
|
+
return hashdiff_diff if hashdiff_diff.empty?
|
24
|
+
|
25
|
+
change_percentage = _calculate_change_percentage(hashdiff_diff)
|
26
|
+
|
27
|
+
if change_percentage >= VALUE_CHANGE_PERCENTAGE_THRESHOLD
|
28
|
+
hashdiff_diff
|
29
|
+
else
|
30
|
+
a_text = ::Specdiff.hashprint(a.value)
|
31
|
+
b_text = ::Specdiff.hashprint(b.value)
|
32
|
+
|
33
|
+
diff = ::Specdiff.diff(a_text, b_text)
|
34
|
+
|
35
|
+
if diff.empty?
|
36
|
+
[]
|
37
|
+
else
|
38
|
+
diff.a.type = a.type
|
39
|
+
diff.b.type = b.type
|
40
|
+
|
41
|
+
diff
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self._calculate_change_percentage(hashdiff_diff)
|
47
|
+
value_change_count = hashdiff_diff.count { |element| element[0] == "~" }
|
48
|
+
addition_count = hashdiff_diff.count { |element| element[0] == "+" }
|
49
|
+
deletion_count = hashdiff_diff.count { |element| element[0] == "-" }
|
50
|
+
# puts "hashdiff_diff: #{hashdiff_diff.inspect}"
|
51
|
+
# puts "value_change_count: #{value_change_count.inspect}"
|
52
|
+
# puts "addition_count: #{addition_count.inspect}"
|
53
|
+
# puts "deletion_count: #{deletion_count.inspect}"
|
54
|
+
|
55
|
+
total_number_of_changes = [
|
56
|
+
value_change_count,
|
57
|
+
addition_count,
|
58
|
+
deletion_count,
|
59
|
+
].sum
|
60
|
+
|
61
|
+
change_fraction = Rational(value_change_count, total_number_of_changes)
|
62
|
+
change_percentage = change_fraction.to_f
|
63
|
+
# puts "change_fraction: #{change_fraction.inspect}"
|
64
|
+
# puts "change_percentage: #{change_percentage.inspect}"
|
65
|
+
|
66
|
+
change_percentage
|
17
67
|
end
|
18
68
|
|
19
69
|
def self.empty?(diff)
|
@@ -23,37 +73,66 @@ class Specdiff::Differ::Hashdiff
|
|
23
73
|
def self.stringify(diff)
|
24
74
|
result = +""
|
25
75
|
|
26
|
-
diff.raw.
|
27
|
-
|
28
|
-
|
29
|
-
|
76
|
+
total_changes = diff.raw.size
|
77
|
+
group_with_newlines = total_changes >= TOTAL_CHANGES_FOR_GROUPING_THRESHOLD
|
78
|
+
|
79
|
+
# hashdiff returns a structure like so:
|
80
|
+
# change[0] = '+', '-' or '~'. denoting type (addition, deletion or change)
|
81
|
+
# change[1] = the path to the change, in array form
|
82
|
+
# change[2] = the value, or the from value in case of '~'
|
83
|
+
# change[3] = the to value, only present when '~'
|
84
|
+
changes_grouped_by_type = diff.raw.group_by { |change| change[0] }
|
85
|
+
if (changes_grouped_by_type.keys - ["+", "-", "~"]).size > 0
|
86
|
+
$stderr.puts(
|
87
|
+
"Specdiff: hashdiff returned unexpected types: #{diff.raw.inspect}"
|
88
|
+
)
|
89
|
+
end
|
30
90
|
|
31
|
-
|
32
|
-
|
91
|
+
deletions = changes_grouped_by_type["-"] || []
|
92
|
+
additions = changes_grouped_by_type["+"] || []
|
93
|
+
value_changes = changes_grouped_by_type["~"] || []
|
33
94
|
|
34
|
-
|
35
|
-
|
36
|
-
|
95
|
+
deletions.each do |change|
|
96
|
+
value = change[2]
|
97
|
+
path = _stringify_path(change[1])
|
37
98
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
to = change[3]
|
99
|
+
result << "missing key: #{path} (#{::Specdiff.diff_inspect(value)})"
|
100
|
+
result << NEWLINE
|
101
|
+
end
|
42
102
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
103
|
+
if deletions.any? && additions.any? && group_with_newlines
|
104
|
+
result << NEWLINE
|
105
|
+
end
|
106
|
+
|
107
|
+
additions.each do |change|
|
108
|
+
value = change[2]
|
109
|
+
path = _stringify_path(change[1])
|
110
|
+
|
111
|
+
result << " new key: #{path} (#{::Specdiff.diff_inspect(value)})"
|
112
|
+
result << NEWLINE
|
113
|
+
end
|
114
|
+
|
115
|
+
if additions.any? && value_changes.any? && group_with_newlines
|
116
|
+
result << NEWLINE
|
117
|
+
end
|
118
|
+
|
119
|
+
value_changes.each do |change|
|
120
|
+
from = change[2]
|
121
|
+
to = change[3]
|
122
|
+
path = _stringify_path(change[1])
|
47
123
|
|
48
|
-
|
124
|
+
from_inspected = ::Specdiff.diff_inspect(from)
|
125
|
+
to_inspected = ::Specdiff.diff_inspect(to)
|
126
|
+
result << "changed key: #{path} (#{from_inspected} -> #{to_inspected})"
|
127
|
+
result << NEWLINE
|
49
128
|
end
|
50
129
|
|
51
130
|
colorize_by_line(result) do |line|
|
52
|
-
if line.start_with?("
|
131
|
+
if line.start_with?("missing key:")
|
53
132
|
red(line)
|
54
|
-
elsif line.start_with?(
|
133
|
+
elsif line.start_with?(/\s+new key:/)
|
55
134
|
green(line)
|
56
|
-
elsif line.start_with?("changed")
|
135
|
+
elsif line.start_with?("changed key:")
|
57
136
|
yellow(line)
|
58
137
|
else
|
59
138
|
reset_color(line)
|
data/lib/specdiff/differ/text.rb
CHANGED
@@ -15,20 +15,32 @@ class Specdiff::Differ::Text
|
|
15
15
|
b_value = b.value
|
16
16
|
|
17
17
|
if a_value.encoding != b_value.encoding
|
18
|
-
return <<~MSG
|
18
|
+
return colorize_by_line(<<~MSG) do |line|
|
19
19
|
Strings have different encodings:
|
20
20
|
#{a.value.encoding.inspect} != #{b.value.encoding.inspect}
|
21
21
|
MSG
|
22
|
+
# makes it stand out a bit more from the red of rspec output
|
23
|
+
reset_color(line)
|
24
|
+
end
|
22
25
|
end
|
23
26
|
|
24
27
|
diff = ""
|
25
28
|
|
29
|
+
# if there are no newlines then the text differ doesn't produce any valuable
|
30
|
+
# output. "word diffing" would improve this case.
|
31
|
+
if a_value.count(NEWLINE) <= 1 && b_value.count(NEWLINE) <= 1
|
32
|
+
return diff
|
33
|
+
end
|
34
|
+
|
26
35
|
a_lines = a_value.split(NEWLINE).map! { _1.chomp }
|
27
36
|
b_lines = b_value.split(NEWLINE).map! { _1.chomp }
|
37
|
+
|
38
|
+
file_length_difference = 0
|
39
|
+
|
28
40
|
hunks = ::Diff::LCS.diff(a_lines, b_lines).map do |piece|
|
29
41
|
::Diff::LCS::Hunk.new(
|
30
|
-
a_lines, b_lines, piece, CONTEXT_LINES,
|
31
|
-
)
|
42
|
+
a_lines, b_lines, piece, CONTEXT_LINES, file_length_difference,
|
43
|
+
).tap { |hunk| file_length_difference = hunk.file_length_difference }
|
32
44
|
end
|
33
45
|
|
34
46
|
hunks.each_cons(2) do |prev_hunk, current_hunk|
|
@@ -36,7 +48,7 @@ class Specdiff::Differ::Text
|
|
36
48
|
if current_hunk.overlaps?(prev_hunk)
|
37
49
|
current_hunk.merge(prev_hunk)
|
38
50
|
else
|
39
|
-
diff << prev_hunk.diff(:unified)
|
51
|
+
diff << prev_hunk.diff(:unified)
|
40
52
|
end
|
41
53
|
ensure
|
42
54
|
diff << NEWLINE
|
@@ -44,12 +56,14 @@ class Specdiff::Differ::Text
|
|
44
56
|
end
|
45
57
|
|
46
58
|
if hunks.last
|
47
|
-
diff <<
|
59
|
+
diff << NEWLINE
|
60
|
+
diff << hunks.last.diff(:unified)
|
48
61
|
end
|
49
62
|
|
50
63
|
return diff if diff == ""
|
51
64
|
|
52
|
-
diff <<
|
65
|
+
diff << NEWLINE
|
66
|
+
diff.lstrip!
|
53
67
|
|
54
68
|
return colorize_by_line(diff) do |line|
|
55
69
|
case line[0].chr
|
@@ -59,7 +73,7 @@ class Specdiff::Differ::Text
|
|
59
73
|
red(line)
|
60
74
|
when "@"
|
61
75
|
if line[1].chr == "@"
|
62
|
-
|
76
|
+
cyan(line)
|
63
77
|
else
|
64
78
|
reset_color(line)
|
65
79
|
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# Tries to print nested hash/array structures such that they are as convenient
|
2
|
+
# as possible for a diffing algorithm designed for text (like source code).
|
3
|
+
# Basically tries to reproduce consistent literal source code form for ruby
|
4
|
+
# hashes and arrays and objects (although it falls back to #inspect).
|
5
|
+
class Specdiff::Hashprint
|
6
|
+
def self.call(...)
|
7
|
+
new.call(...)
|
8
|
+
end
|
9
|
+
|
10
|
+
INDENTATION_SPACES = 2
|
11
|
+
SPACE = " ".freeze
|
12
|
+
COMMA = ",".freeze
|
13
|
+
NEWLINE = "\n".freeze
|
14
|
+
|
15
|
+
def call(thing)
|
16
|
+
@indentation_level = 0
|
17
|
+
@indentation_per_level = SPACE * INDENTATION_SPACES
|
18
|
+
@indent = ""
|
19
|
+
@skip_next_opening_indent = false
|
20
|
+
|
21
|
+
@output = StringIO.new
|
22
|
+
|
23
|
+
output(thing)
|
24
|
+
|
25
|
+
@output.string
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def recalculate_indent
|
31
|
+
@indent = @indentation_per_level * @indentation_level
|
32
|
+
end
|
33
|
+
|
34
|
+
def increase_indentation
|
35
|
+
@indentation_level += 1
|
36
|
+
recalculate_indent
|
37
|
+
end
|
38
|
+
|
39
|
+
def decrease_indentation
|
40
|
+
@indentation_level -= 1
|
41
|
+
recalculate_indent
|
42
|
+
end
|
43
|
+
|
44
|
+
def with_indentation_level(temporary_level)
|
45
|
+
old_level = @indentation_level
|
46
|
+
@indentation_level = temporary_level
|
47
|
+
recalculate_indent
|
48
|
+
|
49
|
+
yield
|
50
|
+
|
51
|
+
@indentation_level = old_level
|
52
|
+
recalculate_indent
|
53
|
+
end
|
54
|
+
|
55
|
+
def skip_next_opening_indent
|
56
|
+
@skip_next_opening_indent = true
|
57
|
+
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def this_indent_should_be_skipped
|
62
|
+
if @skip_next_opening_indent
|
63
|
+
@skip_next_opening_indent = false
|
64
|
+
true
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# #=== allows us to rely on Module implementing #=== instead of relying on the
|
71
|
+
# thing (which could be any kind of wacky object) having to implement
|
72
|
+
# #is_a? or #kind_of?
|
73
|
+
def output(thing)
|
74
|
+
if Hash === thing
|
75
|
+
output_hash(thing)
|
76
|
+
elsif Array === thing
|
77
|
+
output_array(thing)
|
78
|
+
else
|
79
|
+
output_unknown(thing)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
HASH_OPEN = "{".freeze
|
84
|
+
HASH_CLOSE = "}".freeze
|
85
|
+
HASHROCKET = "=>".freeze
|
86
|
+
COLON = ":".freeze
|
87
|
+
|
88
|
+
def output_hash(hash)
|
89
|
+
@output << @indent unless this_indent_should_be_skipped
|
90
|
+
|
91
|
+
@output << HASH_OPEN
|
92
|
+
# unless hash.empty?
|
93
|
+
@output << NEWLINE
|
94
|
+
|
95
|
+
increase_indentation
|
96
|
+
hash.each do |key, value|
|
97
|
+
@output << @indent
|
98
|
+
|
99
|
+
if key.is_a?(Symbol)
|
100
|
+
@output << key
|
101
|
+
@output << COLON
|
102
|
+
@output << SPACE
|
103
|
+
else
|
104
|
+
@output << ::Specdiff.diff_inspect(key)
|
105
|
+
@output << SPACE
|
106
|
+
@output << HASHROCKET
|
107
|
+
@output << SPACE
|
108
|
+
end
|
109
|
+
|
110
|
+
skip_next_opening_indent
|
111
|
+
output(value)
|
112
|
+
|
113
|
+
@output << COMMA
|
114
|
+
@output << NEWLINE
|
115
|
+
end
|
116
|
+
decrease_indentation
|
117
|
+
|
118
|
+
@output << @indent
|
119
|
+
# end
|
120
|
+
|
121
|
+
@output << HASH_CLOSE
|
122
|
+
end
|
123
|
+
|
124
|
+
ARRAY_OPEN = "[".freeze
|
125
|
+
ARRAY_CLOSE = "]".freeze
|
126
|
+
|
127
|
+
def output_array(array)
|
128
|
+
@output << @indent unless this_indent_should_be_skipped
|
129
|
+
|
130
|
+
@output << ARRAY_OPEN
|
131
|
+
|
132
|
+
# unless array.empty?
|
133
|
+
@output << NEWLINE
|
134
|
+
|
135
|
+
increase_indentation
|
136
|
+
array.each do |element|
|
137
|
+
output(element)
|
138
|
+
@output << COMMA
|
139
|
+
@output << NEWLINE
|
140
|
+
end
|
141
|
+
decrease_indentation
|
142
|
+
|
143
|
+
@output << @indent
|
144
|
+
# end
|
145
|
+
|
146
|
+
@output << ARRAY_CLOSE
|
147
|
+
end
|
148
|
+
|
149
|
+
def output_unknown(thing)
|
150
|
+
@output << @indent unless this_indent_should_be_skipped
|
151
|
+
|
152
|
+
@output << ::Specdiff.diff_inspect(thing)
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Specdiff::Inspect
|
2
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M:%S %z"
|
3
|
+
DATE_FORMAT = "%Y-%m-%d"
|
4
|
+
|
5
|
+
def self.call(...)
|
6
|
+
new.call(...)
|
7
|
+
end
|
8
|
+
|
9
|
+
# #=== allows us to rely on Module implementing #=== instead of relying on the
|
10
|
+
# thing (which could be any kind of wacky object) having to implement
|
11
|
+
# #is_a? or #kind_of?
|
12
|
+
def call(thing)
|
13
|
+
if Time === thing
|
14
|
+
"#<Time: #{thing.strftime(TIME_FORMAT)}>"
|
15
|
+
elsif DateTime === thing
|
16
|
+
"#<DateTime: #{thing.rfc3339}>"
|
17
|
+
elsif Date === thing
|
18
|
+
"#<Date: #{thing.strftime(DATE_FORMAT)}>"
|
19
|
+
elsif defined?(BigDecimal) && BigDecimal === thing
|
20
|
+
"#<BigDecimal: #{thing.to_s('F')}>"
|
21
|
+
else
|
22
|
+
begin
|
23
|
+
thing.inspect
|
24
|
+
rescue NoMethodError
|
25
|
+
inspect_anyway(thing)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private def inspect_anyway(uninspectable)
|
31
|
+
"#<uninspectable #{class_of(uninspectable)}>"
|
32
|
+
end
|
33
|
+
|
34
|
+
private def class_of(uninspectable)
|
35
|
+
uninspectable.class
|
36
|
+
rescue NoMethodError
|
37
|
+
singleton_class = class << uninspectable; self; end
|
38
|
+
singleton_class.ancestors
|
39
|
+
.find { |ancestor| !ancestor.equal?(singleton_class) }
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class RSpec::Support::Differ
|
2
|
+
alias old_diff diff
|
3
|
+
|
4
|
+
def diff(actual, expected)
|
5
|
+
diff = ::Specdiff.diff(expected, actual)
|
6
|
+
if diff.empty?
|
7
|
+
""
|
8
|
+
else
|
9
|
+
"\n#{diff}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# This stops rspec from truncating strings w/ ellipsis, as well as making the
|
15
|
+
# "inspect" output consistent with specdiff's.
|
16
|
+
class RSpec::Support::ObjectFormatter
|
17
|
+
class SpecdiffCustomInspector < BaseInspector
|
18
|
+
def self.can_inspect?(_)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
::Specdiff.diff_inspect(object)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
remove_const("INSPECTOR_CLASSES")
|
28
|
+
const_set("INSPECTOR_CLASSES", [SpecdiffCustomInspector])
|
29
|
+
|
30
|
+
def format(object)
|
31
|
+
::Specdiff.diff_inspect(object)
|
32
|
+
end
|
33
|
+
end
|
data/lib/specdiff/version.rb
CHANGED
data/lib/specdiff/webmock.rb
CHANGED
@@ -1 +1,41 @@
|
|
1
|
-
|
1
|
+
require "hashdiff"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module WebMock
|
5
|
+
class RequestBodyDiff
|
6
|
+
def initialize(request_signature, request_stub)
|
7
|
+
@request_signature = request_signature
|
8
|
+
@request_stub = request_stub
|
9
|
+
end
|
10
|
+
|
11
|
+
PrettyPrintableThingy = Struct.new(:specdiff) do
|
12
|
+
# webmock does not print the diff if it responds true to this.
|
13
|
+
def empty?
|
14
|
+
specdiff.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
# webmock prints the diff by passing us to PP.pp, which in turn uses this
|
18
|
+
# method.
|
19
|
+
def pretty_print(pp)
|
20
|
+
pp.text("\r") # remove a space that isn't supposed to be there
|
21
|
+
pp.text(specdiff.to_s)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def body_diff
|
26
|
+
specdiff = Specdiff.diff(request_stub_body, request_signature.body)
|
27
|
+
PrettyPrintableThingy.new(specdiff)
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :request_signature, :request_stub
|
31
|
+
private :request_signature, :request_stub
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def request_stub_body
|
36
|
+
request_stub.request_pattern &&
|
37
|
+
request_stub.request_pattern.body_pattern &&
|
38
|
+
request_stub.request_pattern.body_pattern.pattern
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/specdiff.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
require_relative "specdiff/version"
|
2
2
|
require_relative "specdiff/config"
|
3
3
|
require_relative "specdiff/colorize"
|
4
|
+
require_relative "specdiff/inspect"
|
5
|
+
require_relative "specdiff/hashprint"
|
4
6
|
require_relative "specdiff/compare"
|
5
7
|
|
6
8
|
module Specdiff
|
7
|
-
#
|
9
|
+
# Compare two things, returns a Specdiff::Diff.
|
8
10
|
def self.diff(...)
|
9
11
|
::Specdiff::Compare.call(...)
|
10
12
|
end
|
13
|
+
|
14
|
+
# Use Specdiff's implementation for turning a nested hash/array structure
|
15
|
+
# into a string. Optimized for diff quality.
|
16
|
+
def self.hashprint(...)
|
17
|
+
::Specdiff::Hashprint.call(...)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Use Specdiff's inspect, which has some extra logic layered in for
|
21
|
+
# dates/time/bigdecimal. For most objects this just delegates to #inspect.
|
22
|
+
def self.diff_inspect(...)
|
23
|
+
::Specdiff::Inspect.call(...)
|
24
|
+
end
|
11
25
|
end
|
12
26
|
|
13
27
|
require_relative "specdiff/diff"
|