enhanced_errors 2.0.6 → 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,8 +2,10 @@
2
2
 
3
3
  require 'set'
4
4
  require 'json'
5
+ require 'monitor'
5
6
 
6
7
  require_relative 'enhanced/colors'
8
+ require_relative 'enhanced/exception'
7
9
 
8
10
  IGNORED_EXCEPTIONS = %w[SystemExit NoMemoryError SignalException Interrupt
9
11
  ScriptError LoadError NotImplementedError SyntaxError
@@ -15,100 +17,185 @@ class EnhancedErrors
15
17
  extend ::Enhanced
16
18
 
17
19
  class << self
18
- attr_accessor :enabled, :config_block, :max_length, :on_capture_hook,
19
- :eligible_for_capture, :trace, :skip_list, :capture_rescue,
20
- :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
21
25
 
22
26
  GEMS_REGEX = %r{[\/\\]gems[\/\\]}
23
27
  RSPEC_EXAMPLE_REGEXP = /RSpec::ExampleGroups::[A-Z0-9]+.*/
24
28
  DEFAULT_MAX_LENGTH = 2000
25
-
26
- # Maximum binding infos we will track per-exception instance. This is intended as an extra
27
- # safety rail, not a normal scenario.
28
29
  MAX_BINDING_INFOS = 3
29
30
 
30
- # Add @__memoized and @__inspect_output to the skip list so they don't appear in output
31
- RSPEC_SKIP_LIST = Set.new([
31
+ RSPEC_SKIP_LIST = [
32
+ :@__inspect_output,
33
+ :@__memoized,
34
+ :@assertion_delegator,
35
+ :@assertion_instance,
32
36
  :@assertions,
33
- :@integration_session,
37
+ :@connection_subscriber,
34
38
  :@example,
35
- :@assertion_delegator,
36
39
  :@fixture_cache,
37
40
  :@fixture_cache_key,
38
- :@fixture_connections,
39
41
  :@fixture_connection_pools,
40
- :@loaded_fixtures,
41
- :@connection_subscriber,
42
- :@saved_pool_configs,
43
- :@assertion_instance,
42
+ :@fixture_connections,
43
+ :@integration_session,
44
44
  :@legacy_saved_pool_configs,
45
+ :@loaded_fixtures,
45
46
  :@matcher_definitions,
46
- :@__memoized,
47
- :@__inspect_output
48
- ]).freeze
47
+ :@saved_pool_configs
48
+ ].freeze
49
49
 
50
- RAILS_SKIP_LIST = Set.new([
50
+ RAILS_SKIP_LIST = [
51
51
  :@new_record,
52
52
  :@attributes,
53
53
  :@association_cache,
54
54
  :@readonly,
55
55
  :@previously_new_record,
56
- :@_routes, # usually just shows #<ActionDispatch::Routing::RouteSet:0x000000016087d708>
56
+ :@_routes,
57
57
  :@routes,
58
58
  :@app,
59
+ :@arel_table,
60
+ :@assertion_instance,
61
+ :@association_cache,
62
+ :@attributes,
59
63
  :@destroyed,
60
- :@response, #usually big, gets truncated anyway
61
- :@marked_for_destruction,
62
64
  :@destroyed_by_association,
63
- :@primary_key,
64
- :@strict_loading,
65
- :@assertion_instance,
66
- :@strict_loading_mode,
65
+ :@find_by_statement_cache,
66
+ :@generated_relation_method,
67
+ :@integration_session,
68
+ :@marked_for_destruction,
67
69
  :@mutations_before_last_save,
68
70
  :@mutations_from_database,
69
- :@integration_session,
70
- :@relation_delegate_cache,
71
+ :@new_record,
71
72
  :@predicate_builder,
72
- :@generated_relation_method,
73
- :@find_by_statement_cache,
74
- :@arel_table,
73
+ :@previously_new_record,
74
+ :@primary_key,
75
+ :@readonly,
76
+ :@relation_delegate_cache,
77
+ :@response,
75
78
  :@response_klass,
76
- ]).freeze
79
+ :@routes,
80
+ :@strict_loading,
81
+ :@strict_loading_mode
82
+ ].freeze
83
+
84
+ MINITEST_SKIP_LIST = [:@NAME, :@failures, :@time].freeze
77
85
 
78
- DEFAULT_SKIP_LIST = (RAILS_SKIP_LIST + RSPEC_SKIP_LIST).freeze
86
+ DEFAULT_SKIP_LIST = (RAILS_SKIP_LIST + RSPEC_SKIP_LIST + MINITEST_SKIP_LIST)
79
87
 
80
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
81
100
 
82
- def max_length(value = nil)
83
- if value.nil?
84
- @max_length ||= DEFAULT_MAX_LENGTH
85
- else
86
- @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
87
147
  end
88
- @max_length
89
148
  end
90
149
 
91
- def capture_rescue(value = nil)
92
- if value.nil?
93
- @capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
94
- else
95
- @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
96
178
  end
97
- @capture_rescue
98
179
  end
99
180
 
100
181
  def skip_list
101
- @skip_list ||= DEFAULT_SKIP_LIST.dup
182
+ mutex.synchronize do
183
+ @skip_list ||= DEFAULT_SKIP_LIST
184
+ end
102
185
  end
103
186
 
104
- # takes an exception and bindings, calculates the variables message
105
- # and modifies the exceptions .message to display the variables
106
187
  def override_exception_message(exception, binding_or_bindings)
107
- # Ensure binding_or_bindings is always an array for compatibility
108
- return exception if binding_or_bindings.nil? || binding_or_bindings.empty?
109
- return exception if 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,75 +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
296
+
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
306
+
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
314
+
189
315
  def start_rspec_binding_capture
190
- @rspec_example_binding = nil
191
- @capture_next_binding = false
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
197
-
198
- @rspec_tracepoint = TracePoint.new(:raise, :b_return) do |tp|
199
- # This is super-kluge-y and should be replaced with... something TBD
200
- # only look at block returns once we have seen an ExpectationNotMetError
201
-
202
- case tp.event
203
- when :b_return
204
- # only active if we are capturing the next binding
205
- next unless @capture_next_binding && @rspec_example_binding.nil?
206
- # early easy checks to nope out of the object name and other checks
207
- if @capture_next_binding && tp.method_id.nil? && !(tp.path.include?('rspec')) && tp.path.end_with?('_spec.rb')
208
- # fixes cases where class and name are screwed up or overridden
209
- if determine_object_name(tp) =~ RSPEC_EXAMPLE_REGEXP
210
- @rspec_example_binding = tp.binding
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)
211
337
  end
