specdiff 0.3.0.pre.rc1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,10 +13,11 @@ class Specdiff::Hashprint
13
13
  NEWLINE = "\n".freeze
14
14
 
15
15
  def call(thing)
16
+ @recursion_trail = []
16
17
  @indentation_level = 0
17
18
  @indentation_per_level = SPACE * INDENTATION_SPACES
18
19
  @indent = ""
19
- @skip_next_opening_indent = false
20
+ @next_value_is_hash_value = false
20
21
 
21
22
  @output = StringIO.new
22
23
 
@@ -41,37 +42,39 @@ private
41
42
  recalculate_indent
42
43
  end
43
44
 
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
45
+ def next_value_is_hash_value
46
+ @next_value_is_hash_value = true
57
47
 
58
48
  nil
59
49
  end
60
50
 
61
- def this_indent_should_be_skipped
62
- if @skip_next_opening_indent
63
- @skip_next_opening_indent = false
51
+ def outputting_hash_value?
52
+ if @next_value_is_hash_value
53
+ @next_value_is_hash_value = false
64
54
  true
65
55
  else
66
56
  false
67
57
  end
68
58
  end
69
59
 
60
+ def track_recursion(thing)
61
+ @recursion_trail.push(thing)
62
+ result = yield
63
+ @recursion_trail.pop
64
+ result
65
+ end
66
+
67
+ def deja_vu?(current_place)
68
+ @recursion_trail.any? { |previous_place| previous_place == current_place }
69
+ end
70
+
70
71
  # #=== allows us to rely on Module implementing #=== instead of relying on the
71
72
  # thing (which could be any kind of wacky object) having to implement
72
73
  # #is_a? or #kind_of?
73
74
  def output(thing)
74
- if Hash === thing
75
+ if deja_vu?(thing)
76
+ output_deja_vu(thing)
77
+ elsif Hash === thing
75
78
  output_hash(thing)
76
79
  elsif Array === thing
77
80
  output_array(thing)
@@ -86,69 +89,93 @@ private
86
89
  COLON = ":".freeze
87
90
 
88
91
  def output_hash(hash)
89
- @output << @indent unless this_indent_should_be_skipped
92
+ @output << @indent unless outputting_hash_value?
90
93
 
91
94
  @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
95
+ @output << NEWLINE
96
+
97
+ increase_indentation
98
+ track_recursion(hash) do
99
+ try_to_sort(hash)
100
+ .each do |key, value|
101
+ @output << @indent
102
+
103
+ if key.is_a?(Symbol)
104
+ @output << key
105
+ @output << COLON
106
+ @output << SPACE
107
+ else
108
+ @output << ::Specdiff.diff_inspect(key)
109
+ @output << SPACE
110
+ @output << HASHROCKET
111
+ @output << SPACE
112
+ end
113
+
114
+ next_value_is_hash_value
115
+ output(value)
116
+
117
+ @output << COMMA
118
+ @output << NEWLINE
108
119
  end
120
+ end
121
+ decrease_indentation
109
122
 
110
- skip_next_opening_indent
111
- output(value)
112
-
113
- @output << COMMA
114
- @output << NEWLINE
115
- end
116
- decrease_indentation
123
+ @output << @indent
124
+ @output << HASH_CLOSE
125
+ end
117
126
 
118
- @output << @indent
119
- # end
127
+ def try_to_sort(hash)
128
+ return hash unless hash.keys.all? { |k| String === k || Symbol === k }
120
129
 
121
- @output << HASH_CLOSE
130
+ hash.sort_by { |k, _v| k.to_s }
122
131
  end
123
132
 
124
133
  ARRAY_OPEN = "[".freeze
125
134
  ARRAY_CLOSE = "]".freeze
126
135
 
127
136
  def output_array(array)
128
- @output << @indent unless this_indent_should_be_skipped
137
+ @output << @indent unless outputting_hash_value?
129
138
 
130
139
  @output << ARRAY_OPEN
140
+ @output << NEWLINE
131
141
 
132
- # unless array.empty?
133
- @output << NEWLINE
134
-
135
- increase_indentation
142
+ increase_indentation
143
+ track_recursion(array) do
136
144
  array.each do |element|
137
145
  output(element)
138
146
  @output << COMMA
139
147
  @output << NEWLINE
140
148
  end
141
- decrease_indentation
142
-
143
- @output << @indent
144
- # end
149
+ end
150
+ decrease_indentation
145
151
 
152
+ @output << @indent
146
153
  @output << ARRAY_CLOSE
147
154
  end
148
155
 
149
156
  def output_unknown(thing)
150
- @output << @indent unless this_indent_should_be_skipped
157
+ @output << @indent unless outputting_hash_value?
151
158
 
