rspec-enriched_json 0.1.0 → 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: 563d2936f51c8422129b04e84c39447739a5a0895dd02e3a1e3db58de431a9e4
4
- data.tar.gz: f164a70a9216ff65b4e54dbc90b398858f0a6e3b2ea4fa00967a807e71e534c2
3
+ metadata.gz: ae7cf3c69f8919e4810f402f0f34fa88bb69affbadcf6f0f9ec0f1150a4bd184
4
+ data.tar.gz: 4102bf64a17bf2a5d925814f0cb63cb749c14a41a1c8df099a85c1c636b1737c
5
5
  SHA512:
6
- metadata.gz: b3947459ce3038f272de17e1e6c5425dda737c9d694c04d628ba3a0b00041edf2b0090c51a52daa4c481db970fefe2f123037298760edd334e62e25bcf1d77c5
7
- data.tar.gz: 850a62aef71132f3aa4ab925890c6f6841ff18d499e52cc919b1ced2dbf18aa8ea48c20feed970f07748e2400896292c4cd08de72b7a1e442338774cd4c0778d
6
+ metadata.gz: 389db8798122e41f7bceacd58fe4532b2856b3795b96f540915fbe40f2dab9e6892b1dd7b790c7b04254db450d0696188ef3522d6971309808cf15eaa7bcb63b
7
+ data.tar.gz: 2d1a5f679578f2621bc54e9a60199ba167e5a7e5bb03fb027aaefdb0183d420e10d9e14abc1879e7ab43f04215348a823f732043e48feeb8ae2f898a029a2336
data/README.md CHANGED
@@ -4,35 +4,19 @@ A drop-in replacement for RSpec's built-in JSON formatter that enriches the outp
4
4
 
5
5
  ## Quick Demo
6
6
 
7
- To see the difference between RSpec's built-in JSON formatter and this enriched formatter, use the included demo script:
7
+ To see the difference between RSpec's built-in JSON formatter and this enriched formatter:
8
8
 
9
9
  ```bash
10
- # Compare the two formatters side-by-side
11
- # Built-in formatter (string-only failure messages):
12
- bundle exec rspec demo_all_failures.rb --format json --no-profile 2>/dev/null | jq '.examples[4]'
13
-
14
- # Enriched formatter (includes structured_data field):
15
- bundle exec rspec demo_all_failures.rb --format RSpec::EnrichedJson::Formatters::EnrichedJsonFormatter --no-profile -r ./lib/rspec/enriched_json 2>/dev/null | jq '.examples[4]'
16
-
17
- # For a cleaner comparison, look at just the structured data:
18
- bundle exec rspec demo_all_failures.rb --format RSpec::EnrichedJson::Formatters::EnrichedJsonFormatter --no-profile -r ./lib/rspec/enriched_json 2>/dev/null | jq '.examples[] | select(.description == "fails with string comparison") | .structured_data'
10
+ ruby demo.rb
19
11
  ```
20
12
 
21
- The demo file `demo_all_failures.rb` contains 59 different test failure scenarios covering virtually all RSpec matcher types and how they appear in the JSON output.
13
+ This interactive demo script runs the same failing tests with both formatters and shows you the difference side-by-side. No external dependencies, no file cleanup needed!
22
14
 
23
- The demo script (`demo_all_failures.rb`) includes various types of test failures:
24
- - Simple equality failures
25
- - String, array, and hash comparisons
26
- - Custom error messages
27
- - Exception failures
28
- - Complex object comparisons
29
- - Various matcher types (include, respond_to, be_within, etc.)
30
-
31
- Key differences you'll see:
32
- - **Built-in formatter**: Failure information is embedded in string messages
33
- - **Enriched formatter**: Adds a `structured_data` field with:
15
+ **What you'll see:**
16
+ - **Built-in formatter**: Failure information embedded in string messages
17
+ - **Enriched formatter**: Adds structured data with:
34
18
  - `expected`: The expected value as a proper JSON object
35
- - `actual`: The actual value as a proper JSON object
19
+ - `actual`: The actual value as a proper JSON object
36
20
  - `matcher_name`: The RSpec matcher class used
37
21
  - `original_message`: Preserved when custom messages are provided