212
- end
213
- when :raise
214
- # turn on capture of next binding
215
- if tp.raised_exception.class.name == 'RSpec::Expectations::ExpectationNotMetError'
216
- @capture_next_binding ||= true
217
338
  end
218
339
  end
340
+ @rspec_tracepoint.enable
219
341
  end
220
-
221
- @rspec_tracepoint.enable
222
342
  end
223
343
 
224
344
  def stop_rspec_binding_capture
225
- @rspec_tracepoint&.disable
226
- binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
227
- @capture_next_binding = false
228
- @rspec_example_binding = nil
229
- 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
230
353
  end
231
354
 
232
355
  def convert_binding_to_binding_info(b, capture_let_variables: true)
@@ -239,7 +362,6 @@ class EnhancedErrors
239
362
  instance_vars = receiver.instance_variables
240
363
  instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
241
364
 
242
- # Capture let variables only for RSpec captures
243
365
  lets = {}
244
366
  if capture_let_variables && instance_vars.include?(:@__memoized)
245
367
  outer_memoized = receiver.instance_variable_get(:@__memoized)
@@ -268,59 +390,66 @@ class EnhancedErrors
268
390
  capture_event: 'RSpecContext'
269
391
  }
270
392
 
271
- # Apply skip list to remove @__memoized and @__inspect_output from output
272
- # but only after extracting let variables.
273
393
  default_on_capture(binding_info)
274
394
  end
275
395
 
276
396
  def eligible_for_capture(&block)
277
- if block_given?
278
- @eligible_for_capture = block
279
- else
280
- @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
281
403
  end
282
404
  end
283
405
 
284
406
  def on_capture(&block)
285
- if block_given?
286
- @on_capture_hook = block
287
- else
288
- @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
289
413
  end
290
414
  end
291
415
 
292
416
  def on_capture=(value)
293
- self.on_capture_hook = value
417
+ mutex.synchronize do
418
+ @on_capture_hook = value
419
+ end
294
420
  end
295
421
 
296
422
  def on_format(&block)
297
- if block_given?
298
- @on_format_hook = block
299
- else
300
- @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
301
429
  end
302
430
  end
303
431
 
304
432
  def on_format=(value)
305
- @on_format_hook = value
433
+ mutex.synchronize do
434
+ @on_format_hook = value
435
+ end
306
436
  end
307
437
 
308
438
  def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
309
439
  return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
310
440
 