152
159
  @output << ::Specdiff.diff_inspect(thing)
153
160
  end
161
+
162
+ # The stdlib inspect code returns this when you have recursive structures.
163
+ STANDARD_INSPECT_RECURSIVE_ARRAY = "[...]".freeze
164
+ STANDARD_INSPECT_RECURSIVE_HASH = "{...}".freeze
165
+
166
+ def output_deja_vu(thing)
167
+ @output << @indent unless outputting_hash_value?
168
+
169
+ case thing
170
+ when Array
171
+ # "#<Array ##{thing.object_id}>"
172
+ @output << STANDARD_INSPECT_RECURSIVE_ARRAY
173
+ when Hash
174
+ # "#<Hash ##{thing.object_id}>"
175
+ @output << STANDARD_INSPECT_RECURSIVE_HASH
176
+ else
177
+ # this should never happen
178
+ raise "Specdiff::Hashprint missing deja vu for: #{thing.inspect}"
179
+ end
180
+ end
154
181
  end
@@ -6,11 +6,17 @@ class Specdiff::Inspect
6
6
  new.call(...)
7
7
  end
8
8
 
9
+ def initialize
10
+ @recursion_trail = []
11
+ end
12
+
9
13
  # #=== allows us to rely on Module implementing #=== instead of relying on the
10
14
  # thing (which could be any kind of wacky object) having to implement
11
15
  # #is_a? or #kind_of?
12
16
  def call(thing)
13
- if Time === thing
17
+ if Hash === thing || Array === thing
18
+ recursive_replace_inspect(thing).inspect
19
+ elsif Time === thing
14
20
  "#<Time: #{thing.strftime(TIME_FORMAT)}>"
15
21
  elsif DateTime === thing
16
22
  "#<DateTime: #{thing.rfc3339}>"
@@ -18,6 +24,12 @@ class Specdiff::Inspect
18
24
  "#<Date: #{thing.strftime(DATE_FORMAT)}>"
19
25
  elsif defined?(BigDecimal) && BigDecimal === thing
20
26
  "#<BigDecimal: #{thing.to_s('F')}>"
27
+ elsif rspec_matcher?(thing)
28
+ # Turns out rspec depends on the recursion in its inspection logic to
29
+ # print the "description" of rspec matchers, in situations such as when
30
+ # using multi-matchers (.all, .or or .and), or when nesting them inside
31
+ # eachother (such as match([have_attributes(...)])).
32
+ thing.description
21
33
  else
22
34
  begin
23
35
  thing.inspect
@@ -27,6 +39,12 @@ class Specdiff::Inspect
27
39
  end
28
40
  end
29
41
 
42
+ private def rspec_matcher?(thing)
43
+ defined?(::Specdiff::RSpecIntegration) &&
44
+ ::RSpec::Support.is_a_matcher?(thing) &&
45
+ thing.respond_to?(:description)
46
+ end
47
+
30
48
  private def inspect_anyway(uninspectable)
31
49
  "#<uninspectable #{class_of(uninspectable)}>"
32
50
  end
@@ -38,4 +56,79 @@ class Specdiff::Inspect
38
56
  singleton_class.ancestors
39
57
  .find { |ancestor| !ancestor.equal?(singleton_class) }
40
58
  end
59
+
60
+ # recursion below
61
+
62
+ InspectWrapper = Struct.new(:text) do
63
+ def inspect
64
+ text
65
+ end
66
+ end
67
+
68
+ private def recursive_replace_inspect(thing)
69
+ if deja_vu?(thing)
70
+ # I've just been in this place before
71
+ # And I know it's my time to go...
72
+ return InspectWrapper.new(inspect_deja_vu(thing))
73
+ end
74
+
75
+ case thing
76
+ when Array
77
+ track_recursion(thing) do
78
+ thing.map { |element| recursive_replace_inspect(element) }
79
+ end
80
+ when Hash
81
+ track_recursion(thing) do
82
+ new_hash = {}
83
+
84
+ thing.each do |key, value|
85
+ new_hash[recursive_replace_inspect(key)] = recursive_replace_inspect(value)
86
+ end
87
+
88
+ new_hash
89
+ end
90
+ else
91
+ wrap_inspect(thing)
92
+ end
93
+ rescue SystemStackError => e
94
+ wrap_inspect(
95
+ thing,
96
+ text: "#{e.class}: #{e.message}\n\n" \
97
+ "encountered when inspecting #{thing.inspect}"
98
+ )
99
+ end
100
+
101
+ private def track_recursion(thing)
102
+ @recursion_trail.push(thing)
103
+ result = yield
104
+ @recursion_trail.pop
105
+ result
106
+ end
107
+
108
+ private def deja_vu?(current_place)
109
+ @recursion_trail.any? { |previous_place| previous_place == current_place }
110
+ end
111
+
112
+ private def wrap_inspect(thing, text: :_use_diff_inspect)
113
+ text = call(thing) if text == :_use_diff_inspect
114
+ InspectWrapper.new(text)
115
+ end
116
+
117
+ # The stdlib inspect code returns this when you have recursive structures.
118
+ STANDARD_INSPECT_RECURSIVE_ARRAY = "[...]".freeze
119
+ STANDARD_INSPECT_RECURSIVE_HASH = "{...}".freeze
120
+
121
+ private def inspect_deja_vu(thing)
122
+ case thing
123
+ when Array
124
+ # "#<Array ##{thing.object_id}>"
125
+ STANDARD_INSPECT_RECURSIVE_ARRAY
126
+ when Hash
127
+ # "#<Hash ##{thing.object_id}>"
128
+ STANDARD_INSPECT_RECURSIVE_HASH
129
+ else
130
+ # this should never happen
131
+ raise "Specdiff::Inspect missing deja vu for: #{thing.inspect}"
132
+ end
133
+ end
41
134
  end