38
22
 
@@ -99,11 +83,14 @@ With this gem, you get structured data alongside the original message:
99
83
  "message": "\nexpected: \"Hello, Ruby!\"\n got: \"Hello, World!\"\n\n(compared using ==)\n",
100
84
  "backtrace": ["./spec/example_spec.rb:5:in `block (2 levels) in <top (required)>'"]
101
85
  },
102
- "structured_data": {
86
+ "enriched_with": {
103
87
  "expected": "Hello, Ruby!",
104
88
  "actual": "Hello, World!",
105
89
  "matcher_name": "RSpec::Matchers::BuiltIn::Eq",
106
- "original_message": null
90
+ "original_message": null,
91
+ "diff_info": {
92
+ "diffable": true
93
+ }
107
94
  }
108
95
  }
109
96
  ```
@@ -115,26 +102,29 @@ With this gem, you get structured data alongside the original message:
115
102
  - **Rich object support**: Arrays, hashes, and custom objects are properly serialized
116
103
  - **Original message preservation**: When you override with a custom message, the original is preserved
117
104
  - **Graceful degradation**: Regular exceptions (non-expectation failures) work normally
105
+ - **Enhanced metadata capture**: Test location, tags, hierarchy, and custom metadata
106
+ - **Robust error recovery**: Handles objects that fail to serialize without crashing
107
+ - **Diff information**: Includes `diff_info.diffable` to help tools determine if values can be meaningfully diffed
118
108
 
119
109
  ## Examples
120
110
 
121
111
  ### Simple Values
122
112
  ```ruby
123
113
  expect(1 + 1).to eq(3)
124
- # structured_data: { "expected": 3, "actual": 2 }
114
+ # enriched_with: { "expected": 3, "actual": 2 }
125
115
  ```
126
116
 
127
117
  ### Collections
128
118
  ```ruby
129
119
  expect([1, 2, 3]).to eq([1, 2, 4])
130
- # structured_data: { "expected": [1, 2, 4], "actual": [1, 2, 3] }
120
+ # enriched_with: { "expected": [1, 2, 4], "actual": [1, 2, 3] }
131
121
  ```
132
122
 
133
123
  ### Complex Objects
134
124
  ```ruby
135
125
  Product = Struct.new(:name, :price)
136
126
  expect(Product.new("Laptop", 999)).to eq(Product.new("Laptop", 899))
137
- # structured_data includes class info and struct values
127
+ # enriched_with includes class info and struct values
138
128
  ```
139
129
 
140
130
  ### Custom Messages
@@ -142,7 +132,19 @@ expect(Product.new("Laptop", 999)).to eq(Product.new("Laptop", 899))
142
132
  expect(balance).to be >= required,
143
133
  "Insufficient funds: $#{balance} available, $#{required} required"
144
134
  # exception.message: "Insufficient funds: $50 available, $100 required"
145
- # structured_data: { "original_message": "expected: >= 100\n got: 50" }
135
+ # enriched_with: { "original_message": "expected: >= 100\n got: 50" }
136
+ ```
137
+
138
+ ### Metadata Capture
139
+ ```ruby
140
+ it "validates user input", :slow, :db, priority: :high do
141
+ expect(user).to be_valid
142
+ end
143
+ # metadata includes:
144
+ # - location: "./spec/models/user_spec.rb:42"
145
+ # - absolute_file_path: "/path/to/project/spec/models/user_spec.rb"
146
+ # - tags: { "slow": true, "db": true, "priority": "high" }
147
+ # - example_group_hierarchy: ["User", "validations", "email format"]
146
148
  ```
147
149
 
148
150
  ## Use Cases
@@ -6,11 +6,11 @@ module RSpec
6
6
  module EnrichedJson
7
7
  # Custom exception that carries structured data alongside the message
8
8
  class EnrichedExpectationNotMetError < RSpec::Expectations::ExpectationNotMetError
9
- attr_reader :structured_data
9
+ attr_reader :enriched_with
10
10
 
11
- def initialize(message, structured_data = {})
11
+ def initialize(message, enriched_with = {})
12
12
  super(message)
13
- @structured_data = structured_data
13
+ @enriched_with = enriched_with
14
14
  end
