rspec-enriched_json 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 563d2936f51c8422129b04e84c39447739a5a0895dd02e3a1e3db58de431a9e4
4
+ data.tar.gz: f164a70a9216ff65b4e54dbc90b398858f0a6e3b2ea4fa00967a807e71e534c2
5
+ SHA512:
6
+ metadata.gz: b3947459ce3038f272de17e1e6c5425dda737c9d694c04d628ba3a0b00041edf2b0090c51a52daa4c481db970fefe2f123037298760edd334e62e25bcf1d77c5
7
+ data.tar.gz: 850a62aef71132f3aa4ab925890c6f6841ff18d499e52cc919b1ced2dbf18aa8ea48c20feed970f07748e2400896292c4cd08de72b7a1e442338774cd4c0778d
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Raghu Betina
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # RSpec::EnrichedJson
2
+
3
+ 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
+
5
+ ## Quick Demo
6
+
7
+ To see the difference between RSpec's built-in JSON formatter and this enriched formatter, use the included demo script:
8
+
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'
19
+ ```
20
+
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.
22
+
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:
34
+ - `expected`: The expected value as a proper JSON object
35
+ - `actual`: The actual value as a proper JSON object
36
+ - `matcher_name`: The RSpec matcher class used
37
+ - `original_message`: Preserved when custom messages are provided
38
+
39
+ ## Requirements
40
+
41
+ - Ruby 2.7 or higher
42
+ - RSpec 3.0 or higher
43
+
44
+ ## Installation
45
+
46
+ Add this line to your application's Gemfile:
47
+
48
+ ```ruby
49
+ gem 'rspec-enriched_json'
50
+ ```
51
+
52
+ And then execute:
53
+
54
+ $ bundle install
55
+
56
+ Or install it yourself as:
57
+
58
+ $ gem install rspec-enriched_json
59
+
60
+ ## Usage
61
+
62
+ Use it just like RSpec's built-in JSON formatter:
63
+
64
+ ```bash
65
+ # Command line
66
+ rspec --format RSpec::EnrichedJson::Formatters::EnrichedJsonFormatter
67
+
68
+ # Or in your .rspec file
69
+ --format RSpec::EnrichedJson::Formatters::EnrichedJsonFormatter
70
+
71
+ # Or in spec_helper.rb
72
+ RSpec.configure do |config|
73
+ config.formatter = RSpec::EnrichedJson::Formatters::EnrichedJsonFormatter
74
+ end
75
+ ```
76
+
77
+ ## What's Different?
78
+
79
+ ### Standard RSpec JSON Output
80
+
81
+ With RSpec's built-in JSON formatter, failure information comes as a string:
82
+
83
+ ```json
84
+ {
85
+ "exception": {
86
+ "message": "\nexpected: \"Hello, Ruby!\"\n got: \"Hello, World!\"\n\n(compared using ==)\n"
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Enriched JSON Output
92
+
93
+ With this gem, you get structured data alongside the original message:
94
+
95
+ ```json
96
+ {
97
+ "exception": {
98
+ "class": "RSpec::EnrichedJson::EnrichedExpectationNotMetError",
99
+ "message": "\nexpected: \"Hello, Ruby!\"\n got: \"Hello, World!\"\n\n(compared using ==)\n",
100
+ "backtrace": ["./spec/example_spec.rb:5:in `block (2 levels) in <top (required)>'"]
101
+ },
102
+ "structured_data": {
103
+ "expected": "Hello, Ruby!",
104
+ "actual": "Hello, World!",
105
+ "matcher_name": "RSpec::Matchers::BuiltIn::Eq",
106
+ "original_message": null
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## Features
112
+
113
+ - **Drop-in replacement**: Inherits from RSpec's JsonFormatter, maintaining 100% compatibility
114
+ - **Structured data extraction**: Expected and actual values as proper JSON objects
115
+ - **Rich object support**: Arrays, hashes, and custom objects are properly serialized
116
+ - **Original message preservation**: When you override with a custom message, the original is preserved
117
+ - **Graceful degradation**: Regular exceptions (non-expectation failures) work normally
118
+
119
+ ## Examples
120
+
121
+ ### Simple Values
122
+ ```ruby
123
+ expect(1 + 1).to eq(3)
124
+ # structured_data: { "expected": 3, "actual": 2 }
125
+ ```
126
+
127
+ ### Collections
128
+ ```ruby
129
+ expect([1, 2, 3]).to eq([1, 2, 4])
130
+ # structured_data: { "expected": [1, 2, 4], "actual": [1, 2, 3] }
131
+ ```
132
+
133
+ ### Complex Objects
134
+ ```ruby
135
+ Product = Struct.new(:name, :price)
136
+ expect(Product.new("Laptop", 999)).to eq(Product.new("Laptop", 899))
137
+ # structured_data includes class info and struct values
138
+ ```
139
+
140
+ ### Custom Messages
141
+ ```ruby
142
+ expect(balance).to be >= required,
143
+ "Insufficient funds: $#{balance} available, $#{required} required"
144
+ # exception.message: "Insufficient funds: $50 available, $100 required"
145
+ # structured_data: { "original_message": "expected: >= 100\n got: 50" }
146
+ ```
147
+
148
+ ## Use Cases
149
+
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
+
155
+ ## How It Works
156
+
157
+ The gem works by:
158
+
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
162
+
163
+ ## Development
164
+
165
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
166
+
167
+ ## Performance Considerations
168
+
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
174
+
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
180
+
181
+ ## Contributing
182
+
183
+ Bug reports and pull requests are welcome on GitHub at https://github.com/firstdraft/rspec-enriched_json.
184
+
185
+ ## License
186
+
187
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/expectations"
4
+
5
+ module RSpec
6
+ module EnrichedJson
7
+ # Custom exception that carries structured data alongside the message
8
+ class EnrichedExpectationNotMetError < RSpec::Expectations::ExpectationNotMetError
9
+ attr_reader :structured_data
10
+
11
+ def initialize(message, structured_data = {})
12
+ super(message)
13
+ @structured_data = structured_data
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rspec/expectations"
5
+
6
+ module RSpec
7
+ module EnrichedJson
8
+ # Universal wrapper to catch ALL matchers and attach structured data
9
+ module ExpectationHelperWrapper
10
+ MAX_SERIALIZATION_DEPTH = 5
11
+ MAX_ARRAY_SIZE = 100
12
+ MAX_HASH_SIZE = 100
13
+ MAX_STRING_LENGTH = 1000
14
+ def self.install!
15
+ RSpec::Expectations::ExpectationHelper.singleton_class.prepend(self)
16
+ end
17
+
18
+ def handle_failure(matcher, message, failure_message_method)
19
+ # If a custom message is provided, capture the original message first
20
+ original_message = nil
21
+ if message
22
+ original_message = matcher.send(failure_message_method)
23
+ end
24
+
25
+ # Call original handler with the original message
26
+ super
27
+ rescue RSpec::Expectations::ExpectationNotMetError => e
28
+ # Collect structured data
29
+ structured_data = {
30
+ expected: serialize_value(extract_value(matcher, :expected)),
31
+ actual: serialize_value(extract_value(matcher, :actual)),
32
+ original_message: original_message, # Only populated when custom message overrides it
33
+ matcher_name: matcher.class.name
34
+ }
35
+
36
+ # Raise new exception with data attached
37
+ raise EnrichedExpectationNotMetError.new(e.message, structured_data)
38
+ end
39
+
40
+ private
41
+
42
+ def extract_value(matcher, method_name)
43
+ return nil unless matcher.respond_to?(method_name)
44
+
45
+ value = matcher.send(method_name)
46
+ (value == matcher) ? nil : value
47
+ rescue
48
+ nil
49
+ end
50
+
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)
69
+ end
70
+ rescue => e
71
+ {
72
+ "class" => value.class.name,
73
+ "serialization_error" => e.message
74
+ }
75
+ end
76
+
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
+ }
83
+
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
88
+
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
95
+ 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}]"
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ # Auto-install when this file is required
121
+ RSpec::EnrichedJson::ExpectationHelperWrapper.install!
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rspec/core/formatters/json_formatter"
5
+
6
+ module RSpec
7
+ module EnrichedJson
8
+ module Formatters
9
+ class EnrichedJsonFormatter < RSpec::Core::Formatters::JsonFormatter
10
+ RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :seed, :close
11
+
12
+ def stop(group_notification)
13
+ @output_hash[:examples] = group_notification.notifications.map do |notification|
14
+ format_example(notification.example).tap do |hash|
15
+ e = notification.example.exception
16
+
17
+ if e
18
+ hash[:exception] = {
19
+ class: e.class.name,
20
+ message: e.message,
21
+ backtrace: notification.formatted_backtrace
22
+ }
23
+
24
+ # 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
+ }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module EnrichedJson
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/enriched_json/version"
4
+ require "rspec/enriched_json/enriched_expectation_not_met_error"
5
+ require "rspec/enriched_json/expectation_helper_wrapper"
6
+ require "rspec/enriched_json/formatters/enriched_json_formatter"
7
+
8
+ module RSpec
9
+ module EnrichedJson
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rspec/enriched_json/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rspec-enriched_json"
7
+ spec.version = RSpec::EnrichedJson::VERSION
8
+ spec.authors = ["Raghu Betina"]
9
+ spec.email = ["raghu@firstdraft.com"]
10
+ spec.homepage = "https://github.com/firstdraft/rspec-enriched_json"
11
+ spec.summary = "Enriches RSpec JSON output with structured failure data"
12
+ spec.description = "A drop-in replacement for RSpec's built-in JSON formatter that adds structured test failure data, making it easy to programmatically analyze test results."
13
+ spec.license = "MIT"
14
+
15
+ spec.metadata = {
16
+ "bug_tracker_uri" => "https://github.com/firstdraft/rspec-enriched_json/issues",
17
+ "changelog_uri" => "https://github.com/firstdraft/rspec-enriched_json/blob/main/CHANGELOG.md",
18
+ "homepage_uri" => "https://github.com/firstdraft/rspec-enriched_json",
19
+ "rubygems_mfa_required" => "true",
20
+ "source_code_uri" => "https://github.com/firstdraft/rspec-enriched_json"
21
+ }
22
+
23
+ spec.required_ruby_version = ">= 2.7.0"
24
+ spec.add_dependency "rspec-core", ">= 3.0"
25
+ spec.add_dependency "rspec-expectations", ">= 3.0"
26
+
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "rake", "~> 13.0"
29
+ spec.add_development_dependency "standard", "~> 1.0"
30
+
31
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
32
+ spec.files = Dir["*.gemspec", "lib/**/*"]
33
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-enriched_json
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Raghu Betina
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec-expectations
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: standard
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ description: A drop-in replacement for RSpec's built-in JSON formatter that adds structured
83
+ test failure data, making it easy to programmatically analyze test results.
84
+ email:
85
+ - raghu@firstdraft.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files:
89
+ - LICENSE.md
90
+ - README.md
91
+ files:
92
+ - LICENSE.md
93
+ - README.md
94
+ - lib/rspec/enriched_json.rb
95
+ - lib/rspec/enriched_json/enriched_expectation_not_met_error.rb
96
+ - lib/rspec/enriched_json/expectation_helper_wrapper.rb
97
+ - lib/rspec/enriched_json/formatters/enriched_json_formatter.rb
98
+ - lib/rspec/enriched_json/version.rb
99
+ - rspec-enriched_json.gemspec
100
+ homepage: https://github.com/firstdraft/rspec-enriched_json
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ bug_tracker_uri: https://github.com/firstdraft/rspec-enriched_json/issues
105
+ changelog_uri: https://github.com/firstdraft/rspec-enriched_json/blob/main/CHANGELOG.md
106
+ homepage_uri: https://github.com/firstdraft/rspec-enriched_json
107
+ rubygems_mfa_required: 'true'
108
+ source_code_uri: https://github.com/firstdraft/rspec-enriched_json
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 2.7.0
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.6.9
124
+ specification_version: 4
125
+ summary: Enriches RSpec JSON output with structured failure data
126
+ test_files: []