enhanced_errors 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,113 +2,200 @@
2
2
 
3
3
  require 'set'
4
4
  require 'json'
5
+ require 'monitor'
5
6
 
6
- require_relative 'enhanced/integrations/rspec_error_failure_message'
7
7
  require_relative 'enhanced/colors'
8
+ require_relative 'enhanced/exception'
8
9
 
9
10
  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]
11
+ ScriptError LoadError NotImplementedError SyntaxError
12
+ RSpec::Expectations::ExpectationNotMetError
13
+ RSpec::Matchers::BuiltIn::RaiseError
14
+ SystemStackError Psych::BadAlias]
14
15
 
15
16
  class EnhancedErrors
16
17
  extend ::Enhanced
17
18
 
18
19
  class << self
19
- attr_accessor :enabled, :config_block, :max_length, :on_capture_hook,
20
- :eligible_for_capture, :trace, :skip_list, :capture_rescue,
21
- :override_messages
20
+ def mutex
21
+ @monitor ||= Monitor.new
22
+ end
23
+
24
+ attr_accessor :enabled, :config_block, :on_capture_hook, :eligible_for_capture, :trace, :override_messages
22
25
 
23
26
  GEMS_REGEX = %r{[\/\\]gems[\/\\]}
24
27
  RSPEC_EXAMPLE_REGEXP = /RSpec::ExampleGroups::[A-Z0-9]+.*/
25
28
  DEFAULT_MAX_LENGTH = 2000
26
-
27
- # Maximum binding infos we will track per-exception instance. This is intended as an extra
28
- # safety rail, not a normal scenario.
29
29
  MAX_BINDING_INFOS = 3
30
30
 
31
- # Add @__memoized and @__inspect_output to the skip list so they don't appear in output
32
- RSPEC_SKIP_LIST = Set.new([
31
+ RSPEC_SKIP_LIST = [
32
+ :@__inspect_output,
33
+ :@__memoized,
34
+ :@assertion_delegator,
35
+ :@assertion_instance,
33
36
  :@assertions,
34
- :@integration_session,
37
+ :@connection_subscriber,
35
38
  :@example,
36
- :@assertion_delegator,
37
39
  :@fixture_cache,
38
40
  :@fixture_cache_key,
39
- :@fixture_connections,
40
41
  :@fixture_connection_pools,
41
- :@loaded_fixtures,
42
- :@connection_subscriber,
43
- :@saved_pool_configs,
44
- :@assertion_instance,
42
+ :@fixture_connections,
43
+ :@integration_session,
45
44
  :@legacy_saved_pool_configs,
45
+ :@loaded_fixtures,
46
46
  :@matcher_definitions,
47
- :@__memoized,
48
- :@__inspect_output
49
- ]).freeze
47
+ :@saved_pool_configs
48
+ ].freeze
50
49
 
51
- RAILS_SKIP_LIST = Set.new([
50
+ RAILS_SKIP_LIST = [
52
51
  :@new_record,
53
52
  :@attributes,
54
53
  :@association_cache,
55
54
  :@readonly,
56
55
  :@previously_new_record,
57
- :@_routes, # usually just shows #<ActionDispatch::Routing::RouteSet:0x000000016087d708>
56
+ :@_routes,
58
57
  :@routes,
59
58
  :@app,
59
+ :@arel_table,
60
+ :@assertion_instance,
61
+ :@association_cache,
62
+ :@attributes,
60
63
  :@destroyed,
61
- :@response, #usually big, gets truncated anyway
62
- :@marked_for_destruction,
63
64
  :@destroyed_by_association,
64
- :@primary_key,
65
- :@strict_loading,
66
- :@assertion_instance,
67
- :@strict_loading_mode,
65
+ :@find_by_statement_cache,
66
+ :@generated_relation_method,
67
+ :@integration_session,
68
+ :@marked_for_destruction,
68
69
  :@mutations_before_last_save,
69
70
  :@mutations_from_database,
70
- :@integration_session,
71
- :@relation_delegate_cache,
71
+ :@new_record,
72
72
  :@predicate_builder,
73
- :@generated_relation_method,
74
- :@find_by_statement_cache,
75
- :@arel_table,
73
+ :@previously_new_record,
74
+ :@primary_key,
75
+ :@readonly,
76
+ :@relation_delegate_cache,
77
+ :@response,
76
78
  :@response_klass,
77
- ]).freeze
79
+ :@routes,
80
+ :@strict_loading,
81
+ :@strict_loading_mode
82
+ ].freeze
83
+
84
+ MINITEST_SKIP_LIST = [:@NAME, :@failures, :@time].freeze
78
85
 