15
15
  end
16
16
  end
@@ -15,6 +15,82 @@ module RSpec
15
15
  RSpec::Expectations::ExpectationHelper.singleton_class.prepend(self)
16
16
  end
17
17
 
18
+ # Make serialize_value accessible for other components
19
+ module Serializer
20
+ extend self
21
+
22
+ MAX_SERIALIZATION_DEPTH = 5
23
+ MAX_ARRAY_SIZE = 100
24
+ MAX_HASH_SIZE = 100
25
+ MAX_STRING_LENGTH = 1000
26
+
27
+ def serialize_value(value, depth = 0)
28
+ return "[Max depth exceeded]" if depth > MAX_SERIALIZATION_DEPTH
29
+
30
+ case value
31
+ when nil, Numeric, TrueClass, FalseClass
32
+ value
33
+ when String
34
+ truncate_string(value)
35
+ when Symbol
36
+ value.to_s
37
+ when Array
38
+ return "[Large array: #{value.size} items]" if value.size > MAX_ARRAY_SIZE
39
+ value.map { |v| serialize_value(v, depth + 1) }
40
+ when Hash
41
+ return "[Large hash: #{value.size} keys]" if value.size > MAX_HASH_SIZE
42
+ value.transform_values { |v| serialize_value(v, depth + 1) }
43
+ else
44
+ serialize_object(value, depth)
45
+ end
46
+ rescue => e
47
+ {
48
+ "class" => value.class.name,
49
+ "serialization_error" => e.message
50
+ }
51
+ end
52
+
53
+ def serialize_object(obj, depth = 0)
54
+ result = {
55
+ "class" => obj.class.name,
56
+ "inspect" => safe_inspect(obj),
57
+ "to_s" => safe_to_s(obj)
58
+ }
59
+
60
+ # Handle Structs specially
61
+ if obj.is_a?(Struct)
62
+ result["struct_values"] = obj.to_h.transform_values { |v| serialize_value(v, depth + 1) }
63
+ end
64
+
65
+ # Include instance variables only for small objects
66
+ ivars = obj.instance_variables
67
+ if ivars.any? && ivars.length <= 10
68
+ result["instance_variables"] = ivars.each_with_object({}) do |ivar, hash|
69
+ hash[ivar.to_s] = serialize_value(obj.instance_variable_get(ivar), depth + 1)
70
+ end
71
+ end
72
+
73
+ result
74
+ end
75
+
76
+ def truncate_string(str)
77
+ return str if str.length <= MAX_STRING_LENGTH
78
+ "#{str[0...MAX_STRING_LENGTH]}... (truncated)"
79
+ end
80
+
81
+ def safe_inspect(obj)
82
+ truncate_string(obj.inspect)
83
+ rescue => e
84
+ "[inspect failed: #{e.class}]"
85
+ end
86
+
87
+ def safe_to_s(obj)
88
+ truncate_string(obj.to_s)
89
+ rescue => e
90
+ "[to_s failed: #{e.class}]"
91
+ end
92
+ end
93
+
18
94
  def handle_failure(matcher, message, failure_message_method)
19
95
  # If a custom message is provided, capture the original message first
20
96
  original_message = nil
@@ -25,16 +101,23 @@ module RSpec
25
101
  # Call original handler with the original message
26
102
  super
27
103
  rescue RSpec::Expectations::ExpectationNotMetError => e
104
+ # Extract raw values for diff analysis
105
+ expected_raw = extract_value(matcher, :expected)
106
+ actual_raw = extract_value(matcher, :actual)
107
+
28
108
  # Collect structured data
