specdiff 0.3.0.pre.rc1 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68cf98631e494fb5f016c0b3632cce58f616dad54ba11c782b37e3de4f2b36c9
4
- data.tar.gz: b97fee8a8e586b383a71bf0dcdcb32be7cf1c135356a90ec2ae7488fc2082071
3
+ metadata.gz: 16d43f0ff6248844478dc69d99ebb9e5de1212804fba96b859d947e446e5fae7
4
+ data.tar.gz: 0c9e973795920664617d2c57211e40569a0aee10ee6b6c35b02680a6789edf2f
5
5
  SHA512:
6
- metadata.gz: b374cadd1aa564cf8252c4b3d6c4ca2be952b147891cdbec9f0799383349bf5bcf61a1ef94979bc2b1f7dbe4d850e586d4c11f1271c74a14dc9be09cffc4e184
7
- data.tar.gz: 8379b0c263ae20900ec31b63ea9600e3c15cef8fe9b597c1e1349a708c323d03ee8bfaa46d6f6eeb666e2af0ee751e7ef2cd47917e927eeceb48a7f076a1e126
6
+ metadata.gz: a21ca995b7418a69deaeb6c024f40b2fc6342200edc33f4179f979ce7555bf346b29012184784b59f0736ac55b089a55836dd21756e14100d7a76260b5c7d69a
7
+ data.tar.gz: e457b0266ddc5f11df693baae9594dd790498e687ce7a4d263b9b291108af2a14f976499dd3b1eee9294a6827e2f15f527c0ae451035c8f73dc2dbaea9245f2d
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ # local installed gem bundles, ala node_modules for npm
14
+ .gem-bundle/
data/CHANGELOG.md CHANGED
@@ -7,7 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.3.0-rc1]
10
+ ## [0.3.0] - 2024-04-16
11
+
12
+ ### Changed
13
+
14
+ - Hashprint now sorts keys (if all the keys are strings or symbols), leading to better text diffs from the hash differ.
15
+
16
+ ## [0.3.0.rc2] - 2024-04-05
17
+
18
+ ### Changed
19
+
20
+ - Rework how hashdiff's output gets printed.
21
+ - Rework switching heuristic between text diff/hashdiff in hash differ.
22
+
23
+ ### Fixed
24
+
25
+ - The RSpec integration now inspects hashes and arrays recursively. (Like rspec does by default)
26
+ - RSpec integration no longer breaks description output of matchers when using multi matchers (like .all or .and)
27
+ - The hash differ now deals with recursive hashes and arrays
28
+ - RSpec integration no longer breaks description output of matchers that are part of a diff inside an array or hash. (like when doing `match([have_attributes(...)])`)
29
+
30
+ ## [0.3.0-rc1] - 2024-04-02
11
31
 
12
32
  ### Added
13
33
 
