enhanced_errors 2.0.6 → 2.1.1

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,202 @@ 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)
87
+
88
+ RSPEC_HANDLER_NAMES = ['RSpec::Expectations::PositiveExpectationHandler', 'RSpec::Expectations::NegativeExpectationHandler']
79
89
 
80
90
  @enabled = false
91
+ @max_length = nil
92
+ @capture_rescue = nil
93
+ @skip_list = nil
94
+ @capture_events = nil
95
+ @debug = nil
96
+ @output_format = nil
97
+ @eligible_for_capture = nil
98
+ @original_global_variables = nil
99
+ @trace = nil
100
+ @override_messages = nil
101
+ @rspec_failure_message_loaded = nil
81
102
 
82
- def max_length(value = nil)
83
- if value.nil?
84
- @max_length ||= DEFAULT_MAX_LENGTH
85
- else
86
- @max_length = value
103
+ # Default values
104
+ @max_capture_events = -1 # -1 means no limit
105
+ @capture_events_count = 0
106
+
107
+ # Thread-safe getters and setters
108
+ def enabled=(val)
109
+ mutex.synchronize { @enabled = val }
110
+ end
111
+
112
+ def enabled
113
+ mutex.synchronize { @enabled }
114
+ end
115
+
116
+ def capture_rescue=(val)
117
+ mutex.synchronize { @capture_rescue = val }
118
+ end
119
+
120
+ def capture_rescue
121
+ mutex.synchronize { @capture_rescue }
122
+ end
123
+
124
+ def capture_events_count
125
+ mutex.synchronize { @capture_events_count }
126
+ end
127
+
128
+ def capture_events_count=(val)
129
+ mutex.synchronize { @capture_events_count = val }
130
+ end
131
+
132
+ def max_capture_events
133
+ mutex.synchronize { @max_capture_events }
134
+ end
135
+
136
+ def max_capture_events=(value)
137
+ mutex.synchronize do
138
+ @max_capture_events = value
139
+ if @max_capture_events == 0
140
+ # Disable capturing
141
+ if @enabled
142
+ puts "EnhancedErrors: max_capture_events set to 0, disabling capturing."
143
+ @enabled = false
144
+ @trace&.disable
145
+ @rspec_tracepoint&.disable
146
+ @minitest_trace&.disable
147
+ end
148
+ end
87
149
  end
88
- @max_length
89
150
  end
90
151
 
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
152
+ def increment_capture_events_count
153
+ mutex.synchronize do
154
+ @capture_events_count ||= 0
155
+ @max_capture_events ||= -1
156
+ @capture_events_count += 1
157
+ # Check if we've hit the limit
158
+ if @max_capture_events > 0 && @capture_events_count >= @max_capture_events
159
+ # puts "EnhancedErrors: max_capture_events limit (#{@max_capture_events}) reached, disabling capturing."
160
+ @enabled = false
161
+ end
162
+ end
163
+ end
164
+
165
+ def reset_capture_events_count
166
+ mutex.synchronize do
167
+ @capture_events_count = 0
168
+ @enabled = true
169
+ @rspec_tracepoint.enable if @rspec_tracepoint
170
+ @trace.enable if @trace
171
+ end
172
+ end
173
+
174
+ def max_length(value = nil)
175
+ mutex.synchronize do
176
+ if value.nil?
177
+ @max_length ||= DEFAULT_MAX_LENGTH
178
+ else
179
+ @max_length = value
180
+ end
181
+ @max_length
96
182
  end
97
- @capture_rescue
98
183
  end
99
184
 
100
185
  def skip_list
101
- @skip_list ||= DEFAULT_SKIP_LIST.dup
186
+ mutex.synchronize do
187
+ @skip_list ||= DEFAULT_SKIP_LIST
188
+ end
189
+ end
190
+
191
+ def override_rspec_message(example, binding_or_bindings)
192
+ exception_obj = example.exception
193
+ case exception_obj
194
+ when nil
195
+ return nil
196
+ when RSpec::Core::MultipleExceptionError
197
+ override_exception_message(exception_obj.all_exceptions.first, binding_or_bindings)
198
+ else
199
+ override_exception_message(exception_obj, binding_or_bindings)
200
+ end
201
+
102
202
  end
