enhanced_errors 1.0.0 → 2.0.1

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.
@@ -3,108 +3,47 @@
3
3
  require 'set'
4
4
  require 'json'
5
5
 
6
- require_relative 'colors'
7
- require_relative 'error_enhancements'
8
- require_relative 'binding'
9
-
10
- # Exception class names to ignore. Using strings to avoid uninitialized constant errors.
11
- IGNORED_EXCEPTION_NAMES = %w[SystemExit NoMemoryError SignalException Interrupt
12
- ScriptError LoadError NotImplementedError SyntaxError
13
- SystemStackError Psych::BadAlias]
14
-
15
- # Helper method to safely resolve class names to constants
16
- def resolve_exception_class(name)
17
- names = name.split('::')
18
- names.inject(Object) do |mod, name_part|
19
- if mod.const_defined?(name_part, false)
20
- mod.const_get(name_part)
21
- else
22
- return nil
23
- end
24
- end
25
- rescue NameError
26
- nil
27
- end
6
+ require_relative 'enhanced/integrations/rspec_error_failure_message'
7
+ require_relative 'enhanced/colors'
8
+
9
+ IGNORED_EXCEPTIONS = %w[SystemExit NoMemoryError SignalException Interrupt
10
+ ScriptError LoadError NotImplementedError SyntaxError
11
+ RSpec::Expectations::ExpectationNotMetError
12
+ RSpec::Matchers::BuiltIn::RaiseError
13
+ SystemStackError Psych::BadAlias]
28
14
 
29
- # Attempt to resolve the exception classes, ignoring any that are not defined
30
- IGNORED_EXCEPTIONS = IGNORED_EXCEPTION_NAMES.map { |name| resolve_exception_class(name) }.compact
31
15
 
32
- # The EnhancedErrors class provides mechanisms to enhance exception handling by capturing
33
- # additional context such as binding information, variables, and method arguments when exceptions are raised.
34
- # It offers customization options for formatting and filtering captured data.
35
16
  class EnhancedErrors
17
+ extend ::Enhanced
18
+
36
19
  class << self
37
- # Indicates whether EnhancedErrors is enabled.
38
- #
39
- # @return [Boolean]
40
- attr_accessor :enabled
41
-
42
- # The TracePoint objects used for tracing exceptions per thread.
43
- #
44
- # @return [Hash{Thread => TracePoint}]
45
- attr_accessor :traces
46
-
47
- # The configuration block provided during enhancement.
48
- #
49
- # @return [Proc, nil]
50
- attr_accessor :config_block
51
-
52
- # The maximum length of the formatted exception message.
53
- #
54
- # @return [Integer]
55
- attr_accessor :max_length
56
-
57
- # Hook to modify binding information upon capture.
58
- #
59
- # @return [Proc, nil]
60
- attr_accessor :on_capture_hook
61
-
62
- # Determines whether RSpec `let` variables are captured.
63
- #
64
- # @return [Boolean]
65
- attr_accessor :capture_let_variables
66
-
67
- # A proc that determines if an exception is eligible for capture.
68
- #
69
- # @return [Proc, nil]
70
- attr_accessor :eligible_for_capture
71
-
72
- # A set of variable names to exclude from binding information.
73
- #
74
- # @return [Set<Symbol>]
75
- attr_accessor :skip_list
76
-
77
- # Determines whether to capture :rescue events.
78
- #
79
- # @return [Boolean]
80
- attr_accessor :capture_rescue
81
-
82
- # Regular expression to identify gem paths.
83
- #
84
- # @return [Regexp]
85
- GEMS_REGEX = %r{[\/\\]gems[\/\\]}
20
+ attr_accessor :enabled, :config_block, :max_length, :on_capture_hook,
21
+ :eligible_for_capture, :trace, :skip_list, :capture_rescue,
22
+ :override_messages
86
23
 
87
- # The default maximum length for formatted exception messages.
88
- #
89
- # @return [Integer]
24
+ GEMS_REGEX = %r{[\/\\]gems[\/\\]}
90
25
  DEFAULT_MAX_LENGTH = 2500
91
26
 
92
- # A set of RSpec-specific instance variables to skip.
93
- #
94
- # @return [Set<Symbol>]
27
+ # Maximum binding infos we will track per-exception instance. This is intended as an extra
28
+ # safety rail, not a normal scenario.
29
+ MAX_BINDING_INFOS = 10
30
+
31
+ # Add @__memoized and @__inspect_output to the skip list so they don't appear in output
95
32
  RSPEC_SKIP_LIST = Set.new([
33
+ :@example,
96
34
  :@fixture_cache,
97
35
  :@fixture_cache_key,
36
+ :@fixture_connections,
98
37
  :@fixture_connection_pools,
38
+ :@loaded_fixtures,
99
39
  :@connection_subscriber,
100
40
  :@saved_pool_configs,
101
- :@loaded_fixtures,
102
- :@matcher_definitions
41
+ :@legacy_saved_pool_configs,
42
+ :@matcher_definitions,
43
+ :@__memoized,
44
+ :@__inspect_output
103
45
  ])
