rspec-enriched_json 0.1.0 → 0.2.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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01c7824579e7c7ed920470597289a95c675f57b334796ab2a2b5560366ce5e4b
|
4
|
+
data.tar.gz: 6cc9a5b114339904133849fee204bb1b874e0ca36a54bcb3fc8b95f1639c149b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 362ca7d1944dd84d9b58f0d40aafc1eaf57e1befcf0f55f3876bcf9866769e770d1f62d637053b525ed6e059a9840679034688abab3084907de81675ab8ef0a6
|
7
|
+
data.tar.gz: 9aad9fe933afbf938b721e721d7e381b6b8ad82a40442c097920d7adfe9edafdf21f771b0637b892f70053c2893c67812a32b6c18c27819c445643016944790f
|
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
|
7
|
+
To see the difference between RSpec's built-in JSON formatter and this enriched formatter:
|
8
8
|
|
9
9
|
```bash
|
10
|
-
|
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
|
-
|
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
|
-
|
24
|
-
-
|
25
|
-
-
|
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
|
|
@@ -103,7 +87,10 @@ With this gem, you get structured data alongside the original message:
|
|
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,6 +102,9 @@ 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
|
|
@@ -145,6 +135,18 @@ expect(balance).to be >= required,
|
|
145
135
|
# structured_data: { "original_message": "expected: >= 100\n got: 50" }
|
146
136
|
```
|
147
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"]
|
148
|
+
```
|
149
|
+
|
148
150
|
## Use Cases
|
149
151
|
|
150
152
|
- **CI/CD Integration**: Parse test results to create rich error reports
|
@@ -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,12 +101,19 @@ 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
109
|
structured_data = {
|
30
|
-
expected: serialize_value(
|
31
|
-
actual: serialize_value(
|
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
|
@@ -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
|
-
|
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
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
78
|
-
|
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
|
-
#
|
85
|
-
|
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
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
98
|
-
|
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
|
@@ -23,17 +26,104 @@ module RSpec
|
|
23
26
|
|
24
27
|
# Add structured data if available
|
25
28
|
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
|
-
}
|
29
|
+
hash[:structured_data] = safe_structured_data(e.structured_data)
|
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(structured_data)
|
88
|
+
{
|
89
|
+
expected: safe_serialize(structured_data[:expected]),
|
90
|
+
actual: safe_serialize(structured_data[:actual]),
|
91
|
+
matcher_name: structured_data[:matcher_name],
|
92
|
+
original_message: structured_data[:original_message],
|
93
|
+
diff_info: structured_data[: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
|