103
203
 
104
- # takes an exception and bindings, calculates the variables message
105
- # and modifies the exceptions .message to display the variables
106
204
  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)
205
+ return nil unless exception
206
+ rspec_binding = !(binding_or_bindings.nil? || binding_or_bindings.empty?)
207
+ exception_binding = (exception.binding_infos.length > 0)
208
+ has_message = !(exception.respond_to?(:unaltered_message))
209
+ return nil unless (rspec_binding || exception_binding) && has_message
210
+
110
211
  variable_str = EnhancedErrors.format(binding_or_bindings)
111
212
  message_str = exception.message
213
+ if exception.respond_to?(:captured_variables) && !message_str.include?(exception.captured_variables)
214
+ message_str += exception.captured_variables
215
+ end
112
216
  exception.define_singleton_method(:unaltered_message) { message_str }
113
217
  exception.define_singleton_method(:message) do
114
218
  "#{message_str}#{variable_str}"
@@ -117,26 +221,32 @@ class EnhancedErrors
117
221
  end
118
222
 
119
223
  def add_to_skip_list(*vars)
120
- skip_list.merge(vars)
224
+ mutex.synchronize do
225
+ @skip_list.concat(vars)
226
+ end
121
227
  end
122
228
 
123
229
  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
230
+ mutex.synchronize do
231
+ @trace&.disable
232
+ @trace = nil
130
233
 
131
- if !enabled
234
+ @output_format = nil
235
+ @eligible_for_capture = nil
132
236
  @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
237
+ @override_messages = override_messages
238
+
239
+ # Ensure these are not nil
240
+ @max_capture_events = -1 if @max_capture_events.nil?
241
+ @capture_events_count = 0
242
+
243
+ @rspec_failure_message_loaded = true
244
+
245
+ if !enabled
246
+ @original_global_variables = nil
247
+ @enabled = false
248
+ return
249
+ end
140
250
 
141
251
  @enabled = true
142
252
  @debug = debug
@@ -148,8 +258,6 @@ class EnhancedErrors
148
258
  send(setter_method, value)
149
259
  elsif respond_to?(key)
150
260
  send(key, value)
151
- else
152
- # Ignore unknown options
153
261
  end
154
262
  end
155
263
 
@@ -158,75 +266,129 @@ class EnhancedErrors
158
266
 
159
267
  validate_and_set_capture_events(capture_events)
160
268
 
269
+ # If max_capture_events == 0, capturing is off from the start.
270
+ if @max_capture_events == 0
271
+ @enabled = false
272
+ return
273
+ end
274
+
161
275
  events = @capture_events ? @capture_events.to_a : default_capture_events
162
276
  @trace = TracePoint.new(*events) do |tp|
163
277
  handle_tracepoint_event(tp)
164
278
  end
165
279
 
166
- @trace.enable
280
+ # Only enable trace if still enabled and not limited
281
+ if @enabled && (@max_capture_events == -1 || @capture_events_count < @max_capture_events)
282
+ @trace.enable
283
+ end
167
284
  end
168
285
  end
169
286
 
170
287
  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
288
+ mutex.synchronize do
289
+ if defined?(target_class) && target_class.is_a?(Module)
290
+ target_class.prepend(mod)
291
+ true
292
+ else
293
+ false
294
+ end
176
295
  end
177
296
  end
178
297
 
179
298
  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
299
+ mutex.synchronize do
300
+ return if @rspec_failure_message_loaded
301
+ if defined?(RSpec::Core::Example) && !RSpec::Core::Example < Enhanced::Integrations::RSpecErrorFailureMessage
302
+ RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
303
+ @rspec_failure_message_loaded = true
304
+ end
184
305
  end
185
306
  rescue => e
186
307
  puts "Failed to prepend RSpec custom failure message: #{e.message}"