104
46
 
105
- # A set of Rails-specific instance variables to skip.
106
- #
107
- # @return [Set<Symbol>]
108
47
  RAILS_SKIP_LIST = Set.new([
109
48
  :@new_record,
110
49
  :@attributes,
@@ -126,10 +65,8 @@ class EnhancedErrors
126
65
  :@arel_table
127
66
  ])
128
67
 
129
- # Gets or sets the maximum length for the formatted exception message.
130
- #
131
- # @param value [Integer, nil] The desired maximum length. If `nil`, returns the current value.
132
- # @return [Integer] The maximum length for the formatted message.
68
+ @enabled = false
69
+
133
70
  def max_length(value = nil)
134
71
  if value.nil?
135
72
  @max_length ||= DEFAULT_MAX_LENGTH
@@ -139,23 +76,6 @@ class EnhancedErrors
139
76
  @max_length
140
77
  end
141
78
 
142
- # Gets or sets whether to capture RSpec `let` variables.
143
- #
144
- # @param value [Boolean, nil] The desired state. If `nil`, returns the current value.
145
- # @return [Boolean] Whether RSpec `let` variables are being captured.
146
- def capture_let_variables(value = nil)
147
- if value.nil?
148
- @capture_let_variables = @capture_let_variables.nil? ? true : @capture_let_variables
149
- else
150
- @capture_let_variables = value
151
- end
152
- @capture_let_variables
153
- end
154
-
155
- # Gets or sets whether to capture :rescue events.
156
- #
157
- # @param value [Boolean, nil] The desired state. If `nil`, returns the current value.
158
- # @return [Boolean] Whether :rescue events are being captured.
159
79
  def capture_rescue(value = nil)
160
80
  if value.nil?
161
81
  @capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
@@ -165,45 +85,52 @@ class EnhancedErrors
165
85
  @capture_rescue
166
86
  end
167
87
 
168
- # Retrieves the current skip list, initializing it with default values if not already set.
169
- #
170
- # @return [Set<Symbol>] The current skip list.
171
88
  def skip_list
172
89
  @skip_list ||= default_skip_list
173
90
  end
174
91
 
175
- # Initializes the default skip list by merging Rails and RSpec specific variables.
176
- #
177
- # @return [Set<Symbol>] The default skip list.
178
92
  def default_skip_list
179
93
  Set.new(RAILS_SKIP_LIST).merge(RSPEC_SKIP_LIST)
180
94
  end
181
95
 
182
- # Adds variables to the skip list to exclude them from binding information.
183
- #
184
- # @param vars [Symbol] The variable names to add to the skip list.
185
- # @return [Set<Symbol>] The updated skip list.
96
+ # takes an exception and bindings, calculates the variables message
97
+ # and modifies the exceptions .message to display the variables
98
+ def override_exception_message(exception, binding_or_bindings)
99
+ # Ensure binding_or_bindings is always an array for compatibility
100
+ return exception if binding_or_bindings.nil? || binding_or_bindings.empty? || exception.respond_to?(:unaltered_message)
101
+ variable_str = EnhancedErrors.format(binding_or_bindings)
102
+ message_str = exception.message
103
+ exception.define_singleton_method(:unaltered_message) { message_str }
104
+ exception.define_singleton_method(:message) do
105
+ "#{message_str}#{variable_str}"
106
+ end
107
+ exception
108
+ end
109
+
186
110
  def add_to_skip_list(*vars)
187
111
  skip_list.merge(vars)
188
112
  end
189
113
 
190
- # Enhances the exception handling by setting up tracing and configuration options.
191
- #
192
- # @param enabled [Boolean] Whether to enable EnhancedErrors.
193
- # @param debug [Boolean] Whether to enable debug mode.
194
- # @param options [Hash] Additional configuration options.
195
- # @yield [void] A block for additional configuration.
196
- # @return [void]
197
- def enhance!(enabled: true, debug: false, capture_events: nil, **options, &block)
114
+ def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
115
+ require_relative 'enhanced/exception'
116
+
198
117
  @output_format = nil
199
118
  @eligible_for_capture = nil
200
119
  @original_global_variables = nil
120
+ @override_messages = override_messages
121
+
201
122
  if enabled == false
202
123
  @original_global_variables = nil
203
124
  @enabled = false
204
- # Disable TracePoints in all threads
205
- @traces.each_value { |trace| trace.disable } if @traces
125
+ @trace&.disable
126
+ @trace = nil
206
127
  else
128
+ # if there's an old one, disable it before replacing it
129
+ # this seems to actually matter, although it seems like it
130
+ # shouldn't
131
+ @trace&.disable
132
+ @trace = nil
133
+
207
134
  @enabled = true
