enhanced_errors 2.0.6 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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