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.
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"