208
135
  @debug = debug
209
136
  @original_global_variables = global_variables
@@ -215,7 +142,7 @@ class EnhancedErrors
215
142
  elsif respond_to?(key)
216
143
  send(key, value)
217
144
  else
218
- # Ignore unknown options or handle as needed
145
+ # Ignore unknown options
219
146
  end
220
147
  end
221
148
 
@@ -224,26 +151,110 @@ class EnhancedErrors
224
151
 
225
152
  validate_and_set_capture_events(capture_events)
226
153
 
227
- # Initialize @traces hash to keep track of TracePoints per thread
228
- @traces ||= {}
229
- # Set up TracePoint in the main thread
230
- start_tracing(Thread.current)
154
+ events = @capture_events ? @capture_events.to_a : default_capture_events
155
+ @trace = TracePoint.new(*events) do |tp|
156
+ handle_tracepoint_event(tp)
157
+ end
158
+
159
+ @trace.enable
160
+ end
161
+ end
162
+
163
+ def safe_prepend_module(target_class, mod)
164
+ if defined?(target_class) && target_class.is_a?(Module)
165
+ target_class.prepend(mod)
166
+ true
167
+ else
168
+ false
169
+ end
170
+ end
171
+
172
+ def safely_prepend_rspec_custom_failure_message
173
+ return if @rspec_failure_message_loaded
174
+ if defined?(RSpec::Core::Example)
175
+ RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
176
+ @rspec_failure_message_loaded = true
177
+ else
178
+
179
+ end
180
+ rescue => e
181
+ puts "Failed "
182
+ end
183
+
184
+ def start_rspec_binding_capture
185
+ @rspec_example_binding = nil
231
186
 
232
- # Set up TracePoint in all existing threads
233
- Thread.list.each do |thread|
234
- next if thread == Thread.current
235
- start_tracing(thread)
187
+ # In the Exception binding infos, I observed that re-setting
188
+ # the tracepoint without disabling it seemed to accumulate traces
189
+ # in the test suite where things are disabled and re-enabled often.
190
+ @rspec_tracepoint&.disable
191
+ @rspec_tracepoint = nil
192
+
193
+ @rspec_tracepoint = TracePoint.new(:b_return) do |tp|
194
+ # This is super-kluge-y and should be replaced with... something TBD
195
+ if tp.self.class.name =~ /RSpec::ExampleGroups::[A-Z0-9]+.*/ &&
196
+ tp.method_id.nil? &&
197
+ !(tp.path =~ /rspec/) &&
198
+ tp.path =~ /_spec\.rb$/
199
+ @rspec_example_binding = tp.binding
236
200
  end
201
+ end
202
+ @rspec_tracepoint.enable
203
+ end
204
+
205
+ def stop_rspec_binding_capture
206
+ @rspec_tracepoint&.disable
207
+ @rspec_tracepoint = nil
208
+ binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
209
+ @rspec_example_binding = nil
210
+ binding_info
211
+ end
237
212
 
238
- # Hook into Thread creation to set up TracePoint in new threads
239
- override_thread_new
213
+ def convert_binding_to_binding_info(b, capture_let_variables: true)
214
+ file = b.eval("__FILE__") rescue nil
215
+ line = b.eval("__LINE__") rescue nil
216
+ location = [file, line].compact.join(":")
217
+
218
+ locals = b.local_variables.map { |var| [var, safe_local_variable_get(b, var)] }.to_h
219
+ receiver = b.receiver
220
+ instance_vars = receiver.instance_variables
221
+ instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
222
+
223
+ # Capture let variables only for RSpec captures
224
+ lets = {}
225
+ if capture_let_variables && instance_vars.include?(:@__memoized)
226
+ outer_memoized = receiver.instance_variable_get(:@__memoized)
227
+ memoized = outer_memoized.instance_variable_get(:@memoized) if outer_memoized.respond_to?(:instance_variable_get)
228
+ if memoized.is_a?(Hash)
229
+ lets = memoized.transform_keys(&:to_sym)
230
+ end
240
231
  end
232
+
233
+ binding_info = {
234
+ source: location,
235
+ object: receiver,
236
+ library: !!GEMS_REGEX.match?(location.to_s),
237
+ method_and_args: {
238
+ object_name: '',
239
+ args: ''
240
+ },
241
+ test_name: test_name,
242
+ variables: {
243
+ locals: locals,
244
+ instances: instances,
245
+ lets: lets,
246
+ globals: {}
247
+ },
248
+ exception: 'NoException',
249
+ capture_event: 'RSpecContext'
250
+ }
251
+
252
+ # Apply skip list to remove @__memoized and @__inspect_output from output
253
+ # but only after extracting let variables.
254
+ binding_info = default_on_capture(binding_info)
255
+ binding_info
241
256
  end