79
- DEFAULT_SKIP_LIST = (RAILS_SKIP_LIST + RSPEC_SKIP_LIST).freeze
86
+ DEFAULT_SKIP_LIST = (RAILS_SKIP_LIST + RSPEC_SKIP_LIST + MINITEST_SKIP_LIST)
80
87
 
81
88
  @enabled = false
89
+ @max_length = nil
90
+ @capture_rescue = nil
91
+ @skip_list = nil
92
+ @capture_events = nil
93
+ @debug = nil
94
+ @output_format = nil
95
+ @eligible_for_capture = nil
96
+ @original_global_variables = nil
97
+ @trace = nil
98
+ @override_messages = nil
99
+ @rspec_failure_message_loaded = nil
82
100
 
83
- def max_length(value = nil)
84
- if value.nil?
85
- @max_length ||= DEFAULT_MAX_LENGTH
86
- else
87
- @max_length = value
101
+ # Default values
102
+ @max_capture_events = -1 # -1 means no limit
103
+ @capture_events_count = 0
104
+
105
+ # Thread-safe getters and setters
106
+ def enabled=(val)
107
+ mutex.synchronize { @enabled = val }
108
+ end
109
+
110
+ def enabled
111
+ mutex.synchronize { @enabled }
112
+ end
113
+
114
+ def capture_rescue=(val)
115
+ mutex.synchronize { @capture_rescue = val }
116
+ end
117
+
118
+ def capture_rescue
119
+ mutex.synchronize { @capture_rescue }
120
+ end
121
+
122
+ def capture_events_count
123
+ mutex.synchronize { @capture_events_count }
124
+ end
125
+
126
+ def capture_events_count=(val)
127
+ mutex.synchronize { @capture_events_count = val }
128
+ end
129
+
130
+ def max_capture_events
131
+ mutex.synchronize { @max_capture_events }
132
+ end
133
+
134
+ def max_capture_events=(value)
135
+ mutex.synchronize do
136
+ @max_capture_events = value
137
+ if @max_capture_events == 0
138
+ # Disable capturing
139
+ if @enabled
140
+ puts "EnhancedErrors: max_capture_events set to 0, disabling capturing."
141
+ @enabled = false
142
+ @trace&.disable
143
+ @rspec_tracepoint&.disable
144
+ @minitest_trace&.disable
145
+ end
146
+ end
88
147
  end
89
- @max_length
90
148
  end
91
149
 
92
- def capture_rescue(value = nil)
93
- if value.nil?
94
- @capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
95
- else
96
- @capture_rescue = value
150
+ def increment_capture_events_count
151
+ mutex.synchronize do
152
+ @capture_events_count += 1
153
+ # Check if we've hit the limit
154
+ if @max_capture_events > 0 && @capture_events_count >= @max_capture_events
155
+ # puts "EnhancedErrors: max_capture_events limit (#{@max_capture_events}) reached, disabling capturing."
156
+ @enabled = false
157
+ end
158
+ end
159
+ end
160
+
161
+ def reset_capture_events_count
162
+ mutex.synchronize do
163
+ @capture_events_count = 0
164
+ @enabled = true
165
+ @rspec_tracepoint.enable if @rspec_tracepoint
166
+ @trace.enable if @trace
167
+ end
168
+ end
169
+
170
+ def max_length(value = nil)
171
+ mutex.synchronize do
172
+ if value.nil?
173
+ @max_length ||= DEFAULT_MAX_LENGTH
174
+ else
175
+ @max_length = value
176
+ end
177
+ @max_length
97
178
  end
