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,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
|
data/lib/specdiff/plugins.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
module Specdiff
|
2
|
-
|
3
|
-
|
4
|
-
def self.plugins
|
5
|
-
threadlocal[THREADLOCAL_PLUGINS_KEY]
|
2
|
+
class << self
|
3
|
+
attr_reader :plugins
|
6
4
|
end
|
5
|
+
@plugins = []
|
7
6
|
|
8
7
|
BUILTIN_PLUGINS = %i[json]
|
9
8
|
BUILTIN_TYPES = %i[hash array binary text nil]
|
@@ -59,14 +58,13 @@ module Specdiff
|
|
59
58
|
MSG
|
60
59
|
end
|
61
60
|
|
62
|
-
|
61
|
+
@plugins << plugin
|
63
62
|
end
|
64
63
|
|
65
64
|
# private
|
66
65
|
def self._clear_plugins!
|
67
|
-
|
66
|
+
@plugins = []
|
68
67
|
end
|
69
|
-
_clear_plugins!
|
70
68
|
|
71
69
|
module Plugins
|
72
70
|
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,12 +1,26 @@
|
|
1
1
|
require_relative "specdiff/version"
|
2
|
-
require_relative "specdiff/threadlocal"
|
3
2
|
require_relative "specdiff/config"
|
4
3
|
require_relative "specdiff/colorize"
|
4
|
+
require_relative "specdiff/inspect"
|
5
|
+
require_relative "specdiff/hashprint"
|
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
|
-
::Specdiff::
|
11
|
+
::Specdiff::Compare.call(...)
|
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(...)
|
10
24
|
end
|
11
25
|
end
|
12
26
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: specdiff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0.pre.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Odin Heggvold Bekkelund
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-04-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashdiff
|
@@ -38,7 +38,7 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.5'
|
41
|
-
description:
|
41
|
+
description:
|
42
42
|
email:
|
43
43
|
- odinhb@protonmail.com
|
44
44
|
executables: []
|
@@ -54,6 +54,13 @@ files:
|
|
54
54
|
- LICENSE.txt
|
55
55
|
- README.md
|
56
56
|
- Rakefile
|
57
|
+
- assets/webmock_json_with_specdiff.png
|
58
|
+
- assets/webmock_text_with_specdiff.png
|
59
|
+
- examples/rspec/.rspec
|
60
|
+
- examples/rspec/Gemfile
|
61
|
+
- examples/rspec/Gemfile.lock
|
62
|
+
- examples/rspec/spec/example_spec.rb
|
63
|
+
- examples/rspec/spec/spec_helper.rb
|
57
64
|
- examples/webmock/Gemfile
|
58
65
|
- examples/webmock/Gemfile.lock
|
59
66
|
- examples/webmock/json.rb
|
@@ -61,19 +68,21 @@ files:
|
|
61
68
|
- glossary.txt
|
62
69
|
- lib/specdiff.rb
|
63
70
|
- lib/specdiff/colorize.rb
|
71
|
+
- lib/specdiff/compare.rb
|
64
72
|
- lib/specdiff/config.rb
|
65
73
|
- lib/specdiff/diff.rb
|
66
74
|
- lib/specdiff/differ.rb
|
67
75
|
- lib/specdiff/differ/hashdiff.rb
|
68
76
|
- lib/specdiff/differ/not_found.rb
|
69
77
|
- lib/specdiff/differ/text.rb
|
78
|
+
- lib/specdiff/hashprint.rb
|
79
|
+
- lib/specdiff/inspect.rb
|
70
80
|
- lib/specdiff/plugin.rb
|
71
81
|
- lib/specdiff/plugins.rb
|
72
82
|
- lib/specdiff/plugins/json.rb
|
73
|
-
- lib/specdiff/
|
83
|
+
- lib/specdiff/rspec.rb
|
74
84
|
- lib/specdiff/version.rb
|
75
85
|
- lib/specdiff/webmock.rb
|
76
|
-
- lib/specdiff/webmock/request_body_diff.rb
|
77
86
|
- specdiff.gemspec
|
78
87
|
homepage: https://github.com/odinhb/specdiff
|
79
88
|
licenses:
|
@@ -82,7 +91,7 @@ metadata:
|
|
82
91
|
homepage_uri: https://github.com/odinhb/specdiff
|
83
92
|
source_code_uri: https://github.com/odinhb/specdiff
|
84
93
|
changelog_uri: https://github.com/odinhb/specdiff/CHANGELOG.md
|
85
|
-
post_install_message:
|
94
|
+
post_install_message:
|
86
95
|
rdoc_options: []
|
87
96
|
require_paths:
|
88
97
|
- lib
|
@@ -93,12 +102,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
102
|
version: 3.0.0
|
94
103
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
104
|
requirements:
|
96
|
-
- - "
|
105
|
+
- - ">"
|
97
106
|
- !ruby/object:Gem::Version
|
98
|
-
version:
|
107
|
+
version: 1.3.1
|
99
108
|
requirements: []
|
100
|
-
rubygems_version: 3.
|
101
|
-
signing_key:
|
109
|
+
rubygems_version: 3.2.22
|
110
|
+
signing_key:
|
102
111
|
specification_version: 4
|
103
112
|
summary: Improved request body diffs for webmock
|
104
113
|
test_files: []
|
data/lib/specdiff/threadlocal.rb
DELETED
@@ -1,41 +0,0 @@
|
|
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
|