242
257
 
243
- # Sets or retrieves the eligibility criteria for capturing exceptions.
244
- #
245
- # @yieldparam exception [Exception] The exception to evaluate.
246
- # @return [Proc] The current eligibility proc.
247
258
  def eligible_for_capture(&block)
248
259
  if block_given?
249
260
  @eligible_for_capture = block
@@ -252,10 +263,6 @@ class EnhancedErrors
252
263
  end
253
264
  end
254
265
 
255
- # Sets or retrieves the hook to modify binding information upon capture.
256
- #
257
- # @yieldparam binding_info [Hash] The binding information captured.
258
- # @return [Proc] The current on_capture hook.
259
266
  def on_capture(&block)
260
267
  if block_given?
261
268
  @on_capture_hook = block
@@ -264,18 +271,10 @@ class EnhancedErrors
264
271
  end
265
272
  end
266
273
 
267
- # Sets the on_capture hook.
268
- #
269
- # @param value [Proc] The proc to set as the on_capture hook.
270
- # @return [Proc] The newly set on_capture hook.
271
274
  def on_capture=(value)
272
275
  self.on_capture_hook = value
273
276
  end
274
277
 
275
- # Sets or retrieves the hook to modify formatted exception messages.
276
- #
277
- # @yieldparam formatted_string [String] The formatted exception message.
278
- # @return [Proc] The current on_format hook.
279
278
  def on_format(&block)
280
279
  if block_given?
281
280
  @on_format_hook = block
@@ -284,50 +283,39 @@ class EnhancedErrors
284
283
  end
285
284
  end
286
285
 
287
- # Sets the on_format hook.
288
- #
289
- # @param value [Proc] The proc to set as the on_format hook.
290
- # @return [Proc] The newly set on_format hook.
291
286
  def on_format=(value)
292
287
  @on_format_hook = value
293
288
  end
294
289
 
295
- # Formats the captured binding information into a string based on the specified format.
296
- #
297
- # @param captured_bindings [Array<Hash>] The array of captured binding information.
298
- # @param output_format [Symbol] The format to use (:json, :plaintext, :terminal).
299
- # @return [String] The formatted exception message.
300
- def format(captured_bindings = [], output_format = get_default_format_for_environment)
301
- result = binding_infos_array_to_string(captured_bindings, output_format)
290
+ def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
291
+ return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
292
+
293
+ # If captured_binding_infos is already an array, use it directly; otherwise, wrap it in an array.
294
+ binding_infos = captured_binding_infos.is_a?(Array) ? captured_binding_infos : [captured_binding_infos]
295
+
296
+ result = binding_infos_array_to_string(binding_infos, output_format)
297
+
302
298
  if @on_format_hook
303
299
  begin
304
300
  result = @on_format_hook.call(result)
305
- rescue => e
306
- # Since the on_format_hook failed, do not display the data
301
+ rescue
307
302
  result = ''
308
- # Optionally, log the error safely if logging is guaranteed not to raise exceptions
309
303
  end
310
304
  else
311
305
  result = default_on_format(result)
312
306
  end
307
+
313
308
  result
314
309
  end
315
310
 
316
- # Converts an array of binding information hashes into a formatted string.
317
- #
318
- # @param captured_bindings [Array<Hash>] The array of binding information.
319
- # @param format [Symbol] The format to use (:json, :plaintext, :terminal).
320
- # @return [String] The formatted string representation of the binding information.
321
311
  def binding_infos_array_to_string(captured_bindings, format = :terminal)
312
+ return '' if captured_bindings.nil? || captured_bindings.empty?
313
+ captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
322
314
  Colors.enabled = format == :terminal
323
- formatted_bindings = captured_bindings.map { |binding_info| binding_info_string(binding_info) }
324
-
325
- format == :json ? JSON.pretty_generate(captured_bindings) : formatted_bindings.join("\n")
315
+ formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
316
+ format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
326
317
  end
327
318
 
328
- # Determines the default output format based on the current environment.
329
- #
330
- # @return [Symbol] The default format (:json, :plaintext, :terminal).
331
319
  def get_default_format_for_environment
332
320
  return @output_format unless @output_format.nil?
333
321
  env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
@@ -345,9 +333,6 @@ class EnhancedErrors
345
333
  end
346
334
  end
347
335
 
348
- # Checks if the code is running in a Continuous Integration (CI) environment.
349
- #
350
- # @return [Boolean] `true` if running in CI, otherwise `false`.
351
336
  def running_in_ci?
352
337
  return @running_in_ci if defined?(@running_in_ci)