98
- @capture_rescue
99
179
  end
100
180
 
101
181
  def skip_list
102
- @skip_list ||= DEFAULT_SKIP_LIST.dup
182
+ mutex.synchronize do
183
+ @skip_list ||= DEFAULT_SKIP_LIST
184
+ end
103
185
  end
104
186
 
105
- # takes an exception and bindings, calculates the variables message
106
- # and modifies the exceptions .message to display the variables
107
187
  def override_exception_message(exception, binding_or_bindings)
108
- # Ensure binding_or_bindings is always an array for compatibility
109
- return exception if binding_or_bindings.nil? || binding_or_bindings.empty? || exception.respond_to?(:unaltered_message)
188
+ return nil unless exception
189
+ rspec_binding = !(binding_or_bindings.nil? || binding_or_bindings.empty?)
190
+ exception_binding = (exception.binding_infos.length > 0)
191
+ has_message = !(exception.respond_to?(:unaltered_message))
192
+ return nil unless (rspec_binding || exception_binding) && has_message
193
+
110
194
  variable_str = EnhancedErrors.format(binding_or_bindings)
111
195
  message_str = exception.message
196
+ if exception.respond_to?(:captured_variables) && !message_str.include?(exception.captured_variables)
197
+ message_str += exception.captured_variables
198
+ end
112
199
  exception.define_singleton_method(:unaltered_message) { message_str }
113
200
  exception.define_singleton_method(:message) do
114
201
  "#{message_str}#{variable_str}"
@@ -117,26 +204,32 @@ class EnhancedErrors
117
204
  end
118
205
 
119
206
  def add_to_skip_list(*vars)
120
- skip_list.merge(vars)
207
+ mutex.synchronize do
208
+ @skip_list.concat(vars)
209
+ end
121
210
  end
122
211
 
123
212
  def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
124
- require_relative 'enhanced/exception'
125
-
126
- @output_format = nil
127
- @eligible_for_capture = nil
128
- @original_global_variables = nil
129
- @override_messages = override_messages
213
+ mutex.synchronize do
214
+ @trace&.disable
215
+ @trace = nil
130
216
 
131
- if !enabled
217
+ @output_format = nil
218
+ @eligible_for_capture = nil
132
219
  @original_global_variables = nil
133
- @enabled = false
134
- @trace&.disable
135
- else
136
- # if there's an old one, disable it before replacing it
137
- # this seems to actually matter, although it seems like it
138
- # shouldn't
139
- @trace&.disable
220
+ @override_messages = override_messages
221
+
222
+ # Ensure these are not nil
223
+ @max_capture_events = -1 if @max_capture_events.nil?
224
+ @capture_events_count = 0
225
+
226
+ @rspec_failure_message_loaded = true
227
+
228
+ if !enabled
229
+ @original_global_variables = nil
230
+ @enabled = false
231
+ return
232
+ end
140
233
 
141
234
  @enabled = true
142
235
  @debug = debug
@@ -148,8 +241,6 @@ class EnhancedErrors
148
241
  send(setter_method, value)
149
242
  elsif respond_to?(key)
150
243
  send(key, value)
151
- else
152
- # Ignore unknown options
153
244
  end
154
245
  end
155
246
 
@@ -158,64 +249,107 @@ class EnhancedErrors
158
249
 
159
250
  validate_and_set_capture_events(capture_events)
160
251
 
252
+ # If max_capture_events == 0, capturing is off from the start.
253
+ if @max_capture_events == 0
254
+ @enabled = false
255
+ return
256
+ end
257
+
161
258
  events = @capture_events ? @capture_events.to_a : default_capture_events
162
259
  @trace = TracePoint.new(*events) do |tp|
163
260
  handle_tracepoint_event(tp)
164
261
  end
165
262
 
166
- @trace.enable
263
+ # Only enable trace if still enabled and not limited
264
+ if @enabled && (@max_capture_events == -1 || @capture_events_count < @max_capture_events)
265
+ @trace.enable
266
+ end
167
267
  end
168
268
  end