187
308
  end
188
309
 
310
+ def is_a_minitest?(klass)
311
+ klass.ancestors.include?(Minitest::Test) && klass.name != 'Minitest::Test'
312
+ end
313
+
314
+ def start_minitest_binding_capture
315
+ mutex.synchronize do
316
+ @minitest_trace = TracePoint.new(:return) do |tp|
317
+ next unless tp.method_id.to_s.start_with?('test_') && is_a_minitest?(tp.defined_class)
318
+ @minitest_test_binding = tp.binding
319
+ end
320
+ @minitest_trace.enable
321
+ end
322
+ end
323
+
324
+ def stop_minitest_binding_capture
325
+ mutex.synchronize do
326
+ @minitest_trace&.disable
327
+ @minitest_trace = nil
328
+ convert_binding_to_binding_info(@minitest_test_binding) if @minitest_test_binding
329
+ end
330
+ end
331
+
332
+ def class_to_string(klass)
333
+ return '' if klass.nil?
334
+ if klass.singleton_class?
335
+ klass.to_s.match(/#<Class:(.*?)>/)[1]
336
+ else
337
+ klass.to_s
338
+ end
339
+ end
340
+
341
+ def is_rspec_example?(tracepoint)
342
+ tracepoint.method_id.nil? && !(tracepoint.path.include?('rspec')) && tracepoint.path.end_with?('_spec.rb')
343
+ end
344
+
189
345
  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
346
+ mutex.synchronize do
347
+ @rspec_example_binding = nil
348
+ @capture_next_binding = false
349
+ @rspec_tracepoint&.disable
350
+ @enabled = true if @enabled.nil?
351
+
352
+ @rspec_tracepoint = TracePoint.new(:raise, :b_return) do |tp|
353
+ # puts "name #{tp.raised_exception.class.name rescue ''} method:#{tp.method_id} tp.binding:#{tp.binding.local_variables rescue ''}"
354
+ # puts "event: #{tp.event} defined_class#{class_to_string(tp.defined_class)} #{tp.path}:#{tp.lineno} #{tp.callee_id} "
355
+ # This trickery below is to help us identify the anonymous block return we want to grab
356
+ # Very kluge-y and edge cases have grown it, but it works
357
+ if tp.event == :b_return
358
+ if RSPEC_HANDLER_NAMES.include?(class_to_string(tp.defined_class))
359
+ @capture_next_binding = :next
360
+ next
361
+ end
362
+ next unless @capture_next_binding
363
+
364
+ if @capture_next_binding == :next || @capture_next_binding == :next_matching && is_rspec_example?(tp)
365
+ increment_capture_events_count
366
+ @capture_next_binding = false
210
367
  @rspec_example_binding = tp.binding
211
368
  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
369
+ elsif tp.event == :raise
370
+ class_name = tp.raised_exception.class.name
371
+ case class_name
372
+ when 'RSpec::Expectations::ExpectationNotMetError'
373
+ @capture_next_binding = :next_matching
374
+ else
375
+ handle_tracepoint_event(tp)
376
+ end
217
377
  end
218
378
  end
379
+ @rspec_tracepoint.enable
219
380
  end
220
-
221
- @rspec_tracepoint.enable
222
381
  end
223
382
 
224
383
  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
384
+ mutex.synchronize do
385
+ @rspec_tracepoint&.disable
386
+ @rspec_tracepoint = nil
387
+ binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
388
+ @capture_next_binding = false
389
+ @rspec_example_binding = nil
390
+ binding_info
391
+ end
230
392
  end
231
393
 
232
394
  def convert_binding_to_binding_info(b, capture_let_variables: true)
@@ -239,7 +401,6 @@ class EnhancedErrors
239
401
  instance_vars = receiver.instance_variables
240
402
  instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
241
403
 
242
- # Capture let variables only for RSpec captures
243
404
  lets = {}
244
405
  if capture_let_variables && instance_vars.include?(:@__memoized)
245
406
  outer_memoized = receiver.instance_variable_get(:@__memoized)
@@ -268,59 +429,66 @@ class EnhancedErrors
268
429
  capture_event: 'RSpecContext'
269
430
  }
