enhanced_errors 1.0.0 → 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,95 +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
- # 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([
96
33
  :@fixture_cache,
97
34
  :@fixture_cache_key,
@@ -99,12 +36,11 @@ class EnhancedErrors
99
36
  :@connection_subscriber,
100
37
  :@saved_pool_configs,
101
38
  :@loaded_fixtures,
102
- :@matcher_definitions
39
+ :@matcher_definitions,
40
+ :@__memoized, # Added to skip from output
41
+ :@__inspect_output # Added to skip from output
103
42
  ])
104
43
 
105
- # A set of Rails-specific instance variables to skip.
106
- #
107
- # @return [Set<Symbol>]
108
44
  RAILS_SKIP_LIST = Set.new([
109
45
  :@new_record,
110
46
  :@attributes,
@@ -126,10 +62,8 @@ class EnhancedErrors
126
62
  :@arel_table
127
63
  ])
128
64
 
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.
65
+ @enabled = false
66
+
133
67
  def max_length(value = nil)
134
68
  if value.nil?
135
69
  @max_length ||= DEFAULT_MAX_LENGTH
@@ -139,23 +73,6 @@ class EnhancedErrors
139
73
  @max_length
140
74
  end
141
75
 
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
76
  def capture_rescue(value = nil)
160
77
  if value.nil?
161
78
  @capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
@@ -165,45 +82,52 @@ class EnhancedErrors
165
82
  @capture_rescue
166
83
  end
167
84
 
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
85
  def skip_list
172
86
  @skip_list ||= default_skip_list
173
87
  end
174
88
 
175
- # Initializes the default skip list by merging Rails and RSpec specific variables.
176
- #
177
- # @return [Set<Symbol>] The default skip list.
178
89
  def default_skip_list
179
90
  Set.new(RAILS_SKIP_LIST).merge(RSPEC_SKIP_LIST)
180
91
  end
181
92
 
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.
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
+
186
107
  def add_to_skip_list(*vars)
187
108
  skip_list.merge(vars)
188
109
  end
189
110
 
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)
111
+ def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
112
+ require_relative 'enhanced/exception'
113
+
198
114
  @output_format = nil
199
115
  @eligible_for_capture = nil
200
116
  @original_global_variables = nil
117
+ @override_messages = override_messages
118
+
201
119
  if enabled == false
202
120
  @original_global_variables = nil
203
121
  @enabled = false
204
- # Disable TracePoints in all threads
205
- @traces.each_value { |trace| trace.disable } if @traces
122
+ @trace&.disable
123
+ @trace = nil
206
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
+
207
131
  @enabled = true
208
132
  @debug = debug
209
133
  @original_global_variables = global_variables
@@ -215,7 +139,7 @@ class EnhancedErrors
215
139
  elsif respond_to?(key)
216
140
  send(key, value)
217
141
  else
218
- # Ignore unknown options or handle as needed
142
+ # Ignore unknown options
219
143
  end
220
144
  end
221
145
 
@@ -224,26 +148,105 @@ class EnhancedErrors
224
148
 
225
149
  validate_and_set_capture_events(capture_events)
226
150
 
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)
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
231
159
 
232
- # Set up TracePoint in all existing threads
233
- Thread.list.each do |thread|
234
- next if thread == Thread.current
235
- start_tracing(thread)
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
236
193
  end
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(":")
237
209
 
238
- # Hook into Thread creation to set up TracePoint in new threads
239
- override_thread_new
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
240
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
241
248
  end
242
249
 
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
250
  def eligible_for_capture(&block)
248
251
  if block_given?
249
252
  @eligible_for_capture = block
@@ -252,10 +255,6 @@ class EnhancedErrors
252
255
  end
253
256
  end
254
257
 
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
258
  def on_capture(&block)
260
259
  if block_given?
261
260
  @on_capture_hook = block
@@ -264,18 +263,10 @@ class EnhancedErrors
264
263
  end
265
264
  end
266
265
 
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
266
  def on_capture=(value)
272
267
  self.on_capture_hook = value
273
268
  end
274
269
 
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
270
  def on_format(&block)
280
271
  if block_given?
281
272
  @on_format_hook = block
@@ -284,50 +275,39 @@ class EnhancedErrors
284
275
  end
285
276
  end
286
277
 
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
278
  def on_format=(value)
292
279
  @on_format_hook = value
293
280
  end
294
281
 
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)
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
+
302
290
  if @on_format_hook
303
291
  begin
304
292
  result = @on_format_hook.call(result)
305
- rescue => e
306
- # Since the on_format_hook failed, do not display the data
293
+ rescue
307
294
  result = ''
308
- # Optionally, log the error safely if logging is guaranteed not to raise exceptions
309
295
  end
310
296
  else
311
297
  result = default_on_format(result)
312
298
  end
299
+
313
300
  result
314
301
  end
315
302
 
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
303
  def binding_infos_array_to_string(captured_bindings, format = :terminal)
304
+ return '' if captured_bindings.nil? || captured_bindings.empty?
305
+ captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
322
306
  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")
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")}"
326
309
  end
327
310
 
