specdiff 0.1.1 → 0.3.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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