File List
25 | 40 | 41 |-
48 |
49 |
50 |
- 51 | 52 | 53 | 54 | 55 | 56 |
├── Gemfile ├── doc ├── css │ ├── common.css │ ├── full_list.css │ └── style.css ├── images │ ├── enhance.png │ ├── enhanced-spec.png │ └── enhanced-error.png ├── frames.html ├── file_list.html ├── Enhanced │ ├── Integrations.html │ ├── Integrations │ │ └── RSpecErrorFailureMessage.html │ ├── Context.html │ ├── ExceptionBindingInfos.html │ ├── Colors.html │ └── ExceptionContext.html ├── Exception.html ├── Enhanced.html ├── class_list.html ├── top-level-namespace.html ├── _index.html ├── Context.html ├── js │ ├── full_list.js │ └── app.js ├── ExceptionBindingInfos.html ├── Minitest.html ├── EnhancedExceptionContext.html └── method_list.html ├── spec ├── spec_helper.rb └── enhancement_context_spec.rb ├── .gitignore ├── lib └── enhanced │ ├── context.rb │ ├── minitest_patch.rb │ ├── colors.rb │ ├── exception_context.rb │ └── exception.rb ├── examples ├── demo_minitest.rb ├── demo_exception_enhancement.rb └── demo_rspec.rb ├── benchmark ├── result.txt ├── stackprofile.rb ├── memory_bench.rb └── benchmark.rb ├── Gemfile.lock ├── LICENSE ├── enhanced_errors.gemspec └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /doc/css/common.css: -------------------------------------------------------------------------------- 1 | /* Override this file with custom rules */ -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # spec_helper.rb 2 | require 'rspec' 3 | require 'enhanced_errors' 4 | -------------------------------------------------------------------------------- /doc/images/enhance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericbeland/enhanced_errors/HEAD/doc/images/enhance.png -------------------------------------------------------------------------------- /doc/images/enhanced-spec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericbeland/enhanced_errors/HEAD/doc/images/enhanced-spec.png -------------------------------------------------------------------------------- /doc/images/enhanced-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericbeland/enhanced_errors/HEAD/doc/images/enhanced-error.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /.env 3 | /log 4 | /tmp 5 | *.gem 6 | *.swp 7 | .DS_Store 8 | stackprof.dump 9 | .ruby-version 10 | .idea/ 11 | enhanced_errors.iml 12 | .yardoc 13 | 14 | -------------------------------------------------------------------------------- /lib/enhanced/context.rb: -------------------------------------------------------------------------------- 1 | module Enhanced 2 | class Context 3 | attr_accessor :binding_infos 4 | 5 | def initialize 6 | @binding_infos = [] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /examples/demo_minitest.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'enhanced_errors' 3 | require 'enhanced/minitest_patch' 4 | 5 | # You must install minitest and load it first to run this demo. 6 | # EnhancedErrors does NOT ship with minitest as a dependency. 7 | 8 | class MagicBallTest < Minitest::Test 9 | def setup 10 | @foo = 'bar' 11 | end 12 | 13 | def test_boo_capture 14 | bee = 'fee' 15 | assert false 16 | end 17 | 18 | def test_i_raise 19 | zoo = 'zee' 20 | raise "Crud" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /benchmark/result.txt: -------------------------------------------------------------------------------- 1 | Result from MacOs, Ruby 3.3.6 for benchmark.rb 2 | 3 | -------------------------------------------- 4 | 5 | Cost Exploration 6 | user system total real 7 | Baseline 1k (NO EnhancedErrors, tight error raise loop): 0.000386 0.000049 0.000435 ( 0.000434) 8 | 9 | user system total real 10 | Stress 1k EnhancedErrors (Tight error raising loop w/ EnhancedErrors): 0.007653 0.000241 0.007894 ( 0.007894) 11 | Cost per 100 raised exceptions: 0.79 ms 12 | -------------------------------------------------------------------------------- /doc/frames.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |88 | 89 | 90 | Modules: RSpecErrorFailureMessage 91 | 92 | 93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |95 | 96 | 97 | Modules: ExceptionBindingInfos, ExceptionContext 98 | 99 | 100 | 101 | Classes: Colors, Context 102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |83 | 84 | 85 | Modules: Enhanced, Minitest 86 | 87 | 88 | 89 | Classes: EnhancedErrors, Exception 90 | 91 | 92 |
93 | 94 | 95 |Exceptions we could handle but overlook for other reasons. These class constants are not always loaded and generally are only be available when ‘required`, so we detect them by strings.
107 | 108 | 109 |%w[JSON::ParserError Zlib::Error OpenSSL::SSL::SSLError Psych::BadAlias]
%w[RSpec::Expectations::ExpectationNotMetError RSpec::Matchers::BuiltIn::RaiseError]
77 |
78 |
79 |
|
151 |
144 | 145 | 146 | 147 | 4 148 | 5 149 | 6 150 | 7 151 | 8 152 | 9 153 | 10154 | |
155 |
156 | # File 'lib/enhanced/integrations/rspec_error_failure_message.rb', line 4 157 | 158 | def execution_result 159 | result = super 160 | if result.exception 161 | EnhancedErrors.(result.exception, self.[:expect_binding]) 162 | end 163 | result 164 | end165 | |
166 |
Returns the value of attribute binding_infos.
131 |A new instance of Context.
170 |Returns a new instance of Context.
194 | 195 | 196 |
204 | 205 | 206 | 207 | 4 208 | 5 209 | 6210 | |
211 |
212 | # File 'lib/enhanced/context.rb', line 4 213 | 214 | def initialize 215 | @binding_infos = [] 216 | end217 | |
218 |
Returns the value of attribute binding_infos.
242 | 243 | 244 |
252 | 253 | 254 | 255 | 2 256 | 3 257 | 4258 | |
259 |
260 | # File 'lib/enhanced/context.rb', line 2 261 | 262 | def binding_infos 263 | @binding_infos 264 | end265 | |
266 |
Returns the value of attribute binding_infos.
131 |A new instance of Context.
170 |Returns a new instance of Context.
194 | 195 | 196 |
204 | 205 | 206 | 207 | 5 208 | 6 209 | 7210 | |
211 |
212 | # File 'lib/enhanced/context.rb', line 5 213 | 214 | def initialize 215 | @binding_infos = [] 216 | end217 | |
218 |
Returns the value of attribute binding_infos.
242 | 243 | 244 |
252 | 253 | 254 | 255 | 3 256 | 4 257 | 5258 | |
259 |
260 | # File 'lib/enhanced/context.rb', line 3 261 | 262 | def binding_infos 263 | @binding_infos 264 | end265 | |
266 |
171 | 172 | 173 | 174 | 5 175 | 6 176 | 7 177 | 8 178 | 9 179 | 10 180 | 11 181 | 12182 | |
183 |
184 | # File 'lib/enhanced/exception.rb', line 5 185 | 186 | def binding_infos 187 | ctx = Enhanced::ExceptionContext.context_for(self) 188 | unless ctx 189 | ctx = Context.new 190 | Enhanced::ExceptionContext.store_context(self, ctx) 191 | end 192 | ctx.binding_infos 193 | end194 | |
195 |
211 | 212 | 213 | 214 | 14 215 | 15 216 | 16 217 | 17 218 | 18 219 | 19 220 | 20 221 | 21 222 | 22 223 | 23224 | |
225 |
226 | # File 'lib/enhanced/exception.rb', line 14 227 | 228 | def captured_variables 229 | if binding_infos.any? 230 | bindings_of_interest = select_binding_infos 231 | EnhancedErrors.format(bindings_of_interest) 232 | else 233 | '' 234 | end 235 | rescue 236 | '' 237 | end238 | |
239 |
171 | 172 | 173 | 174 | 6 175 | 7 176 | 8 177 | 9 178 | 10 179 | 11 180 | 12 181 | 13182 | |
183 |
184 | # File 'lib/enhanced/exception.rb', line 6 185 | 186 | def binding_infos 187 | ctx = Enhanced::ExceptionContext.context_for(self) 188 | unless ctx 189 | ctx = Context.new 190 | Enhanced::ExceptionContext.store_context(self, ctx) 191 | end 192 | ctx.binding_infos 193 | end194 | |
195 |
211 | 212 | 213 | 214 | 15 215 | 16 216 | 17 217 | 18 218 | 19 219 | 20 220 | 21221 | |
222 |
223 | # File 'lib/enhanced/exception.rb', line 15 224 | 225 | def captured_variables 226 | return '' unless binding_infos&.any? 227 | bindings_of_interest = select_binding_infos 228 | EnhancedErrors.format(bindings_of_interest) 229 | rescue 230 | '' 231 | end232 | |
233 |
166 | 167 | 168 | 169 | 3170 | |
171 |
172 | # File 'lib/enhanced/minitest_patch.rb', line 3 173 | 174 | alias_method :original_run_one_method, :run_one_method175 | |
176 |
192 | 193 | 194 | 195 | 5 196 | 6 197 | 7 198 | 8 199 | 9 200 | 10 201 | 11 202 | 12 203 | 13 204 | 14 205 | 15 206 | 16207 | |
208 |
209 | # File 'lib/enhanced/minitest_patch.rb', line 5 210 | 211 | def run_one_method(klass, method_name) 212 | EnhancedErrors.start_minitest_binding_capture 213 | result = original_run_one_method(klass, method_name) 214 | ensure 215 | begin 216 | binding_infos = EnhancedErrors.stop_minitest_binding_capture 217 | EnhancedErrors.(result.failures.last, binding_infos) if result.failures.any? 218 | Enhanced::ExceptionContext.clear_all 219 | rescue => e 220 | puts "Ignored error during error enhancement: #{e}" 221 | end 222 | end223 | |
224 |
{ red: 31, green: 32, yellow: 33, blue: 34, purple: 35, cyan: 36, white: 0 }.freeze
"\e[0m".freeze
247 | 248 | 249 | 250 | 20 251 | 21 252 | 22253 | |
254 |
255 | # File 'lib/enhanced/colors.rb', line 20 256 | 257 | def code(num) 258 | "\e[#{num}m".freeze 259 | end260 | |
261 |
277 | 278 | 279 | 280 | 15 281 | 16 282 | 17 283 | 18284 | |
285 |
286 | # File 'lib/enhanced/colors.rb', line 15 287 | 288 | def color(num, string) 289 | return string unless @enabled 290 | "#{code(num)}#{string}#{RESET_CODE}" 291 | end292 | |
293 |
309 | 310 | 311 | 312 | 11 313 | 12 314 | 13315 | |
316 |
317 | # File 'lib/enhanced/colors.rb', line 11 318 | 319 | def enabled=(value) 320 | @enabled = value 321 | end322 | |
323 |
361 | 362 | 363 | 364 | 7 365 | 8 366 | 9367 | |
368 |
369 | # File 'lib/enhanced/colors.rb', line 7 370 | 371 | def enabled? 372 | @enabled 373 | end374 | |
375 |
{}
Monitor.new
241 | 242 | 243 | 244 | 42 245 | 43 246 | 44 247 | 45 248 | 46249 | |
250 |
251 | # File 'lib/enhanced/enhanced_exception_context.rb', line 42 252 | 253 | def clear_all 254 | MUTEX.synchronize do 255 | REGISTRY.clear 256 | end 257 | end258 | |
259 |
275 | 276 | 277 | 278 | 36 279 | 37 280 | 38 281 | 39 282 | 40283 | |
284 |
285 | # File 'lib/enhanced/enhanced_exception_context.rb', line 36 286 | 287 | def clear_context(exception) 288 | MUTEX.synchronize do 289 | REGISTRY.delete(exception.object_id) 290 | end 291 | end292 | |
293 |
309 | 310 | 311 | 312 | 20 313 | 21 314 | 22 315 | 23 316 | 24 317 | 25 318 | 26 319 | 27 320 | 28 321 | 29 322 | 30 323 | 31 324 | 32 325 | 33 326 | 34327 | |
328 |
329 | # File 'lib/enhanced/enhanced_exception_context.rb', line 20 330 | 331 | def context_for(exception) 332 | MUTEX.synchronize do 333 | entry = REGISTRY[exception.object_id] 334 | return nil unless entry 335 | 336 | begin 337 | _ = entry[:weak_exc].__getobj__ # ensure exception is still alive 338 | entry[:context] 339 | rescue RefError 340 | # Exception no longer alive, clean up 341 | REGISTRY.delete(exception.object_id) 342 | nil 343 | end 344 | end 345 | end346 | |
347 |
363 | 364 | 365 | 366 | 14 367 | 15 368 | 16 369 | 17 370 | 18371 | |
372 |
373 | # File 'lib/enhanced/enhanced_exception_context.rb', line 14 374 | 375 | def store_context(exception, context) 376 | MUTEX.synchronize do 377 | REGISTRY[exception.object_id] = { weak_exc: WeakRef.new(exception), context: context } 378 | end 379 | end380 | |
381 |
{}
Monitor.new
241 | 242 | 243 | 244 | 43 245 | 44 246 | 45 247 | 46 248 | 47249 | |
250 |
251 | # File 'lib/enhanced/exception_context.rb', line 43 252 | 253 | def clear_all 254 | MUTEX.synchronize do 255 | REGISTRY.clear 256 | end 257 | end258 | |
259 |
275 | 276 | 277 | 278 | 37 279 | 38 280 | 39 281 | 40 282 | 41283 | |
284 |
285 | # File 'lib/enhanced/exception_context.rb', line 37 286 | 287 | def clear_context(exception) 288 | MUTEX.synchronize do 289 | REGISTRY.delete(exception.object_id) 290 | end 291 | end292 | |
293 |
309 | 310 | 311 | 312 | 21 313 | 22 314 | 23 315 | 24 316 | 25 317 | 26 318 | 27 319 | 28 320 | 29 321 | 30 322 | 31 323 | 32 324 | 33 325 | 34 326 | 35327 | |
328 |
329 | # File 'lib/enhanced/exception_context.rb', line 21 330 | 331 | def context_for(exception) 332 | MUTEX.synchronize do 333 | entry = REGISTRY[exception.object_id] 334 | return nil unless entry 335 | 336 | begin 337 | _ = entry[:weak_exc].__getobj__ # ensure exception is still alive 338 | entry[:context] 339 | rescue RefError 340 | # Exception no longer alive, clean up 341 | REGISTRY.delete(exception.object_id) 342 | nil 343 | end 344 | end 345 | end346 | |
347 |
363 | 364 | 365 | 366 | 15 367 | 16 368 | 17 369 | 18 370 | 19371 | |
372 |
373 | # File 'lib/enhanced/exception_context.rb', line 15 374 | 375 | def store_context(exception, context) 376 | MUTEX.synchronize do 377 | REGISTRY[exception.object_id] = { weak_exc: WeakRef.new(exception), context: context } 378 | end 379 | end380 | |
381 |
34 |
35 |
36 | The RSpec test-time only approach constrained only to test-time.
37 |
38 | ### RSpec Setup
39 |
40 | Use EnhancedErrors with RSpec for test-specific exception capturing, ideal for CI and local testing without impacting production.
41 |
42 | ```ruby
43 | # usually in spec_helper.rb or rails_helper.rb
44 |
45 | require 'enhanced_errors'
46 | require 'awesome_print' # Optional, for better output
47 |
48 | RSpec.configure do |config|
49 |
50 | # Add this config to RSpec to enhance your output
51 | # Consider driving the config with an environment variable to make it configurable per-user or run:
52 | # if ENV['enhanced_errors'] == 'true'
53 | config.before(:example) do |_example|
54 | EnhancedErrors.start_rspec_binding_capture
55 | end
56 |
57 | config.after(:example) do |example|
58 | EnhancedErrors.override_rspec_message(example, EnhancedErrors.stop_rspec_binding_capture)
59 | end
60 | # end
61 |
62 | end
63 | ```
64 |
65 |
136 |