270
431
 
271
- # Apply skip list to remove @__memoized and @__inspect_output from output
272
- # but only after extracting let variables.
273
432
  default_on_capture(binding_info)
274
433
  end
275
434
 
276
435
  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)
436
+ mutex.synchronize do
437
+ if block_given?
438
+ @eligible_for_capture = block
439
+ else
440
+ @eligible_for_capture ||= method(:default_eligible_for_capture)
441
+ end
281
442
  end
282
443
  end
283
444
 
284
445
  def on_capture(&block)
285
- if block_given?
286
- @on_capture_hook = block
287
- else
288
- @on_capture_hook ||= method(:default_on_capture)
446
+ mutex.synchronize do
447
+ if block_given?
448
+ @on_capture_hook = block
449
+ else
450
+ @on_capture_hook ||= method(:default_on_capture)
451
+ end
289
452
  end
290
453
  end
291
454
 
292
455
  def on_capture=(value)
293
- self.on_capture_hook = value
456
+ mutex.synchronize do
457
+ @on_capture_hook = value
458
+ end
294
459
  end
295
460
 
296
461
  def on_format(&block)
297
- if block_given?
298
- @on_format_hook = block
299
- else
300
- @on_format_hook ||= method(:default_on_format)
462
+ mutex.synchronize do
463
+ if block_given?
464
+ @on_format_hook = block
465
+ else
466
+ @on_format_hook ||= method(:default_on_format)
467
+ end
301
468
  end
302
469
  end
303
470
 
304
471
  def on_format=(value)
305
- @on_format_hook = value
472
+ mutex.synchronize do
473
+ @on_format_hook = value
474
+ end
306
475
  end
307
476
 
308
477
  def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
309
478
  return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
310
479
 
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]
480
+ result = binding_infos_array_to_string(captured_binding_infos, output_format)
313
481
 
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 = ''
482
+ mutex.synchronize do
483
+ if @on_format_hook
484
+ begin
485
+ result = @on_format_hook.call(result)
486
+ rescue
487
+ result = ''
488
+ end
489
+ else
490
+ result = default_on_format(result)
321
491
  end
322
- else
323
- result = default_on_format(result)
324
492
  end
325
493
 
326
494
  result
@@ -329,48 +497,55 @@ class EnhancedErrors
329
497
  def binding_infos_array_to_string(captured_bindings, format = :terminal)
330
498
  return '' if captured_bindings.nil? || captured_bindings.empty?
331
499
  captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
332
- Colors.enabled = format == :terminal
500
+ Colors.enabled = (format == :terminal)
333
501
  formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
334
502
  format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
335
503
  end
336
504
 
337
505
  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
506
+ mutex.synchronize do
507
+ return @output_format unless @output_format.nil?
508
+ env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
509
+ @output_format = case env
510
+ when 'development', 'test'
511
+ if running_in_ci?
512
+ :plaintext
513
+ else
514
+ :terminal
515
+ end
516
+ when 'production'
517
+ :json
344
518
  else
345
519
  :terminal
346
520
  end
347
- when 'production'
348
- :json
349
- else
350
- :terminal
351
- end
521
+ end
352
522
  end
353
523
 
354
524
  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' }
525
+ mutex.synchronize do
526
+ return @running_in_ci if defined?(@running_in_ci)
527
+ ci_env_vars = {
528
+ 'CI' => ENV['CI'],
529
+ 'JENKINS' => ENV['JENKINS'],
530
+ 'GITHUB_ACTIONS' => ENV['GITHUB_ACTIONS'],
531
+ 'CIRCLECI' => ENV['CIRCLECI'],
532
+ 'TRAVIS' => ENV['TRAVIS'],
533
+ 'APPVEYOR' => ENV['APPVEYOR'],
534
+ 'GITLAB_CI' => ENV['GITLAB_CI']
535
+ }
536
+ @running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
537
+ end
366
538
  end
