specdiff 0.1.1 → 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 +28 -0
- data/Gemfile.lock +1 -1
- data/README.md +70 -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/glossary.txt +27 -5
- data/lib/specdiff/colorize.rb +5 -0
- data/lib/specdiff/compare.rb +95 -0
- data/lib/specdiff/config.rb +9 -6
- data/lib/specdiff/diff.rb +3 -1
- data/lib/specdiff/differ/hashdiff.rb +101 -25
- data/lib/specdiff/differ/not_found.rb +1 -1
- data/lib/specdiff/differ/text.rb +21 -7
- data/lib/specdiff/differ.rb +1 -94
- data/lib/specdiff/hashprint.rb +154 -0
- data/lib/specdiff/inspect.rb +41 -0
- data/lib/specdiff/plugins.rb +5 -7
- 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 +17 -3
- metadata +20 -11
- data/lib/specdiff/threadlocal.rb +0 -8
- 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/glossary.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
"raw diff"
|
2
|
+
the return value from a differ or plugin's #diff method
|
3
|
+
this can be anything, from an array of arrays (hashdiff) to a string with a
|
4
|
+
git diff inside
|
5
|
+
|
1
6
|
diff
|
2
7
|
the return value from the Specdiff::diff method.
|
3
8
|
this is not the direct return value from a plugin/differ, that is the
|
@@ -8,17 +13,34 @@ differ
|
|
8
13
|
human-comprehensible diff output for your terminal
|
9
14
|
|
10
15
|
plugin
|
11
|
-
external differ (
|
12
|
-
|
16
|
+
external differ (has to respond to more methods)
|
17
|
+
may be provided from outside the gem (for example by a user drowning in xml)
|
18
|
+
|
19
|
+
"built in differ"
|
20
|
+
differ living in the specdiff/differ directory
|
21
|
+
|
22
|
+
"built in plugin"
|
23
|
+
plugin shipped with the gem, but needs to be loaded using Specdiff.load!
|
13
24
|
|
14
25
|
type
|
15
26
|
a symbol like :text, :json or :hash which denotes the type of data in a way
|
16
27
|
which is useful for picking a differ
|
17
28
|
|
29
|
+
a plugin returns a type from its #id method
|
30
|
+
|
18
31
|
:text
|
19
32
|
a string which likely contains plaintext data of some kind
|
20
33
|
|
34
|
+
"plugin type"
|
35
|
+
a type added by loading a plugin (not built into specdiff)
|
36
|
+
|
21
37
|
side
|
22
|
-
an object containing
|
23
|
-
|
24
|
-
|
38
|
+
an object containing a value and a type
|
39
|
+
|
40
|
+
used to represent the two sides to a comparison
|
41
|
+
|
42
|
+
when defining a plugin, you receive two sides: a and b, to various methods
|
43
|
+
|
44
|
+
compare
|
45
|
+
the procedure that implements the main function of specdiff including
|
46
|
+
accounting for any plugin types and differs
|
data/lib/specdiff/colorize.rb
CHANGED
@@ -0,0 +1,95 @@
|
|
1
|
+
class Specdiff::Compare
|
2
|
+
Side = Struct.new(:value, :type, keyword_init: true)
|
3
|
+
|
4
|
+
def self.call(...)
|
5
|
+
new.call(...)
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(raw_a, raw_b)
|
9
|
+
a = parse_side(raw_a)
|
10
|
+
b = parse_side(raw_b)
|
11
|
+
|
12
|
+
if a.type == :text && b.type == :binary
|
13
|
+
new_b = try_reencode(b.value, a.value.encoding)
|
14
|
+
if new_b
|
15
|
+
b = b.dup
|
16
|
+
b.type = :text
|
17
|
+
b.value = new_b
|
18
|
+
end
|
19
|
+
elsif a.type == :binary && b.type == :text
|
20
|
+
new_a = try_reencode(a.value, b.value.encoding)
|
21
|
+
if new_a
|
22
|
+
a = a.dup
|
23
|
+
a.type = :text
|
24
|
+
a.value = new_a
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
differ = pick_differ(a, b)
|
29
|
+
raw = differ.diff(a, b)
|
30
|
+
|
31
|
+
if raw.is_a?(::Specdiff::Diff) # detect recursive plugins, such as json
|
32
|
+
raw
|
33
|
+
else
|
34
|
+
::Specdiff::Diff.new(raw: raw, differ: differ, a: a, b: b)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def parse_side(raw_value)
|
41
|
+
type = detect_type(raw_value)
|
42
|
+
|
43
|
+
Side.new(value: raw_value, type: type)
|
44
|
+
end
|
45
|
+
|
46
|
+
def detect_type(thing)
|
47
|
+
if (type = detect_plugin_types(thing))
|
48
|
+
type
|
49
|
+
elsif thing.is_a?(Hash)
|
50
|
+
:hash
|
51
|
+
elsif thing.is_a?(Array)
|
52
|
+
:array
|
53
|
+
elsif thing.is_a?(String) && thing.encoding == Encoding::BINARY
|
54
|
+
:binary
|
55
|
+
elsif thing.is_a?(String)
|
56
|
+
:text
|
57
|
+
elsif thing.nil?
|
58
|
+
:nil
|
59
|
+
else
|
60
|
+
:unknown
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def detect_plugin_types(thing)
|
65
|
+
Specdiff.plugins
|
66
|
+
.filter { |plugin| plugin.respond_to?(:detect_type) }
|
67
|
+
.detect { |plugin| plugin.detect_type(thing) }
|
68
|
+
&.id
|
69
|
+
end
|
70
|
+
|
71
|
+
def try_reencode(binary_string, target_encoding)
|
72
|
+
binary_string.encode(target_encoding)
|
73
|
+
rescue StandardError
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def pick_differ(a, b)
|
78
|
+
if (differ = pick_plugin_differ(a, b))
|
79
|
+
differ
|
80
|
+
elsif a.type == :text && b.type == :text
|
81
|
+
Specdiff::Differ::Text
|
82
|
+
elsif a.type == :hash && b.type == :hash
|
83
|
+
Specdiff::Differ::Hashdiff
|
84
|
+
elsif a.type == :array && b.type == :array
|
85
|
+
Specdiff::Differ::Hashdiff
|
86
|
+
else
|
87
|
+
Specdiff::Differ::NotFound
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def pick_plugin_differ(a, b)
|
92
|
+
Specdiff.plugins
|
93
|
+
.detect { |plugin| plugin.compatible?(a, b) }
|
94
|
+
end
|
95
|
+
end
|
data/lib/specdiff/config.rb
CHANGED
@@ -5,23 +5,26 @@ module Specdiff
|
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
threadlocal[:config] ||= default_configuration
|
8
|
+
class << self
|
9
|
+
attr_reader :config
|
11
10
|
end
|
12
11
|
|
12
|
+
DEFAULT = Config.new(colorize: true).freeze
|
13
|
+
@config = DEFAULT.dup
|
14
|
+
|
13
15
|
# private, used for testing
|
14
16
|
def self._set_config(new_config)
|
15
|
-
|
17
|
+
@config = new_config
|
16
18
|
end
|
17
19
|
|
18
20
|
# Set the configuration
|
19
21
|
def self.configure
|
20
|
-
yield(config)
|
22
|
+
yield(@config)
|
23
|
+
@config
|
21
24
|
end
|
22
25
|
|
23
26
|
# Generates the default configuration
|
24
27
|
def self.default_configuration
|
25
|
-
|
28
|
+
DEFAULT
|
26
29
|
end
|
27
30
|
end
|
data/lib/specdiff/diff.rb
CHANGED
@@ -1,20 +1,69 @@
|
|
1
1
|
require "hashdiff"
|
2
|
-
require "pp"
|
3
2
|
|
4
3
|
class Specdiff::Differ::Hashdiff
|
5
4
|
extend ::Specdiff::Colorize
|
6
5
|
|
6
|
+
VALUE_CHANGE_PERCENTAGE_THRESHOLD = 0.2
|
7
|
+
TOTAL_CHANGES_FOR_GROUPING_THRESHOLD = 9
|
8
|
+
|
9
|
+
NEWLINE = "\n"
|
10
|
+
|
7
11
|
def self.diff(a, b)
|
8
12
|
# array_path: true returns the path as an array, which differentiates
|
9
13
|
# between symbol keys and string keys in hashes, while the string
|
10
14
|
# representation does not.
|
11
15
|
# hmm it really seems like use_lcs: true gives much less human-readable
|
12
16
|
# (human-comprehensible) output when arrays are involved.
|
13
|
-
Hashdiff.diff(
|
17
|
+
hashdiff_diff = ::Hashdiff.diff(
|
14
18
|
a.value, b.value,
|
15
19
|
array_path: true,
|
16
20
|
use_lcs: false,
|
17
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
|
18
67
|
end
|
19
68
|
|
20
69
|
def self.empty?(diff)
|
@@ -22,41 +71,68 @@ class Specdiff::Differ::Hashdiff
|
|
22
71
|
end
|
23
72
|
|
24
73
|
def self.stringify(diff)
|
25
|
-
diff.raw.pretty_inspect
|
26
|
-
|
27
74
|
result = +""
|
28
75
|
|
29
|
-
diff.raw.
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
33
90
|
|
34
|
-
|
35
|
-
|
91
|
+
deletions = changes_grouped_by_type["-"] || []
|
92
|
+
additions = changes_grouped_by_type["+"] || []
|
93
|
+
value_changes = changes_grouped_by_type["~"] || []
|
36
94
|
|
37
|
-
|
38
|
-
|
39
|
-
|
95
|
+
deletions.each do |change|
|
96
|
+
value = change[2]
|
97
|
+
path = _stringify_path(change[1])
|
40
98
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
to = change[3]
|
99
|
+
result << "missing key: #{path} (#{::Specdiff.diff_inspect(value)})"
|
100
|
+
result << NEWLINE
|
101
|
+
end
|
45
102
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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])
|
50
123
|
|
51
|
-
|
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
|
52
128
|
end
|
53
129
|
|
54
130
|
colorize_by_line(result) do |line|
|
55
|
-
if line.start_with?("
|
131
|
+
if line.start_with?("missing key:")
|
56
132
|
red(line)
|
57
|
-
elsif line.start_with?(
|
133
|
+
elsif line.start_with?(/\s+new key:/)
|
58
134
|
green(line)
|
59
|
-
elsif line.start_with?("changed")
|
135
|
+
elsif line.start_with?("changed key:")
|
60
136
|
yellow(line)
|
61
137
|
else
|
62
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
|
data/lib/specdiff/differ.rb
CHANGED
@@ -1,97 +1,4 @@
|
|
1
|
-
|
2
|
-
Side = Struct.new(:value, :type, keyword_init: true)
|
3
|
-
|
4
|
-
def self.call(...)
|
5
|
-
new.call(...)
|
6
|
-
end
|
7
|
-
|
8
|
-
def call(raw_a, raw_b)
|
9
|
-
a = parse_side(raw_a)
|
10
|
-
b = parse_side(raw_b)
|
11
|
-
|
12
|
-
if a.type == :text && b.type == :binary
|
13
|
-
new_b = try_reencode(b.value, a.value.encoding)
|
14
|
-
if new_b
|
15
|
-
b = b.dup
|
16
|
-
b.type = :text
|
17
|
-
b.value = new_b
|
18
|
-
end
|
19
|
-
elsif a.type == :binary && b.type == :text
|
20
|
-
new_a = try_reencode(a.value, b.value.encoding)
|
21
|
-
if new_a
|
22
|
-
a = a.dup
|
23
|
-
a.type = :text
|
24
|
-
a.value = new_a
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
differ = pick_differ(a, b)
|
29
|
-
raw = differ.diff(a, b)
|
30
|
-
|
31
|
-
if raw.is_a?(::Specdiff::Diff) # detect recursive plugins, such as json
|
32
|
-
raw
|
33
|
-
else
|
34
|
-
::Specdiff::Diff.new(raw: raw, differ: differ, a: a, b: b)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def parse_side(raw_value)
|
41
|
-
type = detect_type(raw_value)
|
42
|
-
|
43
|
-
Side.new(value: raw_value, type: type)
|
44
|
-
end
|
45
|
-
|
46
|
-
def detect_type(thing)
|
47
|
-
if (type = detect_plugin_types(thing))
|
48
|
-
type
|
49
|
-
elsif thing.is_a?(Hash)
|
50
|
-
:hash
|
51
|
-
elsif thing.is_a?(Array)
|
52
|
-
:array
|
53
|
-
elsif thing.is_a?(String) && thing.encoding == Encoding::BINARY
|
54
|
-
:binary
|
55
|
-
elsif thing.is_a?(String)
|
56
|
-
:text
|
57
|
-
elsif thing.nil?
|
58
|
-
:nil
|
59
|
-
else
|
60
|
-
:unknown
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def detect_plugin_types(thing)
|
65
|
-
Specdiff.plugins
|
66
|
-
.filter { |plugin| plugin.respond_to?(:detect_type) }
|
67
|
-
.detect { |plugin| plugin.detect_type(thing) }
|
68
|
-
&.id
|
69
|
-
end
|
70
|
-
|
71
|
-
def try_reencode(binary_string, target_encoding)
|
72
|
-
binary_string.encode(target_encoding)
|
73
|
-
rescue StandardError
|
74
|
-
nil
|
75
|
-
end
|
76
|
-
|
77
|
-
def pick_differ(a, b)
|
78
|
-
if (differ = pick_plugin_differ(a, b))
|
79
|
-
differ
|
80
|
-
elsif a.type == :text && b.type == :text
|
81
|
-
Specdiff::Differ::Text
|
82
|
-
elsif a.type == :hash && b.type == :hash
|
83
|
-
Specdiff::Differ::Hashdiff
|
84
|
-
elsif a.type == :array && b.type == :array
|
85
|
-
Specdiff::Differ::Hashdiff
|
86
|
-
else
|
87
|
-
Specdiff::Differ::NotFound
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def pick_plugin_differ(a, b)
|
92
|
-
Specdiff.plugins
|
93
|
-
.detect { |plugin| plugin.compatible?(a, b) }
|
94
|
-
end
|
1
|
+
module Specdiff::Differ
|
95
2
|
end
|
96
3
|
|
97
4
|
# require only the builtin differs, plugins are optionally loaded later
|