169
269
 
170
270
  def safe_prepend_module(target_class, mod)
171
- if defined?(target_class) && target_class.is_a?(Module)
172
- target_class.prepend(mod)
173
- true
174
- else
175
- false
271
+ mutex.synchronize do
272
+ if defined?(target_class) && target_class.is_a?(Module)
273
+ target_class.prepend(mod)
274
+ true
275
+ else
276
+ false
277
+ end
176
278
  end
177
279
  end
178
280
 
179
281
  def safely_prepend_rspec_custom_failure_message
180
- return if @rspec_failure_message_loaded
181
- if defined?(RSpec::Core::Example) && !RSpec::Core::Example < Enhanced::Integrations::RSpecErrorFailureMessage
182
- RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
183
- @rspec_failure_message_loaded = true
282
+ mutex.synchronize do
283
+ return if @rspec_failure_message_loaded
284
+ if defined?(RSpec::Core::Example) && !RSpec::Core::Example < Enhanced::Integrations::RSpecErrorFailureMessage
285
+ RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
286
+ @rspec_failure_message_loaded = true
287
+ end
184
288
  end
185
289
  rescue => e
186
290
  puts "Failed to prepend RSpec custom failure message: #{e.message}"
187
291
  end
188
292
 
293
+ def is_a_minitest?(klass)
294
+ klass.ancestors.include?(Minitest::Test) && klass.name != 'Minitest::Test'
295
+ end
189
296
 
190
- def start_rspec_binding_capture
191
- @rspec_example_binding = nil
192
-
193
- # In the Exception binding infos, I observed that re-setting
194
- # the tracepoint without disabling it seemed to accumulate traces
195
- # in the test suite where things are disabled and re-enabled often.
196
- @rspec_tracepoint&.disable
297
+ def start_minitest_binding_capture
298
+ mutex.synchronize do
299
+ @minitest_trace = TracePoint.new(:return) do |tp|
300
+ next unless tp.method_id.to_s.start_with?('test_') && is_a_minitest?(tp.defined_class)
301
+ @minitest_test_binding = tp.binding
302
+ end
303
+ @minitest_trace.enable
304
+ end
305
+ end
197
306
 
198
- @rspec_tracepoint = TracePoint.new(:b_return) do |tp|
199
- # This is super-kluge-y and should be replaced with... something TBD
307
+ def stop_minitest_binding_capture
308
+ mutex.synchronize do
309
+ @minitest_trace&.disable
310
+ @minitest_trace = nil
311
+ convert_binding_to_binding_info(@minitest_test_binding) if @minitest_test_binding
312
+ end
313
+ end
200
314
 
201
- # early easy checks to nope out of the object name and other checks
202
- if tp.method_id.nil? && !(tp.path.include?('rspec')) && tp.path.end_with?('_spec.rb')
203
- # fixes cases where class and name are screwed up or overridden
204
- if determine_object_name(tp) =~ RSPEC_EXAMPLE_REGEXP
205
- @rspec_example_binding = tp.binding
315
+ def start_rspec_binding_capture
316
+ mutex.synchronize do
317
+ @rspec_example_binding = nil
318
+ @capture_next_binding = false
319
+ @rspec_tracepoint&.disable
320
+ @enabled = true if @enabled.nil?
321
+
322
+ @rspec_tracepoint = TracePoint.new(:raise, :b_return) do |tp|
323
+ # This trickery is to help us identify the anonymous block return we want to grab
324
+ if tp.event == :b_return
325
+ next unless @capture_next_binding && @rspec_example_binding.nil?
326
+ if @capture_next_binding && tp.method_id.nil? && !(tp.path.include?('rspec')) && tp.path.end_with?('_spec.rb')
327
+ if determine_object_name(tp) =~ RSPEC_EXAMPLE_REGEXP
328
+ increment_capture_events_count
329
+ @rspec_example_binding = tp.binding
330
+ end
331
+ end
332
+ elsif tp.event == :raise
333
+ if tp.raised_exception.class.name == 'RSpec::Expectations::ExpectationNotMetError'
334
+ @capture_next_binding ||= true
335
+ else
336
+ handle_tracepoint_event(tp)
337
+ end
206
338
  end