311
- # If captured_binding_infos is already an array, use it directly; otherwise, wrap it in an array.
312
- binding_infos = captured_binding_infos.is_a?(Array) ? captured_binding_infos : [captured_binding_infos]
441
+ result = binding_infos_array_to_string(captured_binding_infos, output_format)
313
442
 
314
- result = binding_infos_array_to_string(binding_infos, output_format)
315
-
316
- if @on_format_hook
317
- begin
318
- result = @on_format_hook.call(result)
319
- rescue
320
- 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)
321
452
  end
322
- else
323
- result = default_on_format(result)
324
453
  end
325
454
 
326
455
  result
@@ -329,48 +458,55 @@ class EnhancedErrors
329
458
  def binding_infos_array_to_string(captured_bindings, format = :terminal)
330
459
  return '' if captured_bindings.nil? || captured_bindings.empty?
331
460
  captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
332
- Colors.enabled = format == :terminal
461
+ Colors.enabled = (format == :terminal)
333
462
  formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
334
463
  format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
335
464
  end
336
465
 
337
466
  def get_default_format_for_environment
338
- return @output_format unless @output_format.nil?
339
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
340
- @output_format = case env
341
- when 'development', 'test'
342
- if running_in_ci?
343
- :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
344
479
  else
345
480
  :terminal
346
481
  end
347
- when 'production'
348
- :json
349
- else
350
- :terminal
351
- end
482
+ end
352
483
  end
353
484
 
354
485
  def running_in_ci?
355
- return @running_in_ci if defined?(@running_in_ci)
356
- ci_env_vars = {
357
- 'CI' => ENV['CI'],
358
- 'JENKINS' => ENV['JENKINS'],
359
- 'GITHUB_ACTIONS' => ENV['GITHUB_ACTIONS'],
360
- 'CIRCLECI' => ENV['CIRCLECI'],
361
- 'TRAVIS' => ENV['TRAVIS'],
362
- 'APPVEYOR' => ENV['APPVEYOR'],
363
- 'GITLAB_CI' => ENV['GITLAB_CI']
364
- }
365
- @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
366
499
  end
367
500
 
368
501
  def apply_skip_list(binding_info)
369
- variables = binding_info[:variables]
370
- variables[:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s.start_with?('@_') && !@debug) }
371
- variables[:locals]&.reject! { |var, _| skip_list.include?(var) }
372
- return binding_info unless @debug
373
- 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
374
510
  binding_info
375
511
  end
376
512
 
@@ -405,7 +541,6 @@ class EnhancedErrors
405
541
  result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
406
542
  end
407
543
 
408
- # Display let variables for RSpec captures
409
544
  if variables[:lets] && !variables[:lets].empty?
410
545
  result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
411
546
  end
@@ -414,9 +549,13 @@ class EnhancedErrors
414
549
  result += "\n#{Colors.green('Globals:')}\n#{variable_description(variables[:globals])}"
415
550
  end
416
551
 
417
- if result.length > max_length
418
- 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
419
557
  end
558
+
420
559
  result + "\n"
421
560
  rescue => e
422
561
  puts "#{e.message}"
@@ -425,12 +564,18 @@ class EnhancedErrors
425
564
 
426
565
  private
427
566
 
567
+
428
568
  def handle_tracepoint_event(tp)
429
- 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 }
430
571
  return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
572
+
431
573
  Thread.current[:enhanced_errors_processing] = true
432
574
  exception = tp.raised_exception
433
- 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
434
579
 
435
580
  unless capture_me
436
581
  Thread.current[:enhanced_errors_processing] = false
@@ -453,19 +598,19 @@ class EnhancedErrors
453
598
  [var, safe_instance_variable_get(binding_context.receiver, var)]
454
599
  }.to_h
455
600
 
456
- # No let variables for exceptions
457
601
  lets = {}
458
602
 
459
603
  globals = {}
460
- if @debug
461
- globals = (global_variables - @original_global_variables).map { |var|
462
- [var, get_global_variable_value(var)]
463
- }.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
464
610
  end
465
611
 
466
612
  capture_event = safe_to_s(tp.event)
467
613
  location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
468
-
469
614
  binding_info = {
470
615
  source: location,
471
616
  object: tp.self,
@@ -483,11 +628,12 @@ class EnhancedErrors
483
628
  }
484
629
 
485
630
  binding_info = default_on_capture(binding_info)
631
+ on_capture_hook_local = mutex.synchronize { @on_capture_hook }
486
632
 
487
- if on_capture_hook
633
+ if on_capture_hook_local
488
634
  begin