353
338
  ci_env_vars = {
@@ -362,10 +347,6 @@ class EnhancedErrors
362
347
  @running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
363
348
  end
364
349
 
365
- # Applies the skip list to the captured binding information, excluding specified variables.
366
- #
367
- # @param binding_info [Hash] The binding information to filter.
368
- # @return [Hash] The filtered binding information.
369
350
  def apply_skip_list(binding_info)
370
351
  unless @debug
371
352
  variables = binding_info[:variables]
@@ -376,27 +357,22 @@ class EnhancedErrors
376
357
  binding_info
377
358
  end
378
359
 
379
- # Validates the format of the captured binding information.
380
- #
381
- # @param binding_info [Hash] The binding information to validate.
382
- # @return [Hash, nil] The validated binding information or `nil` if invalid.
383
360
  def validate_binding_format(binding_info)
384
361
  unless binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
385
- # Log or handle the invalid format as needed
386
362
  return nil
387
363
  end
388
364
  binding_info
389
365
  end
390
366
 
391
- # Formats a single binding information hash into a string with colorization.
392
- #
393
- # @param binding_info [Hash] The binding information to format.
394
- # @return [String] The formatted string.
395
367
  def binding_info_string(binding_info)
368
+ exception = safe_to_s(binding_info[:exception])
396
369
  capture_event = safe_to_s(binding_info[:capture_event]).capitalize
397
370
  source = safe_to_s(binding_info[:source])
398
- result = "#{Colors.red(capture_event)}: #{Colors.blue(source)}"
399
-
371
+ result = ''
372
+ unless exception.to_s == 'NoException'
373
+ origination = "#{capture_event.capitalize}d"
374
+ result += "#{Colors.green(origination)}: #{Colors.blue(source)}"
375
+ end
400
376
  method_desc = method_and_args_desc(binding_info[:method_and_args])
401
377
  result += method_desc
402
378
 
@@ -412,6 +388,7 @@ class EnhancedErrors
412
388
  result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
413
389
  end
414
390
 
391
+ # Display let variables for RSpec captures
415
392
  if variables[:lets] && !variables[:lets].empty?
416
393
  result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
417
394
  end
@@ -425,161 +402,105 @@ class EnhancedErrors
425
402
  end
426
403
  result + "\n"
427
404
  rescue => e
428
- # Avoid raising exceptions during formatting
429
- return ''
405
+ puts "#{e.message}"
406
+ ''
430
407
  end
431
408
 
432
409
  private
433
410
 
434
- # Starts the TracePoint for capturing exceptions based on configured events in a specific thread.
435
- #
436
- # @param thread [Thread] The thread to start tracing in.
437
- # @return [void]
438
- def start_tracing(thread)
439
- return if @traces[thread]&.enabled?
440
- events = @capture_events ? @capture_events.to_a : default_capture_events
441
- trace = TracePoint.new(*events) do |tp|
442
- next if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
443
- Thread.current[:enhanced_errors_processing] = true
444
- exception = tp.raised_exception
445
- capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
446
-
447
- unless capture_me
448
- Thread.current[:enhanced_errors_processing] = false
449
- next
450
- end
411
+ def handle_tracepoint_event(tp)
412
+ return unless @enabled
413
+ return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
414
+ Thread.current[:enhanced_errors_processing] = true
415
+ exception = tp.raised_exception
416
+ capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
451
417
 
452
- binding_context = tp.binding
418
+ unless capture_me
419
+ Thread.current[:enhanced_errors_processing] = false
420
+ return
421
+ end
453
422
 
454
- unless exception.instance_variable_defined?(:@binding_infos)
455
- exception.instance_variable_set(:@binding_infos, [])
456
- exception.extend(ErrorEnhancements)
457
- end
423
+ binding_context = tp.binding
424
+ method_name = tp.method_id
425
+ method_and_args = {
426
+ object_name: determine_object_name(tp, method_name),
427
+ args: extract_arguments(tp, method_name)
428
+ }
458
429
 
459
- method_name = tp.method_id
460
- method_and_args = {
461
- object_name: determine_object_name(tp, method_name),
462
- args: extract_arguments(tp, method_name)
463
- }
430
+ locals = binding_context.local_variables.map { |var|
431
+ [var, safe_local_variable_get(binding_context, var)]
432
+ }.to_h
464
433
 
465
- locals = binding_context.local_variables.map { |var|
466
- [var, safe_local_variable_get(binding_context, var)]
467
- }.to_h
434
+ instance_vars = binding_context.receiver.instance_variables
435
+ instances = instance_vars.map { |var|
436
+ [var, safe_instance_variable_get(binding_context.receiver, var)]
437
+ }.to_h
468
438
 
469
- instance_vars = binding_context.receiver.instance_variables
439
+ # No let variables for exceptions
440
+ lets = {}
470
441
 
471
- instances = instance_vars.map { |var|
472
- [var, safe_instance_variable_get(binding_context.receiver, var)]
442
+ globals = {}
443
+ if @debug
444
+ globals = (global_variables - @original_global_variables).map { |var|
445
+ [var, get_global_variable_value(var)]
473
446
  }.to_h
447
+ end
474
448
 
475
- # Extract 'let' variables from :@__memoized (RSpec specific)
476
- lets = {}
477
- if capture_let_variables && instance_vars.include?(:@__memoized)
478
- outer_memoized = binding_context.receiver.instance_variable_get(:@__memoized)
479
- memoized = outer_memoized.instance_variable_get(:@memoized) if outer_memoized.respond_to?(:instance_variable_get)
480
- if memoized.is_a?(Hash)
481
- lets = memoized&.transform_keys(&:to_sym)
482
- end
483
- end
484
-
485
- globals = {}
486
- # Capture global variables
487
- if @debug
488
- globals = (global_variables - @original_global_variables).map { |var|
489
- [var, get_global_variable_value(var)]
490
- }.to_h
491
- end
449
+ capture_event = safe_to_s(tp.event)
450
+ location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
451
+
452
+ binding_info = {
453
+ source: location,
454
+ object: tp.self,
455
+ library: !!GEMS_REGEX.match?(location),
456
+ method_and_args: method_and_args,
457
+ test_name: test_name,
458
+ variables: {
459
+ locals: locals,
460
+ instances: instances,
461
+ lets: lets,
462
+ globals: globals
463
+ },
464
+ exception: safe_to_s(exception.class.name),
465
+ capture_event: capture_event
466
+ }
492
467
 
493
- capture_event = safe_to_s(tp.event) # 'raise' or 'rescue'
494
- location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
495
-
496
- binding_info = {
497
- source: location,
498
- object: tp.self,
499
- library: !!GEMS_REGEX.match?(location),
500
- method_and_args: method_and_args,
501
- test_name: test_name,
502
- variables: {
503
- locals: locals,
504
- instances: instances,
505
- lets: lets,
506
- globals: globals
507
- },
508
- exception: safe_to_s(exception.class.name),
509
- capture_event: capture_event
510
- }
511
-
512
- binding_info = default_on_capture(binding_info) # Apply default processing
513
-
514
- if on_capture_hook
515
- begin
516
- Thread.current[:on_capture] = true
517
- binding_info = on_capture_hook.call(binding_info)
518
- rescue => e
519
- # Since the on_capture_hook failed, do not capture this binding_info
520
- binding_info = nil
521
- # Optionally, log the error safely if logging is guaranteed not to raise exceptions
522
- ensure
523
- Thread.current[:on_capture] = false
524
- end
525
- end
468
+ binding_info = default_on_capture(binding_info)
526
469
 
527
- # Proceed only if binding_info is valid
528
- if binding_info
529
- binding_info = validate_binding_format(binding_info)
530
- if binding_info
531
- exception.instance_variable_get(:@binding_infos) << binding_info
532
- end
470
+ if on_capture_hook
471
+ begin
472
+ Thread.current[:on_capture] = true
473
+ binding_info = on_capture_hook.call(binding_info)
474
+ rescue
475
+ binding_info = nil
476
+ ensure
477
+ Thread.current[:on_capture] = false
533
478
  end
534
- rescue => e
535
- # Avoid any code here that could raise exceptions
536
- ensure
537
- Thread.current[:enhanced_errors_processing] = false
538
479
  end
539
480
 
540
- @traces[thread] = trace
541
- trace.enable
542
- end
543
-
544
- # Overrides Thread.new and Thread.start to ensure TracePoint is enabled in new threads.
545
- #
546
- # @return [void]
547
- def override_thread_new
548
- return if @thread_overridden
549
- @thread_overridden = true
481
+ if binding_info
482
+ binding_info = validate_binding_format(binding_info)
550
483
 
551
- class << Thread
552
- alias_method :original_new, :new
553
-
554
- def new(*args, &block)
555
- original_new(*args) do |*block_args|
556
- EnhancedErrors.send(:start_tracing, Thread.current)
557
- block.call(*block_args)
558
- end
484
+ if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
485
+ # delete from the middle of the array as the ends are most interesting
486
+ exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
559
487
  end
560
488
 
561
- alias_method :original_start, :start
562
-
563
- def start(*args, &block)
564
- original_start(*args) do |*block_args|
565
- EnhancedErrors.send(:start_tracing, Thread.current)
566
- block.call(*block_args)
567
- end
489
+ if binding_info
490
+ exception.binding_infos << binding_info
491
+ override_exception_message(exception, exception.binding_infos) if @override_messages
568
492
  end
569
493
  end
494
+ rescue
495
+ # Avoid raising exceptions here
496
+ ensure
497
+ Thread.current[:enhanced_errors_processing] = false
570
498
  end
571
499
 
572
- # Checks if the exception is in the ignored exceptions list.
573
- #
574
- # @param exception [Exception] The exception to check.
575
- # @return [Boolean] `true` if the exception should be ignored, otherwise `false`.
576
500
  def ignored_exception?(exception)
577
- IGNORED_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
501
+ IGNORED_EXCEPTIONS.include?(exception.class.name)
578
502
  end
579
503
 
580
- # Retrieves the current test name from RSpec, if available.
581
- #
582
- # @return [String, nil] The current test name or `nil` if not in a test context.
583
504
  def test_name
584
505
  if defined?(RSpec)
585
506
  RSpec&.current_example&.full_description
@@ -588,9 +509,6 @@ class EnhancedErrors
588
509
  nil
589
510
  end
590
511
 
591
- # Helper method to determine the default capture types based on Ruby version
592
- #
593
- # @return [Set<Symbol>] The default set of capture types
594
512
  def default_capture_events
595
513
  events = [:raise]
596
514
  if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
@@ -599,10 +517,6 @@ class EnhancedErrors
599
517
  Set.new(events)
600
518
  end
601
519
 
602
- # Validates and sets the capture events for TracePoint.
603
- #
604
- # @param capture_events [Array<Symbol>, nil] The events to capture.
605
- # @return [void]
606
520
  def validate_and_set_capture_events(capture_events)
607
521
  if capture_events.nil?
608
522
  @capture_events = default_capture_events
@@ -616,12 +530,12 @@ class EnhancedErrors
616
530
  end
617
531
 
618
532
  if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
619
- puts "EnhancedErrors: Warning: :rescue capture_event is not supported in Ruby versions below 3.3.0 and will be ignored."
533
+ puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
620
534
  capture_events = capture_events - [:rescue]
621
535
  end
622
536
 
623
537
  if capture_events.empty?
624
- puts "No valid capture_events provided to EnhancedErrors.enhance! Falling back to defaults."
538
+ puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
625
539
  @capture_events = default_capture_events
626
540
  return
627
541
  end
@@ -629,21 +543,12 @@ class EnhancedErrors
629
543
  @capture_events = capture_events.to_set
630
544
  end
631
545
 
632
- # Validates the capture events.
633
- #
634
- # @param capture_events [Array<Symbol>] The events to validate.
635
- # @return [Boolean] `true` if valid, otherwise `false`.
636
546
  def valid_capture_events?(capture_events)
637
547
  return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
638
548
  valid_types = [:raise, :rescue].to_set
639
549
  capture_events.to_set.subset?(valid_types)
640
550
  end
641
551
 
642
- # Extracts method arguments from the TracePoint binding.
643
- #
644
- # @param tp [TracePoint] The current TracePoint.
645
- # @param method_name [Symbol] The name of the method.
646
- # @return [String] A string representation of the method arguments.
647
552
  def extract_arguments(tp, method_name)
648
553
  return '' unless method_name
649
554
  begin
@@ -653,7 +558,7 @@ class EnhancedErrors
653
558
  parameters = method_obj.parameters
654
559
  locals = bind.local_variables
655
560
 
656
- parameters.map do |(type, name)|
561
+ parameters.map do |(_, name)|
657
562
  value = locals.include?(name) ? safe_local_variable_get(bind, name) : nil
658
563
  "#{name}=#{safe_inspect(value)}"
659
564
  rescue => e
@@ -664,11 +569,6 @@ class EnhancedErrors
664
569
  end
665
570
  end
666
571
 
667
- # Determines the object name based on the TracePoint and method name.
668
- #
669
- # @param tp [TracePoint] The current TracePoint.
670
- # @param method_name [Symbol] The name of the method.
671
- # @return [String] The formatted object name.
672
572
  def determine_object_name(tp, method_name)
673
573
  if tp.self.is_a?(Class) && tp.self.singleton_class == tp.defined_class
674
574
  "#{safe_to_s(tp.self)}.#{method_name}"
@@ -679,22 +579,12 @@ class EnhancedErrors
679
579
  "#<Error inspecting value: #{e.message}>"
680
580
  end
681
581
 
682
- # Retrieves the value of a global variable by its name.
683
- #
684
- # @param var [Symbol] The name of the global variable.
685
- # @return [Object, String] The value of the global variable or an error message.
686
582
  def get_global_variable_value(var)
687
- begin
688
- var.is_a?(Symbol) ? eval("#{var}") : nil
689
- rescue => e
690
- "#<Error getting value for #{var}>"
691
- end
583
+ var.is_a?(Symbol) ? eval("#{var}") : nil
584
+ rescue => e
585
+ "#<Error getting value for #{var}>"
692
586
  end
693
587
 
694
- # Generates a description for method and arguments.
695
- #
696
- # @param method_info [Hash] Information about the method and its arguments.
697
- # @return [String] The formatted description.
698
588
  def method_and_args_desc(method_info)
699
589
  object_name = safe_to_s(method_info[:object_name])
700
590
  args = safe_to_s(method_info[:args])
@@ -702,26 +592,18 @@ class EnhancedErrors
702
592
  arg_str = args.empty? ? '' : "(#{args})"
703
593
  str = object_name + arg_str
704
594
  "\n#{Colors.green('Method: ')}#{Colors.blue(str)}\n"
705
- rescue => e
595
+ rescue
706
596
  ''
707
597
  end
708
598
 
709
- # Generates a formatted description for a set of variables.
710
- #
711
- # @param vars_hash [Hash] A hash of variable names and their values.
712
- # @return [String] The formatted variables description.
713
599
  def variable_description(vars_hash)
714
600
  vars_hash.map do |name, value|
715
601
  " #{Colors.purple(name)}: #{format_variable(value)}\n"
716
602
  end.join
717
- rescue => e
603
+ rescue
718
604
  ''
719
605
  end
720
606
 
721
- # Formats a variable for display, using `awesome_print` if available and enabled.
722
- #
723
- # @param variable [Object] The variable to format.
724
- # @return [String] The formatted variable.
725
607
  def format_variable(variable)
726
608
  if awesome_print_available? && Colors.enabled?
727
609
  variable.ai
@@ -730,35 +612,24 @@ class EnhancedErrors
730
612
  end
731
613
  rescue => e
732
614
  var_str = safe_to_s(variable)
733
- "#{var_str}: [Inspection Error]"
615
+ "#{var_str}: [Inspection Error #{e.message}]"
734
616
  end
735
617
 
736
- # Checks if the `AwesomePrint` gem is available.
737
- #
738
- # @return [Boolean] `true` if `AwesomePrint` is available, otherwise `false`.
739
618
  def awesome_print_available?
740
619
  return @awesome_print_available unless @awesome_print_available.nil?
741
620
  @awesome_print_available = defined?(AwesomePrint)
742
621
  end
743
622
 
744
- # Safely calls `inspect` on a variable.
745
- #
746
- # @param variable [Object] The variable to inspect.
747
- # @return [String] The inspected variable or a safe fallback.
748
623
  def safe_inspect(variable)
749
624
  variable.inspect
750
- rescue => e
625
+ rescue
751
626
  safe_to_s(variable)
752
627
  end
753
628
 
754
- # Safely converts a variable to a string, handling exceptions.
755
- #
756
- # @param variable [Object] The variable to convert.
757
- # @return [String] The string representation or a safe fallback.
758
629
  def safe_to_s(variable)
759
630
  str = variable.to_s
760
- if str.length > 120
761
- str[0...120] + '...'
631
+ if str.length > 140
632
+ str[0...140] + '...'
762
633
  else
763
634
  str
764
635
  end
@@ -766,53 +637,30 @@ class EnhancedErrors
766
637
  "[Unprintable variable]"
767
638
  end
768
639
 
769
- # Safely retrieves a local variable from a binding.
770
- #
771
- # @param binding_context [Binding] The binding context.
772
- # @param var_name [Symbol] The name of the local variable.
773
- # @return [Object] The value of the local variable or a safe fallback.
774
640
  def safe_local_variable_get(binding_context, var_name)
775
641
  binding_context.local_variable_get(var_name)
776
642
  rescue
777
643
  "[Error accessing local variable #{var_name}]"
778
644
  end
779
645
 
780
- # Safely retrieves an instance variable from an object.
781
- #
782
- # @param obj [Object] The object.
783
- # @param var_name [Symbol] The name of the instance variable.
784
- # @return [Object] The value of the instance variable or a safe fallback.
785
646
  def safe_instance_variable_get(obj, var_name)
786
647
  obj.instance_variable_get(var_name)
787
648
  rescue
788
649
  "[Error accessing instance variable #{var_name}]"
789
650
  end
790
651
 
791
- # Default implementation for the on_format hook.
792
- #
793
- # @param string [String] The formatted exception message.
794
- # @return [String] The unmodified exception message.
795
652
  def default_on_format(string)
796
653
  string
797
654
  end
798
655
 
799
- # Default implementation for the on_capture hook, applying the skip list.
800
- #
801
- # @param binding_info [Hash] The captured binding information.
802
- # @return [Hash] The filtered binding information.
803
656
  def default_on_capture(binding_info)
804
- # Use this to clean up the captured bindings
805
657
  EnhancedErrors.apply_skip_list(binding_info)
806
658
  end
807
659
 
808
- # Default eligibility check for capturing exceptions.
809
- #
810
- # @param exception [Exception] The exception to evaluate.
811
- # @return [Boolean] `true` if the exception should be captured, otherwise `false`.
812
660
  def default_eligible_for_capture(exception)
813
- true
661
+ ignored = ignored_exception?(exception)
662
+ rspec = exception.class.name.start_with?('RSpec::Matchers')
663
+ !ignored && !rspec
814
664
  end
815
-
816
- @enabled = false
817
665
  end
818
666
  end