29
- structured_data = {
30
- expected: serialize_value(extract_value(matcher, :expected)),
31
- actual: serialize_value(extract_value(matcher, :actual)),
109
+ enriched_with = {
110
+ expected: Serializer.serialize_value(expected_raw),
111
+ actual: Serializer.serialize_value(actual_raw),
32
112
  original_message: original_message, # Only populated when custom message overrides it
33
- matcher_name: matcher.class.name
113
+ matcher_name: matcher.class.name,
114
+ diff_info: {
115
+ diffable: values_diffable?(expected_raw, actual_raw, matcher)
116
+ }
34
117
  }
35
118
 
36
119
  # Raise new exception with data attached
37
- raise EnrichedExpectationNotMetError.new(e.message, structured_data)
120
+ raise EnrichedExpectationNotMetError.new(e.message, enriched_with)
38
121
  end
39
122
 
40
123
  private
@@ -43,75 +126,38 @@ module RSpec
43
126
  return nil unless matcher.respond_to?(method_name)
44
127
 
45
128
  value = matcher.send(method_name)
46
- (value == matcher) ? nil : value
129
+ # Don't return nil if the value itself is nil
130
+ # Only return nil if the value is the matcher itself (self-referential)
131
+ (value == matcher && !value.nil?) ? nil : value
47
132
  rescue
48
133
  nil
49
134
  end
50
135
 
51
- def serialize_value(value, depth = 0)
52
- return "[Max depth exceeded]" if depth > MAX_SERIALIZATION_DEPTH
53
-
54
- case value
55
- when nil, Numeric, TrueClass, FalseClass
56
- value
57
- when String
58
- truncate_string(value)
59
- when Symbol
60
- value.to_s
61
- when Array
62
- return "[Large array: #{value.size} items]" if value.size > MAX_ARRAY_SIZE
63
- value.map { |v| serialize_value(v, depth + 1) }
64
- when Hash
65
- return "[Large hash: #{value.size} keys]" if value.size > MAX_HASH_SIZE
66
- value.transform_values { |v| serialize_value(v, depth + 1) }
67
- else
68
- serialize_object(value, depth)
136
+ def values_diffable?(expected, actual, matcher)
137
+ # First check if the matcher itself declares diffability
138
+ if matcher.respond_to?(:diffable?)
139
+ return matcher.diffable?
69
140
  end
70
- rescue => e
71
- {
72
- "class" => value.class.name,
73
- "serialization_error" => e.message
74
- }
75
- end
76
141
 
77
- def serialize_object(obj, depth = 0)
78
- result = {
79
- "class" => obj.class.name,
80
- "inspect" => safe_inspect(obj),
81
- "to_s" => safe_to_s(obj)
82
- }
142
+ # If either value is nil, not diffable
143
+ return false if expected.nil? || actual.nil?
83
144
 
84
- # Handle Structs specially
85
- if obj.is_a?(Struct)
86
- result["struct_values"] = obj.to_h.transform_values { |v| serialize_value(v, depth + 1) }
87
- end
145
+ # For different classes, generally not diffable
146
+ return false unless actual.instance_of?(expected.class)
88
147
 
89
- # Include instance variables only for small objects
90
- ivars = obj.instance_variables
91
- if ivars.any? && ivars.length <= 10
92
- result["instance_variables"] = ivars.each_with_object({}) do |ivar, hash|
93
- hash[ivar.to_s] = serialize_value(obj.instance_variable_get(ivar), depth + 1)
94
- end
148
+ # Check if both values are of the same basic diffable type
149
+ case expected
150
+ when String, Array, Hash
151
+ # These types are inherently diffable when compared to same type
152
+ true
153
+ else
154
+ # For other types, they're diffable if they respond to to_s
155
+ # and their string representations would be meaningful
156
+ expected.respond_to?(:to_s) && actual.respond_to?(:to_s)
95
157
  end
96
-
97
- result
98
- end
99
-
100
- def truncate_string(str)
101
- return str if str.length <= MAX_STRING_LENGTH
102
- "#{str[0...MAX_STRING_LENGTH]}... (truncated)"
103
- end
104
-
105
- def safe_inspect(obj)
106
- truncate_string(obj.inspect)
107
- rescue => e
108
- "[inspect failed: #{e.class}]"
109
- end
110
-
111
- def safe_to_s(obj)
112
- truncate_string(obj.to_s)
113
- rescue => e
114
- "[to_s failed: #{e.class}]"
158
+ rescue
159
+ # If any error occurs during checking, assume not diffable
160
+ false
115
161
  end
116
162
  end
117
163
  end
@@ -12,6 +12,9 @@ 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
+ add_metadata(hash, notification.example)
17
+
15
18
  e = notification.example.exception
16
19
 
17
20
  if e
@@ -22,18 +25,105 @@ module RSpec
22
25
  }