data/DEVELOPMENT.md ADDED
@@ -0,0 +1,74 @@
1
+ # Development
2
+
3
+ ## Glossary
4
+
5
+ Check out the [glossary](./glossary.txt) to make sure we are using the same
6
+ words for things.
7
+
8
+ ## Setup
9
+
10
+ Install the software versions specified in `.tool-versions`.
11
+
12
+ Run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run
13
+ the tests and linter and make sure they're green
14
+ before starting to make your changes.
15
+
16
+ Run `bundle exec rake -AD` for a full list of all the available tasks you may use for development purposes.
17
+
18
+ Run `bundle exec rake` to run the tests and linter.
19
+
20
+ ## Examples
21
+
22
+ The `examples/` directory is used as an ad-hoc integration testing method.
23
+ [Read more here](./examples/)
24
+
25
+ ## Pull request checklist
26
+
27
+ - [ ] Make sure the tests are passing
28
+ - [ ] Make sure the linter is happy
29
+ - [ ] Make sure the `examples/` look the same or have suffered and improvement
30
+ - [ ] Update the unreleased section of the [changelog](./CHANGELOG.md) with a human readable explanation of your changes
31
+
32
+ ## Architecture
33
+
34
+ High level description of the heuristic specdiff implements
35
+
36
+ 1. receive 2 pieces of data: `a` and `b`
37
+ 2. determine types for `a` and `b`
38
+ 1. test against plugin types
39
+ 2. test against built in types
40
+ 3. fall back to the `:unknown` type
41
+ 3. determine which differ is appropriate for the types
42
+ 1. test against plugin differs
43
+ 2. test against built in differs
44
+ 3. fall back to the null differ (`NotFound`)
45
+ 7. run the selected differ with a and b
46
+ 8. package it into a `::Specdiff::Diff` which records the detected types
47
+
48
+ \<time passes>
49
+
50
+ 6. at some point later when `#to_s` is invoked, stringify the diff using the differ's `#stringify`
51
+
52
+ ## Maintainer's notes
53
+
54
+ ### Release procedure
55
+
56
+ - [ ] unit tests are passing (`$ bundle exec rake test`)
57
+ - [ ] linter is happy (`$ bundle exec rake lint`)
58
+ - [ ] `examples/` look good
59
+ - [ ] check the package size using `$ bundle exec inspect_build`, make sure you haven't added any large files by accident
60
+ - [ ] update the version number in `version.rb`
61
+ - [ ] make sure the `examples/` `Gemfile.lock` files are updated (run bundle install)
62
+ - [ ] make sure `Gemfile.lock` is updated (run bundle install)
63
+ - [ ] move unreleased changes to the next version in the [changelog](./CHANGELOG.md)
64
+ - [ ] commit in the form "vX.X.X" and push
65
+ - [ ] make sure the pipeline is green
66
+ - [ ] `$ bundle exec rake release`
67
+
68
+ ### Contemplated improvements (AKA the todo list)
69
+
70
+ - [ ] word diff
71
+ - [ ] yard documentation?
72
+ - [ ] 2.7.0 support
73
+ - [ ] actual integration tests (replacing `examples/`)
74
+ - [ ] real documentation for the plugin interface
data/README.md CHANGED
@@ -1,36 +1,40 @@
1
1
  # Specdiff
2
2
 