207
339
  end
208
-
340
+ @rspec_tracepoint.enable
209
341
  end
210
-
211
- @rspec_tracepoint.enable
212
342
  end
213
343
 
214
344
  def stop_rspec_binding_capture
215
- @rspec_tracepoint&.disable
216
- binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
217
- @rspec_example_binding = nil
218
- binding_info
345
+ mutex.synchronize do
346
+ @rspec_tracepoint&.disable
347
+ @rspec_tracepoint = nil
348
+ binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
349
+ @capture_next_binding = false
350
+ @rspec_example_binding = nil
351
+ binding_info
352
+ end
219
353
  end
220
354
 
221
355
  def convert_binding_to_binding_info(b, capture_let_variables: true)
@@ -228,7 +362,6 @@ class EnhancedErrors
228
362
  instance_vars = receiver.instance_variables
229
363
  instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
230
364
 
231
- # Capture let variables only for RSpec captures
232
365
  lets = {}
233
366
  if capture_let_variables && instance_vars.include?(:@__memoized)
234
367
  outer_memoized = receiver.instance_variable_get(:@__memoized)
@@ -257,59 +390,66 @@ class EnhancedErrors
257
390
  capture_event: 'RSpecContext'
258
391
  }
259
392
 
260
- # Apply skip list to remove @__memoized and @__inspect_output from output
261
- # but only after extracting let variables.
262
393
  default_on_capture(binding_info)
263
394
  end
264
395
 
265
396
  def eligible_for_capture(&block)
266
- if block_given?
267
- @eligible_for_capture = block
268
- else
269
- @eligible_for_capture ||= method(:default_eligible_for_capture)
397
+ mutex.synchronize do
398
+ if block_given?
399
+ @eligible_for_capture = block
400
+ else
401
+ @eligible_for_capture ||= method(:default_eligible_for_capture)
402
+ end
270
403
  end
271
404
  end
272
405
 
273
406
  def on_capture(&block)
274
- if block_given?
275
- @on_capture_hook = block
276
- else
277
- @on_capture_hook ||= method(:default_on_capture)
407
+ mutex.synchronize do
408
+ if block_given?
409
+ @on_capture_hook = block
410
+ else
411
+ @on_capture_hook ||= method(:default_on_capture)
412
+ end
278
413
  end
279
414
  end
280
415
 
281
416
  def on_capture=(value)
282
- self.on_capture_hook = value
417
+ mutex.synchronize do
418
+ @on_capture_hook = value
419
+ end
283
420
  end
284
421
 
285
422
  def on_format(&block)
286
- if block_given?
287
- @on_format_hook = block
288
- else
289
- @on_format_hook ||= method(:default_on_format)
423
+ mutex.synchronize do
424
+ if block_given?
425
+ @on_format_hook = block
426
+ else
427
+ @on_format_hook ||= method(:default_on_format)
428
+ end
290
429
  end
291
430
  end
292
431
 
293
432
  def on_format=(value)
294
- @on_format_hook = value
433
+ mutex.synchronize do
434
+ @on_format_hook = value
435
+ end
295
436
  end
296
437
 
297
438
  def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
298
439
  return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
299
440
 
300
- # If captured_binding_infos is already an array, use it directly; otherwise, wrap it in an array.
301
- binding_infos = captured_binding_infos.is_a?(Array) ? captured_binding_infos : [captured_binding_infos]
302
-
303
- result = binding_infos_array_to_string(binding_infos, output_format)
441
+ result = binding_infos_array_to_string(captured_binding_infos, output_format)
304
442
 
305
- if @on_format_hook
306
- begin
307
- result = @on_format_hook.call(result)
308
- rescue
309
- result = ''
443
+ mutex.synchronize do
444
+ if @on_format_hook
445
+ begin
446
+ result = @on_format_hook.call(result)
447
+ rescue
448
+ result = ''
449
+ end
450
+ else
451
+ result = default_on_format(result)
310
452
  end