328
- # Determines the default output format based on the current environment.
329
- #
330
- # @return [Symbol] The default format (:json, :plaintext, :terminal).
331
311
  def get_default_format_for_environment
332
312
  return @output_format unless @output_format.nil?
333
313
  env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
@@ -345,9 +325,6 @@ class EnhancedErrors
345
325
  end
346
326
  end
347
327
 
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
328
  def running_in_ci?
352
329
  return @running_in_ci if defined?(@running_in_ci)
353
330
  ci_env_vars = {
@@ -362,10 +339,6 @@ class EnhancedErrors
362
339
  @running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
363
340
  end
364
341
 
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
342
  def apply_skip_list(binding_info)
370
343
  unless @debug
371
344
  variables = binding_info[:variables]
@@ -376,27 +349,22 @@ class EnhancedErrors
376
349
  binding_info
377
350
  end
378
351
 
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
352
  def validate_binding_format(binding_info)
384
353
  unless binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
385
- # Log or handle the invalid format as needed
386
354
  return nil
387
355
  end
388
356
  binding_info
389
357
  end
390
358
 
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
359
  def binding_info_string(binding_info)
360
+ exception = safe_to_s(binding_info[:exception])
396
361
  capture_event = safe_to_s(binding_info[:capture_event]).capitalize
397
362
  source = safe_to_s(binding_info[:source])
398
- result = "#{Colors.red(capture_event)}: #{Colors.blue(source)}"
399
-
363
+ result = ''
364
+ unless exception.to_s == 'NoException'
365
+ origination = "#{capture_event.capitalize}d"
366
+ result += "#{Colors.green(origination)}: #{Colors.blue(source)}"
367
+ end
400
368
  method_desc = method_and_args_desc(binding_info[:method_and_args])
401
369
  result += method_desc
402
370
 
@@ -412,6 +380,7 @@ class EnhancedErrors
412
380
  result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
413
381
  end
414
382
 
383
+ # Display let variables for RSpec captures
415
384
  if variables[:lets] && !variables[:lets].empty?
416
385
  result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
417
386
  end
@@ -425,161 +394,105 @@ class EnhancedErrors
425
394
  end
426
395
  result + "\n"
427
396
  rescue => e
428
- # Avoid raising exceptions during formatting
429
- return ''
397
+ puts "#{e.message}"
398
+ ''
430
399
  end
431
400
 
432
401
  private
433
402
 
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
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)
451
409
 
452
- binding_context = tp.binding
410
+ unless capture_me
411
+ Thread.current[:enhanced_errors_processing] = false
412
+ return
413
+ end
453
414
 
454
- unless exception.instance_variable_defined?(:@binding_infos)
455
- exception.instance_variable_set(:@binding_infos, [])
456
- exception.extend(ErrorEnhancements)
457
- 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
+ }
458
421
 
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
- }
422
+ locals = binding_context.local_variables.map { |var|
423
+ [var, safe_local_variable_get(binding_context, var)]
424
+ }.to_h
464
425
 
465
- locals = binding_context.local_variables.map { |var|
466
- [var, safe_local_variable_get(binding_context, var)]
467
- }.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
468
430
 
469
- instance_vars = binding_context.receiver.instance_variables
431
+ # No let variables for exceptions
432
+ lets = {}
470
433
 
471
- instances = instance_vars.map { |var|
472
- [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)]
473
438
  }.to_h
439
+ end
474
440
 
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
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
+ }
492
459
 
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
460
+ binding_info = default_on_capture(binding_info)
526
461
 
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
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
533
470
  end
534
- rescue => e
535
- # Avoid any code here that could raise exceptions
536
- ensure
537
- Thread.current[:enhanced_errors_processing] = false
538
471
  end
539
472
 
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
473
+ if binding_info
474
+ binding_info = validate_binding_format(binding_info)
550
475
 
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
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)
559
479
  end
560
480
 
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
481
+ if binding_info
482
+ exception.binding_infos << binding_info
483
+ override_exception_message(exception, exception.binding_infos) if @override_messages
568
484
  end
569
485
  end
486
+ rescue
487
+ # Avoid raising exceptions here
488
+ ensure
489
+ Thread.current[:enhanced_errors_processing] = false
570
490
  end
571
491
 
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
492
  def ignored_exception?(exception)
577
- IGNORED_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
493
+ IGNORED_EXCEPTIONS.include?(exception.class.name)
578
494
  end
579
495
 
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
496
  def test_name
584
497
  if defined?(RSpec)
585
498
  RSpec&.current_example&.full_description
@@ -588,9 +501,6 @@ class EnhancedErrors
588
501
  nil
589
502
  end
590
503
 
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
504
  def default_capture_events
595
505
  events = [:raise]
596
506
  if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
@@ -599,10 +509,6 @@ class EnhancedErrors
599
509
  Set.new(events)
600
510
  end
601
511
 
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
512
  def validate_and_set_capture_events(capture_events)
607
513
  if capture_events.nil?
608
514
  @capture_events = default_capture_events
@@ -616,12 +522,12 @@ class EnhancedErrors
616
522
  end
617
523
 
618
524
  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."
525
+ puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
620
526
  capture_events = capture_events - [:rescue]