@@ -1,3 +1,7 @@
1
+ raise "rspec must be required before specdiff/rspec!" unless defined?(RSpec)
2
+ raise "RSpec::Support is missing????" unless defined?(RSpec::Support)
3
+
4
+ # https://github.com/rspec/rspec-support/blob/v3.13.1/lib/rspec/support/differ.rb
1
5
  class RSpec::Support::Differ
2
6
  alias old_diff diff
3
7
 
@@ -13,21 +17,12 @@ end
13
17
 
14
18
  # This stops rspec from truncating strings w/ ellipsis, as well as making the
15
19
  # "inspect" output consistent with specdiff's.
20
+ # https://github.com/rspec/rspec-support/blob/v3.13.1/lib/rspec/support/object_formatter.rb
16
21
  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
22
  def format(object)
31
23
  ::Specdiff.diff_inspect(object)
32
24
  end
33
25
  end
26
+
27
+ # marker for successfully loading this integration
28
+ class Specdiff::RSpecIntegration; end # rubocop: disable Lint/EmptyClass
@@ -1,3 +1,3 @@
1
1
  module Specdiff
2
- VERSION = "0.3.0-rc1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,6 +1,6 @@
1
- require "hashdiff"
2
- require "json"
1
+ raise "webmock must be required before specdiff/webmock" unless defined?(WebMock)
3
2
 
3
+ # https://github.com/bblimke/webmock/blob/v3.23.0/lib/webmock/request_body_diff.rb
4
4
  module WebMock
5
5
  class RequestBodyDiff
6
6
  def initialize(request_signature, request_stub)
@@ -39,3 +39,6 @@ module WebMock
39
39
  end
40
40
  end
41
41
  end
42
+
43
+ # marker for successfully loading this integration
44
+ class Specdiff::WebmockIntegration; end # rubocop: disable Lint/EmptyClass
data/lib/specdiff.rb CHANGED
@@ -6,19 +6,14 @@ require_relative "specdiff/hashprint"
6
6
  require_relative "specdiff/compare"
7
7
 
8
8
  module Specdiff
9
- # Compare two things, returns a Specdiff::Diff.
10
9
  def self.diff(...)
11
10
  ::Specdiff::Compare.call(...)
12
11
  end
13
12
 
14
- # Use Specdiff's implementation for turning a nested hash/array structure
15
- # into a string. Optimized for diff quality.
16
13
  def self.hashprint(...)
17
14
  ::Specdiff::Hashprint.call(...)
18
15
  end
19
16
 
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
17
  def self.diff_inspect(...)
23
18
  ::Specdiff::Inspect.call(...)
24
19
  end
data/specdiff.gemspec CHANGED
@@ -6,10 +6,18 @@ Gem::Specification.new do |spec|
6
6
  spec.name = "specdiff"
7
7
  spec.version = Specdiff::VERSION
8
8
  spec.authors = ["Odin Heggvold Bekkelund"]
9
- spec.email = ["odinhb@protonmail.com"]
9
+ spec.email = ["odin.heggvold.bekkelund@dev.dodo.no"]
10
10
 