311
- else
312
- result = default_on_format(result)
313
453
  end
314
454
 
315
455
  result
@@ -318,48 +458,55 @@ class EnhancedErrors
318
458
  def binding_infos_array_to_string(captured_bindings, format = :terminal)
319
459
  return '' if captured_bindings.nil? || captured_bindings.empty?
320
460
  captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
321
- Colors.enabled = format == :terminal
461
+ Colors.enabled = (format == :terminal)
322
462
  formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
323
463
  format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
324
464
  end
325
465
 
326
466
  def get_default_format_for_environment
327
- return @output_format unless @output_format.nil?
328
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
329
- @output_format = case env
330
- when 'development', 'test'
331
- if running_in_ci?
332
- :plaintext
467
+ mutex.synchronize do
468
+ return @output_format unless @output_format.nil?
469
+ env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
470
+ @output_format = case env
471
+ when 'development', 'test'
472
+ if running_in_ci?
473
+ :plaintext
474
+ else
475
+ :terminal
476
+ end
477
+ when 'production'
478
+ :json
333
479
  else
334
480
  :terminal
335
481
  end
336
- when 'production'
337
- :json
338
- else
339
- :terminal
340
- end
482
+ end
341
483
  end
342
484
 
343
485
  def running_in_ci?
344
- return @running_in_ci if defined?(@running_in_ci)
345
- ci_env_vars = {
346
- 'CI' => ENV['CI'],
347
- 'JENKINS' => ENV['JENKINS'],
348
- 'GITHUB_ACTIONS' => ENV['GITHUB_ACTIONS'],
349
- 'CIRCLECI' => ENV['CIRCLECI'],
350
- 'TRAVIS' => ENV['TRAVIS'],
351
- 'APPVEYOR' => ENV['APPVEYOR'],
352
- 'GITLAB_CI' => ENV['GITLAB_CI']
353
- }
354
- @running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
486
+ mutex.synchronize do
487
+ return @running_in_ci if defined?(@running_in_ci)
488
+ ci_env_vars = {
489
+ 'CI' => ENV['CI'],
490
+ 'JENKINS' => ENV['JENKINS'],
491
+ 'GITHUB_ACTIONS' => ENV['GITHUB_ACTIONS'],
492
+ 'CIRCLECI' => ENV['CIRCLECI'],
493
+ 'TRAVIS' => ENV['TRAVIS'],
494
+ 'APPVEYOR' => ENV['APPVEYOR'],
495
+ 'GITLAB_CI' => ENV['GITLAB_CI']
496
+ }
497
+ @running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
498
+ end
355
499
  end
356
500
 
357
501
  def apply_skip_list(binding_info)
358
- variables = binding_info[:variables]
359
- variables[:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s.start_with?('@_') && !@debug) }
360
- variables[:locals]&.reject! { |var, _| skip_list.include?(var) }
361
- return binding_info unless @debug
362
- variables[:globals]&.reject! { |var, _| skip_list.include?(var) }
502
+ mutex.synchronize do
503
+ variables = binding_info[:variables]
504
+ variables[:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s.start_with?('@_') && !@debug) }
505
+ variables[:locals]&.reject! { |var, _| skip_list.include?(var) }
506
+ if @debug
507
+ variables[:globals]&.reject! { |var, _| skip_list.include?(var) }
508
+ end
509
+ end
363
510
  binding_info
364
511
  end
365
512
 
@@ -394,7 +541,6 @@ class EnhancedErrors
394
541
  result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
395
542
  end
396
543
 
397
- # Display let variables for RSpec captures
398
544
  if variables[:lets] && !variables[:lets].empty?
399
545
  result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
400
546
  end
@@ -403,9 +549,13 @@ class EnhancedErrors
403
549
  result += "\n#{Colors.green('Globals:')}\n#{variable_description(variables[:globals])}"
404
550
  end
405
551
 
406
- if result.length > max_length
407
- result = result[0...max_length] + "... (truncated)"
552
+ mutex.synchronize do
553
+ max_len = @max_length || DEFAULT_MAX_LENGTH
554
+ if result.length > max_len
555
+ result = result[0...max_len] + "... (truncated)"
556
+ end
408
557
  end