621
527
  end
622
528
 
623
529
  if capture_events.empty?
624
- 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."
625
531
  @capture_events = default_capture_events
626
532
  return
627
533
  end
@@ -629,21 +535,12 @@ class EnhancedErrors
629
535
  @capture_events = capture_events.to_set
630
536
  end
631
537
 
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
538
  def valid_capture_events?(capture_events)
637
539
  return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
638
540
  valid_types = [:raise, :rescue].to_set
639
541
  capture_events.to_set.subset?(valid_types)
640
542
  end
641
543
 
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
544
  def extract_arguments(tp, method_name)
648
545
  return '' unless method_name
649
546
  begin
@@ -653,7 +550,7 @@ class EnhancedErrors
653
550
  parameters = method_obj.parameters
654
551
  locals = bind.local_variables
655
552
 
656
- parameters.map do |(type, name)|
553
+ parameters.map do |(_, name)|
657
554
  value = locals.include?(name) ? safe_local_variable_get(bind, name) : nil
658
555
  "#{name}=#{safe_inspect(value)}"
659
556
  rescue => e
@@ -664,11 +561,6 @@ class EnhancedErrors
664
561
  end
665
562
  end
666
563
 
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
564
  def determine_object_name(tp, method_name)
673
565
  if tp.self.is_a?(Class) && tp.self.singleton_class == tp.defined_class
674
566
  "#{safe_to_s(tp.self)}.#{method_name}"
@@ -679,22 +571,12 @@ class EnhancedErrors
679
571
  "#<Error inspecting value: #{e.message}>"
680
572
  end
681
573
 
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
574
  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
575
+ var.is_a?(Symbol) ? eval("#{var}") : nil
576
+ rescue => e
577
+ "#<Error getting value for #{var}>"
692
578
  end
693
579
 
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
580
  def method_and_args_desc(method_info)
699
581
  object_name = safe_to_s(method_info[:object_name])
700
582
  args = safe_to_s(method_info[:args])
@@ -702,26 +584,18 @@ class EnhancedErrors
702
584
  arg_str = args.empty? ? '' : "(#{args})"
703
585
  str = object_name + arg_str
704
586
  "\n#{Colors.green('Method: ')}#{Colors.blue(str)}\n"
705
- rescue => e
587
+ rescue
706
588
  ''
707
589
  end
708
590
 
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
591
  def variable_description(vars_hash)
714
592
  vars_hash.map do |name, value|
715
593
  " #{Colors.purple(name)}: #{format_variable(value)}\n"
716
594
  end.join
717
- rescue => e
595
+ rescue
718
596
  ''
719
597
  end
720
598
 
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
599
  def format_variable(variable)
726
600
  if awesome_print_available? && Colors.enabled?
727
601
  variable.ai
@@ -730,35 +604,24 @@ class EnhancedErrors
730
604
  end
731
605
  rescue => e
732
606
  var_str = safe_to_s(variable)
733
- "#{var_str}: [Inspection Error]"
607
+ "#{var_str}: [Inspection Error #{e.message}]"
734
608
  end
735
609
 
736
- # Checks if the `AwesomePrint` gem is available.
737
- #
738
- # @return [Boolean] `true` if `AwesomePrint` is available, otherwise `false`.
739
610
  def awesome_print_available?
740
611
  return @awesome_print_available unless @awesome_print_available.nil?
741
612
  @awesome_print_available = defined?(AwesomePrint)
742
613
  end
743
614
 
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
615
  def safe_inspect(variable)
749
616
  variable.inspect
750
- rescue => e
617
+ rescue
751
618
  safe_to_s(variable)
752
619
  end
753
620
 
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
621
  def safe_to_s(variable)
759
622
  str = variable.to_s
760
- if str.length > 120
761
- str[0...120] + '...'
623
+ if str.length > 140
624
+ str[0...140] + '...'
762
625
  else
763
626
  str
764
627
  end
@@ -766,53 +629,30 @@ class EnhancedErrors
766
629
  "[Unprintable variable]"
767
630
  end
768
631
 
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
632
  def safe_local_variable_get(binding_context, var_name)
775
633
  binding_context.local_variable_get(var_name)
776
634
  rescue
777
635
  "[Error accessing local variable #{var_name}]"
778
636
  end
779
637
 
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
638
  def safe_instance_variable_get(obj, var_name)
786
639
  obj.instance_variable_get(var_name)
787
640
  rescue
788
641
  "[Error accessing instance variable #{var_name}]"
789
642
  end
790
643
 
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
644
  def default_on_format(string)
796
645
  string
797
646
  end
798
647
 
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
648
  def default_on_capture(binding_info)
804
- # Use this to clean up the captured bindings
805
649
  EnhancedErrors.apply_skip_list(binding_info)
806
650
  end
807
651
 
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
652
  def default_eligible_for_capture(exception)
813
- true
653
+ ignored = ignored_exception?(exception)
654
+ rspec = exception.class.name.start_with?('RSpec::Matchers')
655
+ !ignored && !rspec
814
656
  end
815
-
816
- @enabled = false
817
657
  end
818
658
  end