23
26
 
24
27
  # Add structured data if available
25
- if e.is_a?(RSpec::EnrichedJson::EnrichedExpectationNotMetError) && e.structured_data
26
- hash[:structured_data] = {
27
- expected: e.structured_data[:expected],
28
- actual: e.structured_data[:actual],
29
- matcher_name: e.structured_data[:matcher_name],
30
- original_message: e.structured_data[:original_message]
31
- }
28
+ if e.is_a?(RSpec::EnrichedJson::EnrichedExpectationNotMetError) && e.enriched_with
29
+ hash[:enriched_with] = safe_structured_data(e.enriched_with)
32
30
  end
33
31
  end
34
32
  end
35
33
  end
36
34
  end
35
+
36
+ private
37
+
38
+ def add_metadata(hash, example)
39
+ metadata = example.metadata.dup
40
+
41
+ # Extract custom tags (all symbols and specific keys)
42
+ custom_tags = {}
43
+ metadata.each do |key, value|
44
+ # Include all symbol keys (like :focus, :slow, etc.)
45
+ if key.is_a?(Symbol) && value == true
46
+ custom_tags[key] = true
47
+ # Include specific metadata that might be useful
48
+ elsif [:type, :priority, :severity, :db, :js].include?(key)
49
+ custom_tags[key] = value
50
+ end
51
+ end
52
+
53
+ # Add enhanced metadata
54
+ hash[:metadata] = {
55
+ # Location information
56
+ location: example.location,
57
+ absolute_file_path: File.expand_path(example.metadata[:file_path]),
58
+ rerun_file_path: example.location_rerun_argument,
59
+
60
+ # Example hierarchy
61
+ example_group: example.example_group.description,
62
+ example_group_hierarchy: extract_group_hierarchy(example),
63
+
64
+ # Described class if available
65
+ described_class: metadata[:described_class]&.to_s,
66
+
67
+ # Custom tags and metadata
68
+ tags: custom_tags.empty? ? nil : custom_tags,
69
+
70
+ # Shared example information if applicable
71
+ shared_group_inclusion_backtrace: metadata[:shared_group_inclusion_backtrace]
72
+ }.compact # Remove nil values
73
+ end
74
+
75
+ def extract_group_hierarchy(example)
76
+ hierarchy = []
77
+ current_group = example.example_group
78
+
79
+ while current_group
80
+ hierarchy.unshift(current_group.description)
81
+ current_group = (current_group.superclass < RSpec::Core::ExampleGroup) ? current_group.superclass : nil
82
+ end
83
+
84
+ hierarchy
85
+ end
86
+
87
+ def safe_structured_data(enriched_with)
88
+ {
89
+ expected: safe_serialize(enriched_with[:expected]),
90
+ actual: safe_serialize(enriched_with[:actual]),
91
+ matcher_name: enriched_with[:matcher_name],
92
+ original_message: enriched_with[:original_message],
93
+ diff_info: enriched_with[:diff_info]
94
+ }.compact
95
+ end
96
+
97
+ def safe_serialize(value)
98
+ # Delegate to the existing serialization logic in ExpectationHelperWrapper
99
+ RSpec::EnrichedJson::ExpectationHelperWrapper::Serializer.serialize_value(value)
100
+ rescue => e
101
+ # Better error recovery - provide context about what failed
102
+ begin
103
+ obj_class = value.class.name
104
+ rescue
105
+ obj_class = "Unknown"
106
+ end
107
+
108
+ {
109
+ "serialization_error" => true,
110
+ "error_class" => e.class.name,
111
+ "error_message" => e.message,
112
+ "object_class" => obj_class,
113
+ "fallback_value" => safe_fallback_value(value)
114
+ }
115
+ end
116
+
117
+ def safe_fallback_value(value)
118
+ # Try multiple fallback strategies
119
+ value.to_s
120
+ rescue
121
+ begin
122
+ value.class.name
123
+ rescue
124
+ "Unable to serialize"
125
+ end
126
+ end
37
127
  end
38
128
  end
39
129
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module EnrichedJson
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raghu Betina