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.
@@ -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
@@ -1,9 +1,8 @@
1
1
  module Specdiff
2
- THREADLOCAL_PLUGINS_KEY = :__specdiff_plugins
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
- ::Specdiff.plugins << plugin
61
+ @plugins << plugin
63
62
  end
64
63
 
65
64
  # private
66
65
  def self._clear_plugins!
67
- threadlocal[THREADLOCAL_PLUGINS_KEY] = []
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
@@ -1,3 +1,3 @@
1
1
  module Specdiff
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0-rc1"
3
3
  end
@@ -1 +1,41 @@
1
- require_relative "webmock/request_body_diff"
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
- # Diff two things
9
+ # Compare two things, returns a Specdiff::Diff.
8
10
  def self.diff(...)
9
- ::Specdiff::Differ.call(...)
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.1.1
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: 2023-12-04 00:00:00.000000000 Z
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/threadlocal.rb
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: '0'
107
+ version: 1.3.1
99
108
  requirements: []
100
- rubygems_version: 3.4.10
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: []
@@ -1,8 +0,0 @@
1
- module Specdiff
2
- THREADLOCAL_KEY = :__specdiff_threadlocals
3
-
4
- def self.threadlocal
5
- Thread.current.thread_variable_get(THREADLOCAL_KEY) ||
6
- Thread.current.thread_variable_set(THREADLOCAL_KEY, {})
7
- end
8
- end
@@ -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