558
+
409
559
  result + "\n"
410
560
  rescue => e
411
561
  puts "#{e.message}"
@@ -414,12 +564,18 @@ class EnhancedErrors
414
564
 
415
565
  private
416
566
 
567
+
417
568
  def handle_tracepoint_event(tp)
418
- return unless @enabled
569
+ # Check enabled outside the synchronized block for speed, but still safe due to re-check inside.
570
+ return unless mutex.synchronize { @enabled }
419
571
  return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
572
+
420
573
  Thread.current[:enhanced_errors_processing] = true
421
574
  exception = tp.raised_exception
422
- capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
575
+
576
+ capture_me = mutex.synchronize do
577
+ !exception.frozen? && (@eligible_for_capture || method(:default_eligible_for_capture)).call(exception)
578
+ end
423
579
 
424
580
  unless capture_me
425
581
  Thread.current[:enhanced_errors_processing] = false
@@ -442,19 +598,19 @@ class EnhancedErrors
442
598
  [var, safe_instance_variable_get(binding_context.receiver, var)]
443
599
  }.to_h
444
600
 
445
- # No let variables for exceptions
446
601
  lets = {}
447
602
 
448
603
  globals = {}
449
- if @debug
450
- globals = (global_variables - @original_global_variables).map { |var|
451
- [var, get_global_variable_value(var)]
452
- }.to_h
604
+ mutex.synchronize do
605
+ if @debug
606
+ globals = (global_variables - @original_global_variables.to_a).map { |var|
607
+ [var, get_global_variable_value(var)]
608
+ }.to_h
609
+ end
453
610
  end
454
611
 
455
612
  capture_event = safe_to_s(tp.event)
456
613
  location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
457
-
458
614
  binding_info = {
459
615
  source: location,
460
616
  object: tp.self,
@@ -472,11 +628,12 @@ class EnhancedErrors
472
628
  }
473
629
 
474
630
  binding_info = default_on_capture(binding_info)
631
+ on_capture_hook_local = mutex.synchronize { @on_capture_hook }
475
632
 
476
- if on_capture_hook
633
+ if on_capture_hook_local
477
634
  begin
478
635
  Thread.current[:on_capture] = true
479
- binding_info = on_capture_hook.call(binding_info)
636
+ binding_info = on_capture_hook_local.call(binding_info)
480
637
  rescue
481
638
  binding_info = nil
482
639
  ensure
@@ -486,15 +643,15 @@ class EnhancedErrors
486
643
 
487
644
  if binding_info
488
645
  binding_info = validate_binding_format(binding_info)
489
-
490
646
  if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
491
- # delete from the middle of the array as the ends are most interesting
492
647
  exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
493
648
  end
494
-
495
649
  if binding_info
496
650
  exception.binding_infos << binding_info
497
- override_exception_message(exception, exception.binding_infos) if @override_messages
651
+ mutex.synchronize do
652
+ override_exception_message(exception, exception.binding_infos) if @override_messages
653
+ end
654
+ increment_capture_events_count
498
655
  end
499
656
  end
500
657
  rescue
@@ -508,52 +665,54 @@ class EnhancedErrors
508
665
  end
509
666
 
510
667
  def test_name
511
- if defined?(RSpec)
512
- RSpec&.current_example&.full_description
668
+ begin
669
+ defined?(RSpec) ? RSpec&.current_example&.full_description : nil
670
+ rescue
671
+ nil
513
672
  end
514
- rescue
515
- nil
516
673
  end
517
674
 
518
675
  def default_capture_events