367
539
 
368
540
  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) }
541
+ mutex.synchronize do
542
+ variables = binding_info[:variables]
543
+ variables[:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s.start_with?('@_') && !@debug) }
544
+ variables[:locals]&.reject! { |var, _| skip_list.include?(var) }
545
+ if @debug
546
+ variables[:globals]&.reject! { |var, _| skip_list.include?(var) }
547
+ end
548
+ end
374
549
  binding_info
375
550
  end
376
551
 
@@ -405,7 +580,6 @@ class EnhancedErrors
405
580
  result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
406
581
  end
407
582
 
408
- # Display let variables for RSpec captures
409
583
  if variables[:lets] && !variables[:lets].empty?
410
584
  result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
411
585
  end
@@ -414,9 +588,13 @@ class EnhancedErrors
414
588
  result += "\n#{Colors.green('Globals:')}\n#{variable_description(variables[:globals])}"
415
589
  end
416
590
 
417
- if result.length > max_length
418
- result = result[0...max_length] + "... (truncated)"
591
+ mutex.synchronize do
592
+ max_len = @max_length || DEFAULT_MAX_LENGTH
593
+ if result.length > max_len
594
+ result = result[0...max_len] + "... (truncated)"
595
+ end
419
596
  end
597
+
420
598
  result + "\n"
421
599
  rescue => e
422
600
  puts "#{e.message}"
@@ -425,12 +603,18 @@ class EnhancedErrors
425
603
 
426
604
  private
427
605
 
606
+
428
607
  def handle_tracepoint_event(tp)
429
- return unless @enabled
608
+ # Check enabled outside the synchronized block for speed, but still safe due to re-check inside.
609
+ return unless mutex.synchronize { @enabled }
430
610
  return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
611
+
431
612
  Thread.current[:enhanced_errors_processing] = true
432
613
  exception = tp.raised_exception
433
- capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
614
+
615
+ capture_me = mutex.synchronize do
616
+ !exception.frozen? && (@eligible_for_capture || method(:default_eligible_for_capture)).call(exception)
617
+ end
434
618
 
435
619
  unless capture_me
436
620
  Thread.current[:enhanced_errors_processing] = false
@@ -453,19 +637,19 @@ class EnhancedErrors
453
637
  [var, safe_instance_variable_get(binding_context.receiver, var)]
454
638
  }.to_h
455
639
 
456
- # No let variables for exceptions
457
640
  lets = {}
458
641
 
459
642
  globals = {}
460
- if @debug
461
- globals = (global_variables - @original_global_variables).map { |var|
462
- [var, get_global_variable_value(var)]
463
- }.to_h
643
+ mutex.synchronize do
644
+ if @debug
645
+ globals = (global_variables - @original_global_variables.to_a).map { |var|
646
+ [var, get_global_variable_value(var)]
647
+ }.to_h
648
+ end
464
649
  end
465
650
 
466
651
  capture_event = safe_to_s(tp.event)
467
652
  location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
468
-
469
653
  binding_info = {
470
654
  source: location,
471
655
  object: tp.self,
@@ -483,11 +667,12 @@ class EnhancedErrors
483
667
  }
484
668
 
485
669
  binding_info = default_on_capture(binding_info)
670
+ on_capture_hook_local = mutex.synchronize { @on_capture_hook }
486
671
 
487
- if on_capture_hook
672
+ if on_capture_hook_local
488
673
  begin
489
674
  Thread.current[:on_capture] = true
490
- binding_info = on_capture_hook.call(binding_info)
675
+ binding_info = on_capture_hook_local.call(binding_info)
491
676
  rescue
492
677
  binding_info = nil
493
678
  ensure
@@ -497,15 +682,15 @@ class EnhancedErrors
497
682
 
498
683
  if binding_info
499
684
  binding_info = validate_binding_format(binding_info)
500
-
501
685
  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
686
  exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
504
687
  end
505
-
506
688
  if binding_info
507
689
  exception.binding_infos << binding_info