3
- A gem for improving diff output in webmock.
3
+ Specdiff is an opinionated gem that provides diffing between arbitrary data. It
4
+ was built in an effort to improve [WebMock](https://rubygems.org/gems/webmock)'s
5
+ and [RSpec](https://rubygems.org/gems/rspec)'s diff output, and
6
+ comes with integrations for both. It produces output that looks very similiar to
7
+ RSpec, but with better support for nested hash/array structures.
4
8
 
5
- Webmock currently has a somewhat hidden feature where it will produce a diff
6
- between a request body and a registered stub when making an unregistered request
7
- if they happen to both be json (including setting content type). This gem aims
8
- to bring text diffing (ala rspec) to webmock via monkey-patch, as well as
9
- dropping the content type requirement.
9
+ By default, specdiff will produce diff output for multiline text by using
10
+ [diff-lcs](https://rubygems.org/gems/diff-lcs):
10
11
 
11
- Specdiff automagically detects the types of provided data and prints a suitable
12
- diff between them.
12
+ ![example of multiline text](assets/specdiff_multiline_text_diff_example.png)
13
13
 
14
- ## Cool, what does it look like?
14
+ It also diffs hashes and arrays:
15
15
 
16
- When specdiff is enabled, webmock will produce a generic text diff using
17
- [`Diff::LCS`](https://github.com/halostatue/diff-lcs) (basically the same
18
- diff as rspec uses):
16
+ | ![img](assets/specdiff_hashdiff_diff_example.png) | ![img](assets/specdiff_text_based_hash_diff_example.png) |
17
+ | --- | --- |
18
+ | Hashdiff based hash diff | Text based hash diff |
19
19
 
20
- ![webmock_text_with_specdiff](./assets/webmock_text_with_specdiff.png)
20
+ Specdiff will automatically switch between these two types of hash/array diff
21
+ output depending on what it thinks is more readable. The first type of output
22
+ showcased here comes from the
23
+ [hashdiff](https://rubygems.org/gems/hashdiff) gem, one of specdiff's
24
+ dependencies. The other is the *text diff* run on the output of *hashprint*.
25
+ Specdiff switches to using hashprint/text diff when it estimates that a lot of
26
+ the keys changed names, which is a point of weakness for hashdiff's output.
21
27
 
22
- It will also produce a json [hashdiff](https://github.com/liufengyun/hashdiff),
23
- even if the request did not have the content type header:
24
-
25
- ![d](./assets/webmock_json_with_specdiff.png)
28
+ ## Installation
26
29
 
27
- (The output of the json diff is experimental, feedback would be great!)
30
+ ### Requirements
28
31
 
29
- You might also check out the `examples/` directory to play with it:
32
+ Specdiff is tested to work with:
33
+ - Ruby `>= 3.0`.
34
+ - RSpec `3.13.0`, `3.12.2`, `3.12.0`.
35
+ - WebMock `3.19.1`, `3.18.1`.
30
36
 
31
- `$ cd examples/webmock && bundle install`
32
-
33
- ## Installation
37
+ ### Use bundler
34
38
 
35
39
  Add this line to your application's Gemfile:
36
40
 
@@ -40,123 +44,159 @@ gem "specdiff", require: false
40
44
 
41
45
  And then execute:
42
46
 
43
- $ bundle install
44
-
45
- Or install it yourself as:
47
+ `$ bundle install`
46
48
 
47
- $ gem install specdiff
49
+ ### Loading the gem and its integrations
48
50
 
49
- ## Usage
50
-
51
- Put the following in your `spec_helper.rb`. (Or equivalent initializer
52
- for test environment) You probably don't want to load/use this gem in a release
53
- environment.
51
+ Put the following in the initializer for your test environment (`spec_helper.rb`):
54
52
 
55
53
  ```rb
56
- # spec_helper.rb
57
-
58
- require "specdiff"
59
- require "specdiff/webmock" # optional, webmock patches
54
+ require "specdiff" # may be unneccessary if you're using bundler to load your gems
55
+ require "specdiff/rspec" # optional, enables RSpec integration
56
+ require "specdiff/webmock" # optional, enables WebMock integration
57
+ Specdiff.load!(:json) # optional, automatically detects json and uses the hash differ on it
60
58
 
61
- # optionally, you can turn off terminal colors
62
59
  Specdiff.configure do |config|
63
- config.colorize = true
60
+ config.colorize = true # toggles color output
64
61
  end
65
62
  ```
66
63
 
64
+ The monkey-patches should go after the gems they are patching.
65
+
66
+ *This gem is intended for use in test environments, not production or release environments.*
67
+
68
+ ## Usage
69
+
70
+ Specdiff has two integrations: [WebMock](https://rubygems.org/gems/webmock) and
71
+ [RSpec](https://rubygems.org/gems/rspec). These are the main way specdiff is
72
+ intended to be used.
73
+
74
+ ### RSpec
75
+
76
+ The RSpec integration improves on RSpec's built in diffing functionality by
77
+ diffing nested hash/array structures with
78
+ [hashdiff](https://rubygems.org/gems/hashdiff). This produces clearer
79
+ output when deeply nested structures are involved:
80
+
81
+ | ![img](assets/source_hashdiff_hash.png) | ![img](assets/rspecs_hashdiff_hash.png) | ![img](assets/specdiffs_hashdiff_hash.png) |
82
+ | --- | --- | --- |
83
+ | Test source | RSpec's diff | Specdiff's diff |
84
+
85
+ In some cases specdiff may produce a text-based diff of deeply nested hashes or
86
+ arrays instead. This also represents an improvement over RSpec's text-based diff
87
+ of nested hashes/arrays, by virtue of custom pretty-printing code designed for
88
+ the text differ to work on:
89
+
90
+ | ![img](assets/source_text_hash.png) | ![img](assets/rspecs_text_hash.png) | ![img](assets/specdiffs_text_hash.png) |
91
+ | --- | --- | --- |
92
+ | Test source | RSpec's diff | Specdiff's diff |
93
+
94
+ The RSpec integration also prevents RSpec from truncating your data before
95
+ printing it (by replacing the inspect implementation), avoiding the necessity
96
+ of a diff in some instances:
97
+
98
+ | ![img](assets/rspec_truncating.png) | ![img](assets/specdiff_no_truncating.png) |
99
+ | --- | --- |
100
+ | RSpec truncating your data | Specdiff preventing truncation |
101
+
102
+ (Although this is an instance where a "word diff" would be more helpful)
103
+
67
104
  ### WebMock
68
105
 
69
- The webmock patch should make webmock show request body diffs by using the
70
- specdiff gem when stubs mismatch. It only applies to request bodies.
106
+ WebMock already ships with a dependency on
107
+ [hashdiff](https://rubygems.org/gems/hashdiff), providing diffs of json request
108
+ stubs. Specdiff prints hashdiff's output differently, and does not require the
109
+ content type to be specified:
71
110
 
72
- ### Direct usage
111
+ (This requires you to enable the json plugin)
73
112
 
74
- You can also use the gem directly:
113
+ | ![img](assets/webmock_json_diff_source.png) | ![img](assets/webmock_json_diff.png) | ![img](assets/webmock_json_diff_specdiff.png) |
114
+ | --- | --- | --- |
115
+ | Source | WebMock | Specdiff |
75
116
 
76
- ```rb
77
- diff = Specdiff.diff(something, and_something_else)
117
+ With specdiff enabled you also get text diffs where previously there were none:
78
118
 
79
- diff.empty? # => true/false, if it is empty you might want to not print the diff, it is probably useless
80
- diff.to_s # => a string for showing to a developer who may or may not be scratching their head
81
- ```
119
+ | ![img](assets/webmock_newlines_diff_source.png) | ![img](assets/webmock_newlines_diff.png) | ![img](assets/webmock_newlines_diff_specdiff.png) |
120
+ | --- | --- | --- |
121
+ | Source | WebMock | Specdiff |
82
122
 
83
- ### Registering plugins
123
+ The WebMock integration continues to respect the `show_body_diff` setting in
124
+ WebMock:
84
125
 
85
126
  ```rb
86
- # Specdiff comes with json support, but it must be loaded like so:
87
- Specdiff.load!(:json)
88
-
89
- # Custom plugins can be loaded like this:
90
- Specdiff.load!(MyCustomType)
127
+ # this will cause diffs to not be produced, regardless of whether specdiff is
128
+ # loaded. body diffs are enabled by default in WebMock so you don't need to
129
+ # touch this.
130
+ WebMock.hide_body_diff!
91
131
  ```
92
132
 
93
- [Check out the source code](./lib/specdiff/plugins/json.rb) to learn the plugin interface.
133
+ ### Direct usage
94
134
 
95
- ## Development
135
+ It is also possible to call into specdiff with arbitrary data.
136
+ This is essentially what the integrations do for you.
137
+ This is suitable if you are developing a library and intend to depend on
138
+ specdiff for diff output.
96
139
 
97
- Check out the [glossary](./glossary.txt) to make sure you (and I) are using the
98
- same words for things ;)
140
+ ```rb
141
+ # Generate a diff, using all plugins available.
142
+ diff = Specdiff.diff(something, anything)
99
143
 
100
- Install the software versions specified in `.tool-versions`.
144
+ diff.empty? # => true/false, if true you probably don't want to actually show the diff
145
+ diff.to_s # => a String for showing to a developer who may or may not be scratching their head
101
146
 
102
- Run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests and linter and make sure they're green before starting to make your changes.
147
+ # Generate an indented, pretty-printed string representation of a ruby hash.
148
+ printed_hash = Specdiff.hashprint(my_big_nested_hash)
149
+ printed_hash # => String
150
+
151
+ # Inspect something, but with prettier output for certain classes
152
+ # (Time/DateTime/BigDecimal). This is not indented. Usually defers to #inspect.
153
+ specdiff_inspected = Specdiff.diff_inspect(something)
154
+ specdiff_inspected # => String
155
+ ```
103
156
 
104
- Run `bundle exec rake -AD` for a full list of all the available tasks you may use for development purposes.
157
+ Any methods or properties not documented here are considered private
158
+ implementation details.
105
159
 
106
- You can also run `bin/console` for an interactive prompt that will allow you to experiment with the gem code loaded.
160
+ ## Plugins
107
161
 
108
- Remember to update the unreleased section of the [changelog](./CHANGELOG.md) before you submit your pull request.
162
+ It is possible to create and load plugins into specdiff to customize Specdiff's
163
+ behaviour. This was implemented in case you have special needs (e.g. diffing
164
+ things other than text, hashes, arrays and json).
109
165
 
110
- ## How it works/"Architecture"
166
+ ### JSON
111
167
 
112
- High level description of the heuristic specdiff implements
168
+ The built-in json plugin is optional, and loaded like so:
113
169
 
114
- 1. receive 2 pieces of data: `a` and `b`
115
- 2. determine types for `a` and `b`
116
- 1. test against plugin types
117
- 2. test against built in types
118
- 3. fall back to the `:unknown` type
119
- 3. determine which differ is appropriate for the types
120
- 1. test against plugin differs
121
- 2. test against built in differs
122
- 3. fall back to the null differ (`NotFound`)
123
- 7. run the selected differ with a and b
124
- 8. package it into a `::Specdiff::Diff` which records the detected types
170
+ ```rb
171
+ Specdiff.load!(:json)
172
+ ```
125
173
 
126
- \<time passes>
174
+ The JSON plugin attempts to parse strings into json using `JSON.parse`. If
175
+ successful, it considers that string a json, and will use the parse result as if
176
+ it had been passed directly to specdiff in the first place.
127
177
 
128
- 6. at some point later when `#to_s` is invoked, stringify the diff using the differ's `#stringify`
178
+ ### Defining your own plugins
129
179
 
130
- ## Releasing
180
+ While other plugins are loaded by simply passing them to `::load!`:
131
181
 
132
- ### Release procedure
182
+ ```rb
183
+ class MySpecdiffPlugin
184
+ # Read the source code to figure out how plugins work. Sorry.
185
+ end
133
186
 
134
- - [ ] unit tests are passing (`$ bundle exec rake test`)
135
- - [ ] linter is happy (`$ bundle exec rake lint`)
136
- - [ ] `examples/` look good
137
- - [ ] update the version number in `version.rb`
138
- - [ ] make sure the `examples/` `Gemfile.lock` files are updated (run bundle install)
139
- - [ ] make sure `Gemfile.lock` is updated (run bundle install)
140
- - [ ] move unreleased changes to the next version in the [changelog](./CHANGELOG.md)
141
- - [ ] commit in the form "vX.X.X" and push
142
- - [ ] make sure the pipeline is green
143
- - [ ] `$ bundle exec rake release`
187
+ Specdiff.load!(MySpecdiffPlugin)
188
+ ```
144
189
 
145
- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
190
+ This was intended to allow implementing (potentially janky and inefficient)
191
+ plugins to help diff XML, but in practice the text differ was good enough for
192
+ diffing indented XML.
146
193
 
147
194
  ## Contributing
148
195
 
196
+ Read [DEVELOPMENT.md](./DEVELOPMENT.md) for current development practices.
197
+
149
198
  Bug reports and pull requests are welcome on GitHub at https://github.com/odinhb/specdiff.
150
199
 
151
200
  ## License
152
201
 
153
202
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
154
-
155
- ## TODO
156
-
157
- This documents potential improvements/issues I know about or have thought about.
158
-
159
- - [ ] test the webmock monkey-patch. currently there is an empty webmock_spec.rb (should we do this using rspec?) and the examples/ directory contains a few webmock examples (which are a good idea to run before releasing) but it would be nice to have the pipeline fail if it doesn't work for whatever reason
160
- - [ ] finalize plugin interface (are the methods named intuitively? should we split type detector definitions and differ definitions?)
161
- - [ ] document how to define a plugin properly (instead of just linking to the source code)
162
- - [ ] is the stringification of hashdiff's output really better than pretty print? or just more wordy? (the colors are definitely nice)
data/glossary.txt CHANGED
@@ -44,3 +44,7 @@ side
44
44
  compare
45
45
  the procedure that implements the main function of specdiff including
46
46
  accounting for any plugin types and differs
47
+
48
+ integration
49
+ refers to the provided monkey-patches and conscious relationship between
50
+ specdiff and another gem or piece of software it was intended to work with.
@@ -80,9 +80,9 @@ private
80
80
  elsif a.type == :text && b.type == :text
81
81
  Specdiff::Differ::Text
82
82
  elsif a.type == :hash && b.type == :hash
83
- Specdiff::Differ::Hashdiff
83
+ Specdiff::Differ::Hash
84
84
  elsif a.type == :array && b.type == :array
85
- Specdiff::Differ::Hashdiff
85
+ Specdiff::Differ::Hash
86
86
  else
87
87
  Specdiff::Differ::NotFound
88
88
  end
data/lib/specdiff/diff.rb CHANGED
@@ -19,12 +19,15 @@
19
19
  end
20
20
 
21
21
  def inspect
22
- if empty?
23
- "<Specdiff::Diff (empty)>"
24
- elsif raw.respond_to?(:bytesize)
25
- "<Specdiff::Diff w/ #{raw&.bytesize || 0} bytes of #raw diff>"
26
- else
27
- "<Specdiff::Diff #{raw.inspect}>"
28
- end
22
+ raw_diff = if empty?
23
+ "empty"
24
+ elsif differ == ::Specdiff::Differ::Text
25
+ bytes = raw&.bytesize || 0
26
+ "#{bytes} bytes of #raw diff"
27
+ else
28
+ "#{raw.inspect}"
29
+ end
30
+
31
+ "<Specdiff::Diff (#{a.type}/#{b.type}) (#{differ}) (#{raw_diff})>"
29
32
  end
30
33
  end
@@ -1,19 +1,27 @@
1
1
  require "hashdiff"
2
2
 
3
- class Specdiff::Differ::Hashdiff
3
+ class Specdiff::Differ::Hash
4
4
  extend ::Specdiff::Colorize
5
5
 
6
- VALUE_CHANGE_PERCENTAGE_THRESHOLD = 0.2
7
- TOTAL_CHANGES_FOR_GROUPING_THRESHOLD = 9
6
+ # The percentage of changes that must (potentially) be a key rename in a hash
7
+ # for text diffing to kick in. Expressed as a fraction of 1.
8
+ KEY_CHANGE_PERCENTAGE_THRESHOLD = 0.8
8
9
 
9
- NEWLINE = "\n"
10
+ # The number of changes that must be detected by hashdiff before we print some
11
+ # extra newlines to better group extra/missing/new_values visually.
12
+ TOTAL_CHANGES_FOR_GROUPING_THRESHOLD = 9
10
13
 
11
14
  def self.diff(a, b)
12
15
  # array_path: true returns the path as an array, which differentiates
13
16
  # between symbol keys and string keys in hashes, while the string
14
17
  # representation does not.
18
+
15
19
  # hmm it really seems like use_lcs: true gives much less human-readable
16
20
  # (human-comprehensible) output when arrays are involved.
21
+
22
+ # use_lcs: true may also cause Hashdiff to use a lot of memory when BIG
23
+ # arrays are involved: https://github.com/liufengyun/hashdiff/issues/49
24
+ # so we might as well avoid that problem altogether.
17
25
  hashdiff_diff = ::Hashdiff.diff(
18
26
  a.value, b.value,
19
27
  array_path: true,
@@ -24,54 +32,60 @@ class Specdiff::Differ::Hashdiff
24
32
 
25
33
  change_percentage = _calculate_change_percentage(hashdiff_diff)
26
34
 
27
- if change_percentage >= VALUE_CHANGE_PERCENTAGE_THRESHOLD
35
+ if change_percentage <= KEY_CHANGE_PERCENTAGE_THRESHOLD
28
36
  hashdiff_diff
29
37
  else
30
38
  a_text = ::Specdiff.hashprint(a.value)
31
39
  b_text = ::Specdiff.hashprint(b.value)
32
40
 
33
- diff = ::Specdiff.diff(a_text, b_text)
41
+ text_diff = ::Specdiff.diff(a_text, b_text)
34
42
 
35
- if diff.empty?
43
+ if text_diff.empty?
36
44
  []
37
45
  else
38
- diff.a.type = a.type
39
- diff.b.type = b.type
40
-
41
- diff
46
+ text_diff
42
47
  end
43
48
  end
44
49
  end
45
50
 
46
51
  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] == "-" }
52
+ extra_keys = hashdiff_diff.count { |element| element[0] == "+" }
53
+ missing_keys = hashdiff_diff.count { |element| element[0] == "-" }
54
+ new_values = hashdiff_diff.count { |element| element[0] == "~" }
50
55
  # 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
56
+ # puts "extra_keys: #{extra_keys.inspect}"
57
+ # puts "missing_keys: #{missing_keys.inspect}"
58
+ # puts "new_values: #{new_values.inspect}"
59
+
60
+ potential_changed_keys = [extra_keys, missing_keys].min
61
+ adjusted_extra_keys = extra_keys - potential_changed_keys
62
+ adjusted_missing_keys = missing_keys - potential_changed_keys
63
+ # puts "potential_changed_keys: #{potential_changed_keys.inspect}"
64
+ # puts "adjusted_extra_keys: #{adjusted_extra_keys.inspect}"
65
+ # puts "adjusted_missing_keys: #{adjusted_missing_keys.inspect}"
66
+
67
+ non_changed_keys = adjusted_extra_keys + adjusted_missing_keys + new_values
68
+ total_changes = non_changed_keys + potential_changed_keys
69
+ # puts "non_changed_keys: #{non_changed_keys.inspect}"
70
+ # puts "total_changes: #{total_changes.inspect}"
71
+
72
+ key_change_fraction = Rational(potential_changed_keys, total_changes)
73
+ key_change_percentage = key_change_fraction.to_f
74
+ # puts "key_change_fraction: #{key_change_fraction.inspect}"
75
+ # puts "key_change_percentage: #{key_change_percentage.inspect}"
76
+
77
+ key_change_percentage
67
78
  end
68
79
 
69
80
  def self.empty?(diff)
70
81
  diff.raw.empty?
71
82
  end
72
83
 
84
+ NEWLINE = "\n"
85
+
73
86
  def self.stringify(diff)
74
87
  result = +""
88
+ return result if diff.empty?
75
89
 
76
90
  total_changes = diff.raw.size
77
91
  group_with_newlines = total_changes >= TOTAL_CHANGES_FOR_GROUPING_THRESHOLD
@@ -92,6 +106,9 @@ class Specdiff::Differ::Hashdiff
92
106
  additions = changes_grouped_by_type["+"] || []
93
107
  value_changes = changes_grouped_by_type["~"] || []
94
108
 
109
+ result << "@@ +#{additions.size}/-#{deletions.size}/~#{value_changes.size} @@"
110
+ result << NEWLINE
111
+
95
112
  deletions.each do |change|
96
113
  value = change[2]
97
114
  path = _stringify_path(change[1])
@@ -108,7 +125,7 @@ class Specdiff::Differ::Hashdiff
108
125
  value = change[2]
109
126
  path = _stringify_path(change[1])
110
127
 
111
- result << " new key: #{path} (#{::Specdiff.diff_inspect(value)})"
128
+ result << " extra key: #{path} (#{::Specdiff.diff_inspect(value)})"
112
129
  result << NEWLINE
113
130
  end
114
131
 
@@ -123,17 +140,19 @@ class Specdiff::Differ::Hashdiff
123
140
 
124
141
  from_inspected = ::Specdiff.diff_inspect(from)
125
142
  to_inspected = ::Specdiff.diff_inspect(to)
126
- result << "changed key: #{path} (#{from_inspected} -> #{to_inspected})"
143
+ result << " new value: #{path} (#{from_inspected} -> #{to_inspected})"
127
144
  result << NEWLINE
128
145
  end
129
146
 
130
147
  colorize_by_line(result) do |line|
131
148
  if line.start_with?("missing key:")
132
149
  red(line)
133
- elsif line.start_with?(/\s+new key:/)
150
+ elsif line.start_with?(" extra key:")
134
151
  green(line)
135
- elsif line.start_with?("changed key:")
152
+ elsif line.start_with?(" new value:")
136
153
  yellow(line)
154
+ elsif line.start_with?("@@")
155
+ cyan(line)
137
156
  else
138
157
  reset_color(line)
139
158
  end
@@ -4,4 +4,4 @@ end
4
4
  # require only the builtin differs, plugins are optionally loaded later
5
5
  require_relative "differ/not_found"
6
6
  require_relative "differ/text"
7
- require_relative "differ/hashdiff"
7
+ require_relative "differ/hash"