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