508
- override_exception_message(exception, exception.binding_infos) if @override_messages
690
+ mutex.synchronize do
691
+ override_exception_message(exception, exception.binding_infos) if @override_messages
692
+ end
693
+ increment_capture_events_count
509
694
  end
510
695
  end
511
696
  rescue
@@ -519,52 +704,54 @@ class EnhancedErrors
519
704
  end
520
705
 
521
706
  def test_name
522
- if defined?(RSpec)
523
- RSpec&.current_example&.full_description
707
+ begin
708
+ defined?(RSpec) ? RSpec&.current_example&.full_description : nil
709
+ rescue
710
+ nil
524
711
  end
525
- rescue
526
- nil
527
712
  end
528
713
 
529
714
  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
715
+ mutex.synchronize do
716
+ events = [:raise]
717
+ rescue_available = !!(Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0'))
718
+ if capture_rescue && rescue_available
719
+ events << :rescue
720
+ end
721
+ events
534
722
  end
535
- @default_capture_events = events
536
723
  end
537
724
 
538
725
  def validate_and_set_capture_events(capture_events)
539
- if capture_events.nil?
540
- @capture_events = default_capture_events
541
- return
542
- end
726
+ mutex.synchronize do
727
+ if capture_events.nil?
728
+ @capture_events = default_capture_events
729
+ return
730
+ end
543
731
 
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
732
+ unless valid_capture_events?(capture_events)
733
+ puts "EnhancedErrors: Invalid capture_events provided. Falling back to defaults."
734
+ @capture_events = default_capture_events
735
+ return
736
+ end
549
737
 
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
738
+ if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
739
+ puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
740
+ capture_events = capture_events - [:rescue]
741
+ end
554
742
 
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
743
+ if capture_events.empty?
744
+ puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
745
+ @capture_events = default_capture_events
746
+ return
747
+ end
560
748
 
561
- @capture_events = capture_events.to_set
749
+ @capture_events = capture_events
750
+ end
562
751
  end
563
752
 
564
753
  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)
754
+ capture_events.is_a?(Array) && [:raise, :rescue] && capture_events
568
755
  end
569
756
 
570
757
  def extract_arguments(tp, method_name)
@@ -589,14 +776,7 @@ class EnhancedErrors
589
776
 
590
777
  def determine_object_name(tp, method_name = '')
591
778
  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
779
  self_class = Object.instance_method(:class).bind(tp.self).call
598
-
599
- # Similarly, bind and call `singleton_class` safely
600
780
  singleton_class = Object.instance_method(:singleton_class).bind(tp.self).call
601
781
 
602
782
  if self_class && tp.defined_class == singleton_class
@@ -608,7 +788,7 @@ class EnhancedErrors
608
788
  method_suffix = method_name && !method_name.empty? ? "##{method_name}" : ""
609
789
  "#{object_class_name}#{method_suffix}"
610
790
  end
611
- rescue Exception => e
791
+ rescue
612
792
  '[ErrorGettingName]'
613
793
  end
614
794
  end
@@ -650,8 +830,10 @@ class EnhancedErrors
650
830
  end
651
831
 
652
832
  def awesome_print_available?
653
- return @awesome_print_available unless @awesome_print_available.nil?
654
- @awesome_print_available = defined?(AwesomePrint)
833
+ mutex.synchronize do
834
+ return @awesome_print_available unless @awesome_print_available.nil?
835
+ @awesome_print_available = defined?(AwesomePrint)
836
+ end
655
837
  end
656
838
 
657
839
  def safe_inspect(variable)
@@ -693,7 +875,7 @@ class EnhancedErrors
693
875
  end
694
876
 
695
877
  def default_on_capture(binding_info)
696
- EnhancedErrors.apply_skip_list(binding_info)
878
+ apply_skip_list(binding_info)
697
879
  end
698
880
 
699
881
  def default_eligible_for_capture(exception)
@@ -702,4 +884,4 @@ class EnhancedErrors
702
884
  !ignored && !rspec
703
885
  end
704
886
  end
705
- end
887
+ end