489
635
  Thread.current[:on_capture] = true
490
- binding_info = on_capture_hook.call(binding_info)
636
+ binding_info = on_capture_hook_local.call(binding_info)
491
637
  rescue
492
638
  binding_info = nil
493
639
  ensure
@@ -497,15 +643,15 @@ class EnhancedErrors
497
643
 
498
644
  if binding_info
499
645
  binding_info = validate_binding_format(binding_info)
500
-
501
646
  if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
502
- # delete from the middle of the array as the ends are most interesting
503
647
  exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
504
648
  end
505
-
506
649
  if binding_info
507
650
  exception.binding_infos << binding_info
508
- 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
509
655
  end
510
656
  end
511
657
  rescue
@@ -519,52 +665,54 @@ class EnhancedErrors
519
665
  end
520
666
 
521
667
  def test_name
522
- if defined?(RSpec)
523
- RSpec&.current_example&.full_description
668
+ begin
669
+ defined?(RSpec) ? RSpec&.current_example&.full_description : nil
670
+ rescue
671
+ nil
524
672
  end
525
- rescue
526
- nil
527
673
  end
528
674
 
529
675
  def default_capture_events
530
- return @default_capture_events if @default_capture_events
531
- events = [:raise]
532
- if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
533
- 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
534
683
  end
535
- @default_capture_events = events
536
684
  end
537
685
 
538
686
  def validate_and_set_capture_events(capture_events)
539
- if capture_events.nil?
540
- @capture_events = default_capture_events
541
- return
542
- end
687
+ mutex.synchronize do
688
+ if capture_events.nil?
689
+ @capture_events = default_capture_events
690
+ return
691
+ end
543
692
 
544
- unless valid_capture_events?(capture_events)
545
- puts "EnhancedErrors: Invalid capture_events provided. Falling back to defaults."
546
- @capture_events = default_capture_events
547
- return
548
- 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
549
698
 
550
- if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
551
- puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
552
- capture_events = capture_events - [:rescue]
553
- 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
554
703
 
555
- if capture_events.empty?
556
- puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
557
- @capture_events = default_capture_events
558
- return
559
- 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
560
709
 
561
- @capture_events = capture_events.to_set
710
+ @capture_events = capture_events
711
+ end
562
712
  end
563
713
 
564
714
  def valid_capture_events?(capture_events)
565
- return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
566
- valid_types = [:raise, :rescue].to_set
567
- capture_events.to_set.subset?(valid_types)
715
+ capture_events.is_a?(Array) && [:raise, :rescue] && capture_events
568
716
  end
569
717
 
570
718
  def extract_arguments(tp, method_name)
@@ -589,14 +737,7 @@ class EnhancedErrors
589
737
 
590
738
  def determine_object_name(tp, method_name = '')
591
739
  begin
592
- # These tricks are used to get around the fact that `tp.self` can be a class that is
593
- # wired up with method_missing where every direct call alters the class. This is true
594
- # on certain builders or config objects and caused problems.
595
-
596
- # Directly bind and call the `class` method to avoid triggering `method_missing`
597
740
  self_class = Object.instance_method(:class).bind(tp.self).call
598
-
599
- # Similarly, bind and call `singleton_class` safely
600
741
  singleton_class = Object.instance_method(:singleton_class).bind(tp.self).call
601
742
 
602
743
  if self_class && tp.defined_class == singleton_class
@@ -608,7 +749,7 @@ class EnhancedErrors
608
749
  method_suffix = method_name && !method_name.empty? ? "##{method_name}" : ""
609
750
  "#{object_class_name}#{method_suffix}"
610
751
  end
611
- rescue Exception => e
752
+ rescue
612
753
  '[ErrorGettingName]'
613
754
  end
614
755
  end
@@ -650,8 +791,10 @@ class EnhancedErrors
650
791
  end
651
792
 
652
793
  def awesome_print_available?
653
- return @awesome_print_available unless @awesome_print_available.nil?
654
- @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
655
798
  end
656
799
 
657
800
  def safe_inspect(variable)
@@ -693,7 +836,7 @@ class EnhancedErrors
693
836
  end
694
837
 
695
838
  def default_on_capture(binding_info)
696
- EnhancedErrors.apply_skip_list(binding_info)
839
+ apply_skip_list(binding_info)
697
840
  end
698
841
 
699
842
  def default_eligible_for_capture(exception)
@@ -702,4 +845,4 @@ class EnhancedErrors
702
845
  !ignored && !rspec
703
846
  end
704
847
  end
705
- end
848
+ end