519
- return @default_capture_events if @default_capture_events
520
- events = [:raise]
521
- if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
522
- events << :rescue
676
+ mutex.synchronize do
677
+ events = [:raise]
678
+ rescue_available = !!(Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0'))
679
+ if capture_rescue && rescue_available
680
+ events << :rescue
681
+ end
682
+ events
523
683
  end
524
- @default_capture_events = events
525
684
  end
526
685
 
527
686
  def validate_and_set_capture_events(capture_events)
528
- if capture_events.nil?
529
- @capture_events = default_capture_events
530
- return
531
- end
687
+ mutex.synchronize do
688
+ if capture_events.nil?
689
+ @capture_events = default_capture_events
690
+ return
691
+ end
532
692
 
533
- unless valid_capture_events?(capture_events)
534
- puts "EnhancedErrors: Invalid capture_events provided. Falling back to defaults."
535
- @capture_events = default_capture_events
536
- return
537
- end
693
+ unless valid_capture_events?(capture_events)
694
+ puts "EnhancedErrors: Invalid capture_events provided. Falling back to defaults."
695
+ @capture_events = default_capture_events
696
+ return
697
+ end
538
698
 
539
- if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
540
- puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
541
- capture_events = capture_events - [:rescue]
542
- end
699
+ if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
700
+ puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
701
+ capture_events = capture_events - [:rescue]
702
+ end
543
703
 
544
- if capture_events.empty?
545
- puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
546
- @capture_events = default_capture_events
547
- return
548
- end
704
+ if capture_events.empty?
705
+ puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
706
+ @capture_events = default_capture_events
707
+ return
708
+ end
549
709
 
550
- @capture_events = capture_events.to_set
710
+ @capture_events = capture_events
711
+ end
551
712
  end
552
713
 
553
714
  def valid_capture_events?(capture_events)
554
- return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
555
- valid_types = [:raise, :rescue].to_set
556
- capture_events.to_set.subset?(valid_types)
715
+ capture_events.is_a?(Array) && [:raise, :rescue] && capture_events
557
716
  end
558
717
 
559
718
  def extract_arguments(tp, method_name)
@@ -578,14 +737,7 @@ class EnhancedErrors
578
737
 
579
738
  def determine_object_name(tp, method_name = '')
580
739
  begin
581
- # These tricks are used to get around the fact that `tp.self` can be a class that is
582
- # wired up with method_missing where every direct call alters the class. This is true
583
- # on certain builders or config objects and caused problems.
584
-
585
- # Directly bind and call the `class` method to avoid triggering `method_missing`
586
740
  self_class = Object.instance_method(:class).bind(tp.self).call
587
-
588
- # Similarly, bind and call `singleton_class` safely
589
741
  singleton_class = Object.instance_method(:singleton_class).bind(tp.self).call
590
742
 
591
743
  if self_class && tp.defined_class == singleton_class
@@ -597,7 +749,7 @@ class EnhancedErrors
597
749
  method_suffix = method_name && !method_name.empty? ? "##{method_name}" : ""
598
750
  "#{object_class_name}#{method_suffix}"
599
751
  end
600
- rescue Exception => e
752
+ rescue
601
753
  '[ErrorGettingName]'
602
754
  end
603
755
  end
@@ -621,7 +773,7 @@ class EnhancedErrors
621
773
 
622
774
  def variable_description(vars_hash)
623
775
  vars_hash.map do |name, value|
624
- " #{Colors.purple(name)}: #{format_variable(value)}\n"
776
+ " #{Colors.purple(name)}: #{format_variable(value)}\n"
625
777
  end.join
626
778
  rescue
627
779
  ''
@@ -639,8 +791,10 @@ class EnhancedErrors
639
791
  end
640
792
 
641
793
  def awesome_print_available?
642
- return @awesome_print_available unless @awesome_print_available.nil?
643
- @awesome_print_available = defined?(AwesomePrint)
794
+ mutex.synchronize do
795
+ return @awesome_print_available unless @awesome_print_available.nil?
796
+ @awesome_print_available = defined?(AwesomePrint)
797
+ end
644
798
  end
645
799
 
646
800
  def safe_inspect(variable)
@@ -682,7 +836,7 @@ class EnhancedErrors
682
836
  end
683
837
 
684
838
  def default_on_capture(binding_info)
685
- EnhancedErrors.apply_skip_list(binding_info)
839
+ apply_skip_list(binding_info)
686
840
  end
687
841
 
688
842
  def default_eligible_for_capture(exception)