11
- spec.summary = "Improved request body diffs for webmock"
12
- spec.homepage = "https://github.com/odinhb/specdiff"
11
+ spec.summary = "Improved diffing for WebMock and RSpec"
12
+ spec.description = <<~TXT
13
+ Specdiff aims to improve both RSpec's and WebMock's diffing by applying \
14
+ opinionated heuristics, and comes with integrations (monkey-patches) for \
15
+ both. Particularly noteworthy improvements are made to working with deeply \
16
+ nested hash/array structures in RSpec, and plaintext/xml request bodies in \
17
+ WebMock.
18
+ TXT
19
+
20
+ spec.homepage = "https://github.com/dodoas/specdiff"
13
21
  spec.license = "MIT"
14
22
  spec.required_ruby_version = ">= 3.0.0"
15
23
 
@@ -17,13 +25,12 @@ Gem::Specification.new do |spec|
17
25
  spec.metadata["source_code_uri"] = spec.homepage
18
26
  spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
19
27
 
20
- # Specify which files should be added to the gem when it is released.
21
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
- `git ls-files -z`.split("\x0").reject do |f|
24
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
- end
26
- end
28
+ spec.files =
29
+ Dir["*.gemspec"] +
30
+ Dir["*.md"] +
31
+ Dir["*.txt"] +
32
+ Dir[".gitignore"] +
33
+ Dir["lib/**/*.rb"]
27
34
  spec.require_paths = ["lib"]
28
35
 
29
36
  spec.add_dependency "hashdiff", "~> 1.0"
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.3.0.pre.rc1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Odin Heggvold Bekkelund
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-02 00:00:00.000000000 Z
11
+ date: 2024-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashdiff
@@ -38,33 +38,23 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.5'
41
- description:
41
+ description: 'Specdiff aims to improve both RSpec''s and WebMock''s diffing by applying
42
+ opinionated heuristics, and comes with integrations (monkey-patches) for both. Particularly
43
+ noteworthy improvements are made to working with deeply nested hash/array structures
44
+ in RSpec, and plaintext/xml request bodies in WebMock.
45
+
46
+ '
42
47
  email:
43
- - odinhb@protonmail.com
48
+ - odin.heggvold.bekkelund@dev.dodo.no
44
49
  executables: []
45
50
  extensions: []
46
51
  extra_rdoc_files: []
47
52
  files:
48
- - ".rspec"
49
- - ".rubocop.yml"
50
- - ".tool-versions"
53
+ - ".gitignore"
51
54
  - CHANGELOG.md
52
- - Gemfile
53
- - Gemfile.lock
55
+ - DEVELOPMENT.md
54
56
  - LICENSE.txt
55
57
  - README.md
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
64
- - examples/webmock/Gemfile
65
- - examples/webmock/Gemfile.lock
66
- - examples/webmock/json.rb
67
- - examples/webmock/text.rb
68
58
  - glossary.txt
69
59
  - lib/specdiff.rb
70
60
  - lib/specdiff/colorize.rb
@@ -72,7 +62,7 @@ files:
72
62
  - lib/specdiff/config.rb
73
63
  - lib/specdiff/diff.rb
74
64
  - lib/specdiff/differ.rb
75
- - lib/specdiff/differ/hashdiff.rb
65
+ - lib/specdiff/differ/hash.rb
76
66
  - lib/specdiff/differ/not_found.rb
77
67
  - lib/specdiff/differ/text.rb
78
68
  - lib/specdiff/hashprint.rb
@@ -84,13 +74,13 @@ files:
84
74
  - lib/specdiff/version.rb
85
75
  - lib/specdiff/webmock.rb
86
76
  - specdiff.gemspec
87
- homepage: https://github.com/odinhb/specdiff
77
+ homepage: https://github.com/dodoas/specdiff
88
78
  licenses:
89
79
  - MIT
90
80
  metadata:
91
- homepage_uri: https://github.com/odinhb/specdiff
92
- source_code_uri: https://github.com/odinhb/specdiff
93
- changelog_uri: https://github.com/odinhb/specdiff/CHANGELOG.md
81
+ homepage_uri: https://github.com/dodoas/specdiff
82
+ source_code_uri: https://github.com/dodoas/specdiff
83
+ changelog_uri: https://github.com/dodoas/specdiff/CHANGELOG.md
94
84
  post_install_message:
95
85
  rdoc_options: []
96
86
  require_paths:
@@ -102,12 +92,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
92
  version: 3.0.0
103
93
  required_rubygems_version: !ruby/object:Gem::Requirement
104
94
  requirements:
105
- - - ">"
95
+ - - ">="
106
96
  - !ruby/object:Gem::Version
107
- version: 1.3.1
97
+ version: '0'
108
98
  requirements: []
109
99
  rubygems_version: 3.2.22
110
100
  signing_key:
111
101
  specification_version: 4
112
- summary: Improved request body diffs for webmock
102
+ summary: Improved diffing for WebMock and RSpec
113
103
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper