rspec-enriched_json 0.6.2 → 0.8.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: d601f3b9829a2dd3bc4af4a42e65b01819ec6152156552f25b9e188ad47d828d
4
- data.tar.gz: 1a08c66c4308c8f7e33ab3769c7bc1bc0601ace0588c242690d7edb2d02175d0
3
+ metadata.gz: 840927ecfa09fc825a0f9656e11b7e10c4f4f5b368c144e5536dfa0595714192
4
+ data.tar.gz: 5616ee6697817e0321938e53ae71410c110d9dc13bbd697dd0f2e974325efa7e
5
5
  SHA512:
6
- metadata.gz: 86d612e624b12e6da7e6a32d877479a548ee301d79e5c231a6741843c83f07f635c11415e9945d84c68b076dda67b96712a5786a7d3cd4967923d4526be2f6d8
7
- data.tar.gz: 5478f0f570d46b58139d82f16f1e305290370ca41ee99045ff382dfa4e77644afab8dd6fd408e98c495d2c23f14fc7c227120e87d1ab08da533dcb77df4e225f
6
+ metadata.gz: ea0dcb189eafca59326cab5550444fe262f4475575b0860381c203dc3c16886fb7c4244df7123bdd854c398ad0b47b4fdaf0d523425fcba843845ea1f72f3eb0
7
+ data.tar.gz: d8c279936a9641ea12377e4952c747bc9da60abaa15542bd973143180c1249bbcb8504e161411cb78a5cb07cd406c96612ce2ed24a00bfd695c268e24c5ea799
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # RSpec::EnrichedJson
2
2
 
3
+ [![CI](https://github.com/firstdraft/rspec-enriched_json/actions/workflows/ci.yml/badge.svg)](https://github.com/firstdraft/rspec-enriched_json/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/rspec-enriched_json.svg)](https://badge.fury.io/rb/rspec-enriched_json)
5
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/standardrb/standard)
6
+
3
7
  A drop-in replacement for RSpec's built-in JSON formatter that enriches the output with structured failure data. This makes it easy to programmatically analyze test results, extract expected/actual values, and build better CI/CD integrations.
4
8
 
5
9
  ## Quick Demo
@@ -30,7 +34,7 @@ This interactive demo script runs the same failing tests with both formatters an
30
34
  Add this line to your application's Gemfile:
31
35
 
32
36
  ```ruby
33
- gem 'rspec-enriched_json'
37
+ gem "rspec-enriched_json"
34
38
  ```
35
39
 
36
40
  And then execute:
@@ -95,14 +99,14 @@ With this gem, you get structured data alongside the original message:
95
99
 
96
100
  ## Features
97
101
 
98
- - **Drop-in replacement**: Inherits from RSpec's JsonFormatter, maintaining 100% compatibility
99
- - **Structured data extraction**: Expected and actual values as proper JSON objects
100
- - **Rich object support**: Arrays, hashes, and custom objects are properly serialized
101
- - **Original message preservation**: When you override with a custom message, the original is preserved
102
- - **Graceful degradation**: Regular exceptions (non-expectation failures) work normally
103
- - **Enhanced metadata capture**: Test location, tags, hierarchy, and custom metadata
104
- - **Robust error recovery**: Handles objects that fail to serialize without crashing
105
- - **Diff information**: Includes `diffable` to help tools determine if values can be meaningfully diffed
102
+ - **Drop-in replacement**: Inherits from RSpec's JsonFormatter, maintaining 100% compatibility.
103
+ - **Structured data extraction**: Expected and actual values as proper JSON objects.
104
+ - **Rich object support**: Arrays, hashes, and custom objects are properly serialized.
105
+ - **Original message preservation**: When you override with a custom message, the original is preserved.
106
+ - **Graceful degradation**: Regular exceptions (non-expectation failures) work normally.
107
+ - **Enhanced metadata capture**: Test location, tags, hierarchy, and custom metadata.
108
+ - **Robust error recovery**: Handles objects that fail to serialize without crashing.
109
+ - **Diff information**: Includes `diffable` to help tools determine if values can be meaningfully diffed.
106
110
 
107
111
  ## Examples
108
112
 
@@ -147,36 +151,50 @@ end
147
151
 
148
152
  ## Use Cases
149
153
 
150
- - **CI/CD Integration**: Parse test results to create rich error reports
151
- - **Test Analytics**: Track which values commonly cause test failures
152
- - **Debugging Tools**: Build tools that can display expected vs actual diffs
153
- - **Learning Platforms**: Provide detailed feedback on why tests failed
154
+ - **CI/CD Integration**: Parse test results to create rich error reports.
155
+ - **Test Analytics**: Track which values commonly cause test failures.
156
+ - **Debugging Tools**: Build tools that can display expected vs actual diffs.
157
+ - **Learning Platforms**: Provide detailed feedback on why tests failed.
154
158
 
155
159
  ## How It Works
156
160
 
157
161
  The gem works by:
158
162
 
159
- 1. Patching RSpec's expectation system to capture structured data when expectations fail
160
- 2. Extending the JsonFormatter to include this data in the JSON output
161
- 3. Maintaining full backward compatibility with existing tools
163
+ 1. Patching RSpec's expectation system to capture structured data when expectations fail.
164
+ 2. Extending the JsonFormatter to include this data in the JSON output.
165
+ 3. Maintaining full backward compatibility with existing tools.
162
166
 
163
167
  ## Development
164
168
 
165
169
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
166
170
 
167
- ## Performance Considerations
171
+ This project uses [StandardRB](https://github.com/standardrb/standard) for code formatting and style. Before committing:
172
+
173
+ ```bash
174
+ # Check for style violations
175
+ bundle exec standardrb
176
+
177
+ # Auto-fix style violations
178
+ bundle exec standardrb --fix
179
+ ```
180
+
181
+ ## Additional Features
182
+
183
+ ### Passing Test Value Capture
184
+ The formatter also captures expected/actual values for passing tests, useful for:
185
+ - Test analytics and insights.
186
+ - Understanding test coverage patterns.
187
+ - Debugging flaky tests.
168
188
 
169
- The enriched formatter adds minimal overhead:
170
- - Only processes failing tests (passing tests have no extra processing)
171
- - Limits serialization depth to prevent infinite recursion
172
- - Truncates large strings and collections to maintain reasonable output sizes
173
- - No impact on test execution time, only on failure reporting
189
+ ### Negation Detection
190
+ Tests using `not_to` or `to_not` include a `negated: true` flag in the details.
174
191
 
175
- Default limits:
176
- - Max serialization depth: 5 levels
177
- - Max array size: 100 items
178
- - Max hash size: 100 keys
179
- - Max string length: 1000 characters
192
+ ### Serialization
193
+ Values are serialized using [Oj](https://github.com/ohler55/oj) in object mode, providing:
194
+ - Circular reference handling.
195
+ - Proper Ruby object serialization.
196
+ - Excellent performance.
197
+ - Special handling for Regexp objects (serialized as inspect strings).
180
198
 
181
199
  ## Contributing
182
200
 
@@ -1,275 +1,201 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "oj"
4
5
  require "rspec/expectations"
5
6
  require "rspec/support/differ"
6
7
 
7
8
  module RSpec
8
9
  module EnrichedJson
9
- # Universal wrapper to catch ALL matchers and attach structured data
10
+ @all_test_values = {}
11
+
12
+ def self.all_test_values
13
+ @all_test_values
14
+ end
15
+
16
+ def self.clear_test_values
17
+ @all_test_values = {}
18
+ end
19
+
10
20
  module ExpectationHelperWrapper
11
- MAX_SERIALIZATION_DEPTH = 5
12
- MAX_ARRAY_SIZE = 100
13
- MAX_HASH_SIZE = 100
14
- MAX_STRING_LENGTH = 1000
15
21
  def self.install!
16
22
  RSpec::Expectations::ExpectationHelper.singleton_class.prepend(self)
23
+ RSpec::Expectations::PositiveExpectationHandler.singleton_class.prepend(PositiveHandlerWrapper)
24
+ RSpec::Expectations::NegativeExpectationHandler.singleton_class.prepend(NegativeHandlerWrapper)
17
25
  end
18
26
 
19
- # Make serialize_value accessible for other components
20
27
  module Serializer
21
28
  extend self
22
29
 
23
- MAX_SERIALIZATION_DEPTH = 5
24
- MAX_ARRAY_SIZE = 100
25
- MAX_HASH_SIZE = 100
26
- MAX_STRING_LENGTH = 1000
27
-
28
- def serialize_value(value, depth = 0)
29
- return "[Max depth exceeded]" if depth > MAX_SERIALIZATION_DEPTH
30
-
31
- case value
32
- when Numeric, TrueClass, FalseClass
33
- value
34
- when String
35
- unescape_string_double_quotes(
36
- truncate_string(value)
37
- )
38
- when Symbol
39
- serialize_object(value)
40
- when nil
41
- serialize_object(value)
42
- when Array
43
- return "[Large array: #{value.size} items]" if value.size > MAX_ARRAY_SIZE
44
- value.map { |v| serialize_value(v, depth + 1) }
45
- when Hash
46
- return "[Large hash: #{value.size} keys]" if value.size > MAX_HASH_SIZE
47
- value.transform_values { |v| serialize_value(v, depth + 1) }
48
- else
49
- serialize_object(value, depth)
50
- end
51
- rescue => e
52
- {
53
- "class" => value.class.name,
54
- "serialization_error" => e.message
55
- }
56
- end
57
-
58
- def serialize_object(obj, depth = 0)
59
- result = {
60
- "class" => obj.class.name,
61
- "inspect" => safe_inspect(obj),
62
- "to_s" => safe_to_s(obj)
63
- }
64
-
65
- # Handle Structs specially
66
- if obj.is_a?(Struct)
67
- result["struct_values"] = obj.to_h.transform_values { |v| serialize_value(v, depth + 1) }
68
- end
69
-
70
- # Include instance variables only for small objects
71
- ivars = obj.instance_variables
72
- if ivars.any? && ivars.length <= 10
73
- result["instance_variables"] = ivars.each_with_object({}) do |ivar, hash|
74
- hash[ivar.to_s] = serialize_value(obj.instance_variable_get(ivar), depth + 1)
75
- end
76
- end
77
-
78
- result
79
- end
80
-
81
- def truncate_string(str)
82
- return str if str.length <= MAX_STRING_LENGTH
83
- "#{str[0...MAX_STRING_LENGTH]}... (truncated)"
84
- end
30
+ OJ_OPTIONS = {
31
+ mode: :object,
32
+ circular: true,
33
+ class_cache: false,
34
+ create_additions: false,
35
+ symbol_keys: false,
36
+ auto_define: false,
37
+ create_id: nil,
38
+ use_to_json: false,
39
+ use_as_json: false,
40
+ use_raw_json: false,
41
+ bigdecimal_as_decimal: true,
42
+ nan: :word
43
+ }
85
44
 
86
- def unescape_string_double_quotes(str)
87
- if str.start_with?('"') && str.end_with?('"')
88
- begin
89
- # Only undump if it's a valid dumped string
90
- # Check if the string is properly escaped by attempting undump
91
- str.undump
92
- rescue RuntimeError => e
93
- # If undump fails, just return the original string
94
- # This handles cases where the string has quotes but isn't a valid dump format
95
- str
96
- end
97
- else
98
- str
45
+ def serialize_value(value)
46
+ if value.is_a?(Regexp)
47
+ return Oj.dump(value.inspect, mode: :compat)
99
48
  end
100
- end
101
49
 
102
- def safe_inspect(obj)
103
- truncate_string(obj.inspect)
50
+ Oj.dump(value, OJ_OPTIONS)
104
51
  rescue => e
105
- "[inspect failed: #{e.class}]"
106
- end
107
-
108
- def safe_to_s(obj)
109
- truncate_string(obj.to_s)
110
- rescue => e
111
- "[to_s failed: #{e.class}]"
52
+ Oj.dump({
53
+ "_serialization_error" => e.message,
54
+ "_class" => value.class.name,
55
+ "_to_s" => begin
56
+ value.to_s
57
+ rescue
58
+ "[to_s failed]"
59
+ end
60
+ }, mode: :compat)
112
61
  end
113
62
  end
114
63
 
115
64
  def handle_failure(matcher, message, failure_message_method)
116
- # If a custom message is provided, capture the original message first
117
65
  original_message = nil
118
66
  if message
119
67
  original_message = matcher.send(failure_message_method)
120
68
  end
121
69
 
122
- # Call original handler with the original message
123
70
  super
124
71
  rescue RSpec::Expectations::ExpectationNotMetError => e
125
- # Extract raw values for diff analysis
126
- expected_raw = extract_value(matcher, :expected)
127
- actual_raw = extract_value(matcher, :actual)
72
+ expected_raw = extract_value(matcher, :expected, failure_message_method)
73
+ actual_raw = extract_value(matcher, :actual, failure_message_method)
74
+ negated = failure_message_method == :failure_message_when_negated
128
75
 
129
- # Collect structured data
130
76
  details = {
131
77
  expected: Serializer.serialize_value(expected_raw),
132
78
  actual: Serializer.serialize_value(actual_raw),
133
- original_message: original_message, # Only populated when custom message overrides it
79
+ original_message: original_message,
134
80
  matcher_name: matcher.class.name,
135
- diffable: values_diffable?(expected_raw, actual_raw, matcher)
81
+ diffable: matcher.respond_to?(:diffable?) && matcher.diffable?,
82
+ negated: negated
136
83
  }
137
84
 
138
- # Generate diff if values are diffable
139
85
  if details[:diffable] && expected_raw && actual_raw
140
86
  diff = generate_diff(actual_raw, expected_raw)
141
87
  details[:diff] = diff unless diff.nil? || diff.strip.empty?
142
88
  end
143
89
 
144
- # Capture matcher-specific instance variables
145
- matcher_data = extract_matcher_specific_data(matcher)
146
- details.merge!(matcher_data) unless matcher_data.empty?
147
-
148
- # Raise new exception with data attached
149
90
  raise EnrichedExpectationNotMetError.new(e.message, details)
150
91
  end
151
92
 
152
93
  private
153
94
 
154
- def extract_value(matcher, method_name)
155
- return nil unless matcher.respond_to?(method_name)
156
-
157
- value = matcher.send(method_name)
158
- # Don't return nil if the value itself is nil
159
- # Only return nil if the value is the matcher itself (self-referential)
160
- (value == matcher && !value.nil?) ? nil : value
95
+ def extract_value(matcher, method_name, failure_message_method = nil)
96
+ if matcher.is_a?(RSpec::Matchers::BuiltIn::BePredicate) || matcher.is_a?(RSpec::Matchers::BuiltIn::Has)
97
+ case method_name
98
+ when :expected
99
+ !(failure_message_method == :failure_message_when_negated)
100
+ when :actual
101
+ if matcher.instance_variable_defined?(:@predicate_result)
102
+ matcher.instance_variable_get(:@predicate_result)
103
+ end
104
+ end
105
+ else
106
+ return nil unless matcher.respond_to?(method_name)
107
+ value = matcher.send(method_name)
108
+ (value == matcher && !value.nil?) ? nil : value
109
+ end
161
110
  rescue
162
111
  nil
163
112
  end
164
113
 
165
- def extract_matcher_specific_data(matcher)
166
- # Skip common instance variables that are already captured
167
- skip_vars = [
168
- :@expected, :@actual, :@args, :@name,
169
- # Skip internal implementation details
170
- :@matcher, :@matchers, :@target,
171
- :@delegator, :@base_matcher,
172
- :@block, :@event_proc,
173
- # Skip verbose internal state
174
- :@pairings_maximizer, :@best_solution,
175
- :@expected_captures, :@match_captures,
176
- :@failures, :@errors,
177
- # Skip RSpec internals
178
- :@matcher_execution_context,
179
- :@chained_method_with_args_combos
180
- ]
114
+ def generate_diff(actual, expected)
115
+ differ = RSpec::Support::Differ.new(
116
+ object_preparer: lambda { |obj|
117
+ RSpec::Matchers::Composable.surface_descriptions_in(obj)
118
+ },
119
+ color: RSpec.configuration.color_enabled?
120
+ )
121
+ differ.diff(actual, expected)
122
+ rescue
123
+ nil
124
+ end
125
+ end
181
126
 
182
- # Define meaningful variables we want to keep
183
- useful_vars = [
184
- :@missing_items, :@extra_items, # ContainExactly
185
- :@expecteds, :@actuals, # Include
186
- :@operator, :@delta, :@tolerance, # Comparison matchers
187
- :@expected_before, :@expected_after, :@actual_before, :@actual_after, # Change matcher
188
- :@from, :@to, :@minimum, :@maximum, :@count, # Various matchers
189
- :@failure_message, :@failure_message_when_negated,
190
- :@description
191
- ]
127
+ module HandlerWrapperShared
128
+ def capture_test_values(actual, initial_matcher, negated: false)
129
+ return unless initial_matcher && RSpec.current_example
192
130
 
193
- # Get all instance variables
194
- ivars = matcher.instance_variables - skip_vars
195
- return {} if ivars.empty?
131
+ begin
132
+ if initial_matcher.is_a?(RSpec::Matchers::BuiltIn::BePredicate) || initial_matcher.is_a?(RSpec::Matchers::BuiltIn::Has)
133
+ expected_value = !negated
196
134
 
197
- # Build a hash of matcher-specific data
198
- matcher_data = {}
135
+ if negated && initial_matcher.respond_to?(:does_not_match?)
136
+ initial_matcher.does_not_match?(actual)
137
+ else
138
+ initial_matcher.matches?(actual)
139
+ end
199
140
 
200
- ivars.each do |ivar|
201
- # Only include if it's in our useful list or looks like user data
202
- unless useful_vars.include?(ivar) || ivar.to_s.match?(/^@(missing|extra|failed|unmatched|matched)_/)
203
- next
141
+ actual_value = if initial_matcher.instance_variable_defined?(:@predicate_result)
142
+ initial_matcher.instance_variable_get(:@predicate_result)
143
+ end
144
+ else
145
+ expected_value = initial_matcher.respond_to?(:expected) ? initial_matcher.expected : nil
146
+ actual_value = actual
204
147
  end
205
148
 
206
- value = matcher.instance_variable_get(ivar)
207
-
208
- # Skip if value is nil or the matcher itself
209
- next if value.nil? || value == matcher
149
+ key = RSpec.current_example.id
150
+ RSpec::EnrichedJson.all_test_values[key] = {
151
+ expected: ExpectationHelperWrapper::Serializer.serialize_value(expected_value),
152
+ actual: ExpectationHelperWrapper::Serializer.serialize_value(actual_value),
153
+ matcher_name: initial_matcher.class.name,
154
+ passed: nil
155
+ }
210
156
 
211
- # Skip procs and complex objects unless they're simple collections
212
- if value.is_a?(Proc) || (value.is_a?(Object) && !value.is_a?(Enumerable) && !value.is_a?(Numeric) && !value.is_a?(String) && !value.is_a?(Symbol))
213
- next
157
+ RSpec::EnrichedJson.all_test_values[key][:negated] = true if negated
158
+ rescue => e
159
+ if defined?(RSpec.configuration) && RSpec.configuration.reporter
160
+ RSpec.configuration.reporter.message("Warning: Error capturing test values: #{e.message}")
161
+ elsif ENV["DEBUG"]
162
+ puts "Error capturing test values: #{e.message}"
214
163
  end
215
-
216
- # Convert instance variable name to a more readable format
217
- # @missing_items -> missing_items
218
- key = ivar.to_s.delete_prefix("@").to_sym
219
-
220
- # Serialize the value
221
- matcher_data[key] = Serializer.serialize_value(value)
222
- rescue
223
- # Skip this instance variable if we can't serialize it
224
- next
225
164
  end
226
-
227
- matcher_data
228
165
  end
229
166
 
230
- def values_diffable?(expected, actual, matcher)
231
- # First check if the matcher itself declares diffability
232
- if matcher.respond_to?(:diffable?)
233
- return matcher.diffable?
234
- end
167
+ def mark_as_passed(initial_matcher)
168
+ return unless initial_matcher && RSpec.current_example
235
169
 
236
- # If either value is nil, not diffable
237
- return false if expected.nil? || actual.nil?
170
+ key = RSpec.current_example.id
171
+ if RSpec::EnrichedJson.all_test_values[key]
172
+ RSpec::EnrichedJson.all_test_values[key][:passed] = true
173
+ end
174
+ end
175
+ end
238
176
 
239
- # For different classes, generally not diffable
240
- return false unless actual.instance_of?(expected.class)
177
+ module PositiveHandlerWrapper
178
+ include HandlerWrapperShared
241
179
 
242
- # Check if both values are of the same basic diffable type
243
- case expected
244
- when String, Array, Hash
245
- # These types are inherently diffable when compared to same type
246
- true
247
- else
248
- # For other types, they're diffable if they respond to to_s
249
- # and their string representations would be meaningful
250
- expected.respond_to?(:to_s) && actual.respond_to?(:to_s)
251
- end
252
- rescue
253
- # If any error occurs during checking, assume not diffable
254
- false
180
+ def handle_matcher(actual, initial_matcher, custom_message = nil, &block)
181
+ capture_test_values(actual, initial_matcher, negated: false)
182
+ result = super
183
+ mark_as_passed(initial_matcher)
184
+ result
255
185
  end
186
+ end
256
187
 
257
- def generate_diff(actual, expected)
258
- # Use RSpec's own differ for consistency
259
- differ = RSpec::Support::Differ.new(
260
- object_preparer: lambda { |obj|
261
- RSpec::Matchers::Composable.surface_descriptions_in(obj)
262
- },
263
- color: false # Always disable color for JSON output
264
- )
265
- differ.diff(actual, expected)
266
- rescue
267
- # If diff generation fails, return nil rather than crashing
268
- nil
188
+ module NegativeHandlerWrapper
189
+ include HandlerWrapperShared
190
+
191
+ def handle_matcher(actual, initial_matcher, custom_message = nil, &block)
192
+ capture_test_values(actual, initial_matcher, negated: true)
193
+ result = super
194
+ mark_as_passed(initial_matcher)
195
+ result
269
196
  end
270
197
  end
271
198
  end
272
199
  end
273
200
 
274
- # Auto-install when this file is required
275
201
  RSpec::EnrichedJson::ExpectationHelperWrapper.install!
@@ -12,9 +12,7 @@ module RSpec
12
12
  def stop(group_notification)
13
13
  @output_hash[:examples] = group_notification.notifications.map do |notification|
14
14
  format_example(notification.example).tap do |hash|
15
- # Add enhanced metadata
16
15
  add_metadata(hash, notification.example)
17
-
18
16
  e = notification.example.exception
19
17
 
20
18
  if e
@@ -24,16 +22,14 @@ module RSpec
24
22
  backtrace: notification.formatted_backtrace
25
23
  }
26
24
 
27
- # Add structured data if available
28
25
  if e.is_a?(RSpec::EnrichedJson::EnrichedExpectationNotMetError) && e.details
29
- hash[:details] = safe_structured_data(e.details)
26
+ hash[:details] = e.details
30
27
  end
31
-
32
- if hash.key?(:details) && hash[:details].key?(:expected) && hash[:details].key?(:actual)
33
- exception_message = hash[:exception][:message]
34
- if exception_message.include?("\nDiff:")
35
- hash[:exception][:message] = exception_message.sub(/Diff:.*/m, "").strip
36
- end
28
+ else
29
+ key = notification.example.id
30
+ if RSpec::EnrichedJson.all_test_values.key?(key)
31
+ captured_values = RSpec::EnrichedJson.all_test_values[key]
32
+ hash[:details] = captured_values
37
33
  end
38
34
  end
39
35
  end
@@ -45,38 +41,25 @@ module RSpec
45
41
  def add_metadata(hash, example)
46
42
  metadata = example.metadata.dup
47
43
 
48
- # Extract custom tags (all symbols and specific keys)
49
44
  custom_tags = {}
50
45
  metadata.each do |key, value|
51
- # Include all symbol keys (like :focus, :slow, etc.)
52
46
  if key.is_a?(Symbol) && value == true
53
47
  custom_tags[key] = true
54
- # Include specific metadata that might be useful
55
48
  elsif [:type, :priority, :severity, :db, :js].include?(key)
56
49
  custom_tags[key] = value
57
50
  end
58
51
  end
59
52
 
60
- # Add enhanced metadata
61
53
  hash[:metadata] = {
62
- # Location information
63
54
  location: example.location,
64
55
  absolute_file_path: File.expand_path(example.metadata[:file_path]),
65
56
  rerun_file_path: example.location_rerun_argument,
66
-
67
- # Example hierarchy
68
57
  example_group: example.example_group.description,
69
58
  example_group_hierarchy: extract_group_hierarchy(example),
70
-
71
- # Described class if available
72
59
  described_class: metadata[:described_class]&.to_s,
73
-
74
- # Custom tags and metadata
75
60
  tags: custom_tags.empty? ? nil : custom_tags,
76
-
77
- # Shared example information if applicable
78
61
  shared_group_inclusion_backtrace: metadata[:shared_group_inclusion_backtrace]
79
- }.compact # Remove nil values
62
+ }.compact
80
63
  end
81
64
 
82
65
  def extract_group_hierarchy(example)
@@ -91,54 +74,9 @@ module RSpec
91
74
  hierarchy
92
75
  end
93
76
 
94
- def safe_structured_data(details)
95
- # Start with core fields
96
- result = {
97
- expected: safe_serialize(details[:expected]),
98
- actual: safe_serialize(details[:actual]),
99
- matcher_name: details[:matcher_name],
100
- original_message: details[:original_message],
101
- diffable: details[:diffable]
102
- }
103
-
104
- # Add any additional matcher-specific fields
105
- details.each do |key, value|
106
- next if [:expected, :actual, :matcher_name, :original_message, :diffable].include?(key)
107
- result[key] = safe_serialize(value)
108
- end
109
-
110
- result.compact
111
- end
112
-
113
- def safe_serialize(value)
114
- # Delegate to the existing serialization logic in ExpectationHelperWrapper
115
- RSpec::EnrichedJson::ExpectationHelperWrapper::Serializer.serialize_value(value)
116
- rescue => e
117
- # Better error recovery - provide context about what failed
118
- begin
119
- obj_class = value.class.name
120
- rescue
121
- obj_class = "Unknown"
122
- end
123
-
124
- {
125
- "serialization_error" => true,
126
- "error_class" => e.class.name,
127
- "error_message" => e.message,
128
- "object_class" => obj_class,
129
- "fallback_value" => safe_fallback_value(value)
130
- }
131
- end
132
-
133
- def safe_fallback_value(value)
134
- # Try multiple fallback strategies
135
- value.to_s
136
- rescue
137
- begin
138
- value.class.name
139
- rescue
140
- "Unable to serialize"
141
- end
77
+ def close(_notification)
78
+ super
79
+ RSpec::EnrichedJson.clear_test_values
142
80
  end
143
81
  end
144
82
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module EnrichedJson
5
- VERSION = "0.6.2"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
@@ -20,9 +20,10 @@ Gem::Specification.new do |spec|
20
20
  "source_code_uri" => "https://github.com/firstdraft/rspec-enriched_json"
21
21
  }
22
22
 
23
- spec.required_ruby_version = ">= 2.7.0"
23
+ spec.required_ruby_version = ">= 3.2"
24
24
  spec.add_dependency "rspec-core", ">= 3.0"
25
25
  spec.add_dependency "rspec-expectations", ">= 3.0"
26
+ spec.add_dependency "oj", "~> 3.16"
26
27
 
27
28
  spec.add_development_dependency "rspec", "~> 3.0"
28
29
  spec.add_development_dependency "rake", "~> 13.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-enriched_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raghu Betina
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: oj
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.16'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.16'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rspec
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -113,7 +127,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
113
127
  requirements:
114
128
  - - ">="
115
129
  - !ruby/object:Gem::Version
116
- version: 2.7.0
130
+ version: '3.2'
117
131
  required_rubygems_version: !ruby/object:Gem::Requirement
118
132
  requirements:
119
133
  - - ">="