enhanced_errors 3.0.2 → 3.0.4
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.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/benchmark/benchmark.rb +31 -29
- data/benchmark/memory_bench.rb +1 -1
- data/benchmark/result.txt +13 -0
- data/doc/Enhanced/Colors.html +2 -2
- data/doc/Enhanced/Context.html +283 -0
- data/doc/Enhanced/ExceptionBindingInfos.html +255 -0
- data/doc/Enhanced/ExceptionContext.html +397 -0
- data/doc/Enhanced.html +8 -4
- data/doc/EnhancedErrors.html +385 -269
- data/doc/EnhancedExceptionContext.html +15 -15
- data/doc/Exception.html +5 -5
- data/doc/ExceptionBindingInfos.html +2 -2
- data/doc/Minitest.html +3 -3
- data/doc/_index.html +12 -6
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +9 -4
- data/doc/index.html +9 -4
- data/doc/method_list.html +34 -18
- data/doc/top-level-namespace.html +18 -8
- data/enhanced_errors.gemspec +1 -1
- data/lib/enhanced/context.rb +7 -5
- data/lib/enhanced/exception.rb +35 -36
- data/lib/enhanced/exception_context.rb +49 -0
- data/lib/enhanced/minitest_patch.rb +1 -1
- data/lib/enhanced_errors.rb +170 -122
- metadata +8 -9
- data/.yardoc/checksums +0 -6
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/lib/enhanced/enhanced_exception_context.rb +0 -47
data/lib/enhanced/exception.rb
CHANGED
@@ -1,55 +1,54 @@
|
|
1
1
|
# exception.rb
|
2
|
-
require_relative '
|
3
|
-
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
ctx
|
9
|
-
|
2
|
+
require_relative 'exception_context'
|
3
|
+
|
4
|
+
module Enhanced
|
5
|
+
module ExceptionBindingInfos
|
6
|
+
def binding_infos
|
7
|
+
ctx = Enhanced::ExceptionContext.context_for(self)
|
8
|
+
unless ctx
|
9
|
+
ctx = Context.new
|
10
|
+
Enhanced::ExceptionContext.store_context(self, ctx)
|
11
|
+
end
|
12
|
+
ctx.binding_infos
|
10
13
|
end
|
11
|
-
ctx.binding_infos
|
12
|
-
end
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
def captured_variables
|
16
|
+
return '' unless binding_infos&.any?
|
16
17
|
bindings_of_interest = select_binding_infos
|
17
18
|
EnhancedErrors.format(bindings_of_interest)
|
18
|
-
|
19
|
+
rescue
|
19
20
|
''
|
20
21
|
end
|
21
|
-
rescue
|
22
|
-
''
|
23
|
-
end
|
24
22
|
|
25
|
-
|
23
|
+
private
|
26
24
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
def select_binding_infos
|
26
|
+
# Preference:
|
27
|
+
# 1. First 'raise' binding that isn't from a library (gem).
|
28
|
+
# 2. If none, the first binding.
|
29
|
+
# 3. The last 'rescue' binding if available.
|
32
30
|
|
33
|
-
|
31
|
+
bindings_of_interest = []
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
first_app_raise = binding_infos.find do |info|
|
34
|
+
info[:capture_event] == 'raise' && !info[:library]
|
35
|
+
end
|
36
|
+
bindings_of_interest << first_app_raise if first_app_raise
|
39
37
|
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
if bindings_of_interest.empty? && binding_infos.first
|
39
|
+
bindings_of_interest << binding_infos.first
|
40
|
+
end
|
43
41
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
last_rescue = binding_infos.reverse.find do |info|
|
43
|
+
info[:capture_event] == 'rescue'
|
44
|
+
end
|
45
|
+
bindings_of_interest << last_rescue if last_rescue
|
48
46
|
|
49
|
-
|
47
|
+
bindings_of_interest.compact
|
48
|
+
end
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
52
|
class Exception
|
54
|
-
prepend ExceptionBindingInfos
|
53
|
+
prepend Enhanced::ExceptionBindingInfos
|
55
54
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'weakref'
|
2
|
+
|
3
|
+
require_relative 'context'
|
4
|
+
|
5
|
+
require 'weakref'
|
6
|
+
require 'monitor'
|
7
|
+
|
8
|
+
module Enhanced
|
9
|
+
module ExceptionContext
|
10
|
+
extend self
|
11
|
+
|
12
|
+
REGISTRY = {}
|
13
|
+
MUTEX = Monitor.new
|
14
|
+
|
15
|
+
def store_context(exception, context)
|
16
|
+
MUTEX.synchronize do
|
17
|
+
REGISTRY[exception.object_id] = { weak_exc: WeakRef.new(exception), context: context }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def context_for(exception)
|
22
|
+
MUTEX.synchronize do
|
23
|
+
entry = REGISTRY[exception.object_id]
|
24
|
+
return nil unless entry
|
25
|
+
|
26
|
+
begin
|
27
|
+
_ = entry[:weak_exc].__getobj__ # ensure exception is still alive
|
28
|
+
entry[:context]
|
29
|
+
rescue RefError
|
30
|
+
# Exception no longer alive, clean up
|
31
|
+
REGISTRY.delete(exception.object_id)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear_context(exception)
|
38
|
+
MUTEX.synchronize do
|
39
|
+
REGISTRY.delete(exception.object_id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear_all
|
44
|
+
MUTEX.synchronize do
|
45
|
+
REGISTRY.clear
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -9,7 +9,7 @@ module Minitest
|
|
9
9
|
begin
|
10
10
|
binding_infos = EnhancedErrors.stop_minitest_binding_capture
|
11
11
|
EnhancedErrors.override_exception_message(result.failures.last, binding_infos) if result.failures.any?
|
12
|
-
|
12
|
+
Enhanced::ExceptionContext.clear_all
|
13
13
|
rescue => e
|
14
14
|
puts "Ignored error during error enhancement: #{e}"
|
15
15
|
end
|
data/lib/enhanced_errors.rb
CHANGED
@@ -4,12 +4,12 @@ require 'set'
|
|
4
4
|
require 'json'
|
5
5
|
require 'monitor'
|
6
6
|
|
7
|
-
|
8
|
-
require_relative 'enhanced/exception'
|
7
|
+
module Enhanced; end
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
RSpec::Matchers::BuiltIn::RaiseError
|
9
|
+
# Exceptions we could handle but overlook for other reasons. These class constants are not always loaded
|
10
|
+
# and generally are only be available when `required`, so we detect them by strings.
|
11
|
+
IGNORED_EXCEPTIONS = %w[RSpec::Expectations::ExpectationNotMetError RSpec::Matchers::BuiltIn::RaiseError
|
12
|
+
JSON::ParserError Zlib::Error OpenSSL::SSL::SSLError Psych::Exception]
|
13
13
|
|
14
14
|
class EnhancedErrors
|
15
15
|
extend ::Enhanced
|
@@ -45,13 +45,8 @@ class EnhancedErrors
|
|
45
45
|
].freeze
|
46
46
|
|
47
47
|
RAILS_SKIP_LIST = [
|
48
|
-
:@new_record,
|
49
|
-
:@attributes,
|
50
48
|
:@association_cache,
|
51
|
-
:@readonly,
|
52
|
-
:@previously_new_record,
|
53
49
|
:@_routes,
|
54
|
-
:@routes,
|
55
50
|
:@app,
|
56
51
|
:@arel_table,
|
57
52
|
:@assertion_instance,
|
@@ -127,7 +122,7 @@ class EnhancedErrors
|
|
127
122
|
mutex.synchronize { @max_capture_length || DEFAULT_MAX_CAPTURE_LENGTH }
|
128
123
|
end
|
129
124
|
|
130
|
-
def max_capture_length=(
|
125
|
+
def max_capture_length=(value)
|
131
126
|
mutex.synchronize { @max_capture_length = value }
|
132
127
|
end
|
133
128
|
|
@@ -140,7 +135,6 @@ class EnhancedErrors
|
|
140
135
|
end
|
141
136
|
end
|
142
137
|
|
143
|
-
|
144
138
|
def reset!
|
145
139
|
mutex.synchronize do
|
146
140
|
@rspec_tracepoint&.disable
|
@@ -155,31 +149,28 @@ class EnhancedErrors
|
|
155
149
|
|
156
150
|
def skip_list
|
157
151
|
mutex.synchronize do
|
158
|
-
@skip_list ||= DEFAULT_SKIP_LIST
|
152
|
+
@skip_list ||= DEFAULT_SKIP_LIST.to_set
|
159
153
|
end
|
160
154
|
end
|
161
155
|
|
162
156
|
def override_rspec_message(example, binding_or_bindings)
|
163
157
|
exception_obj = example.exception
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
158
|
+
return if exception_obj.nil?
|
159
|
+
|
160
|
+
from_bindings = [binding_or_bindings].flatten.compact
|
161
|
+
case exception_obj.class.to_s
|
162
|
+
when 'RSpec::Core::MultipleExceptionError'
|
168
163
|
exception_obj.all_exceptions.each do |exception|
|
169
|
-
override_exception_message(exception,
|
164
|
+
override_exception_message(exception, from_bindings + exception.binding_infos)
|
170
165
|
end
|
171
|
-
|
166
|
+
when 'RSpec::Expectations::ExpectationNotMetError'
|
172
167
|
override_exception_message(exception_obj, binding_or_bindings)
|
168
|
+
else
|
169
|
+
override_exception_message(exception_obj, from_bindings + exception_obj.binding_infos)
|
173
170
|
end
|
174
171
|
end
|
175
172
|
|
176
173
|
def override_exception_message(exception, binding_or_bindings)
|
177
|
-
return nil unless exception && exception.respond_to?(:message)
|
178
|
-
test_binding = !(binding_or_bindings.nil? || binding_or_bindings.empty?)
|
179
|
-
exception_binding = (exception.binding_infos.length > 0)
|
180
|
-
has_message = !(exception.respond_to?(:unaltered_message))
|
181
|
-
return nil unless (test_binding || exception_binding) && has_message
|
182
|
-
|
183
174
|
variable_str = EnhancedErrors.format(binding_or_bindings)
|
184
175
|
message_str = exception.message
|
185
176
|
exception.define_singleton_method(:unaltered_message) { message_str }
|
@@ -190,12 +181,13 @@ class EnhancedErrors
|
|
190
181
|
|
191
182
|
def add_to_skip_list(*vars)
|
192
183
|
mutex.synchronize do
|
193
|
-
@skip_list.
|
184
|
+
@skip_list.add(*vars)
|
194
185
|
end
|
195
186
|
end
|
196
187
|
|
197
188
|
def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
|
198
189
|
mutex.synchronize do
|
190
|
+
ensure_extensions_are_required
|
199
191
|
@exception_trace&.disable
|
200
192
|
@exception_trace = nil
|
201
193
|
|
@@ -226,10 +218,11 @@ class EnhancedErrors
|
|
226
218
|
|
227
219
|
events = @capture_events ? @capture_events.to_a : default_capture_events
|
228
220
|
@exception_trace = TracePoint.new(*events) do |tp|
|
221
|
+
return unless exception_is_handleable?(tp.raised_exception)
|
229
222
|
handle_tracepoint_event(tp)
|
230
223
|
end
|
231
224
|
|
232
|
-
@exception_trace
|
225
|
+
@exception_trace&.enable if @enabled
|
233
226
|
end
|
234
227
|
end
|
235
228
|
|
@@ -238,7 +231,8 @@ class EnhancedErrors
|
|
238
231
|
end
|
239
232
|
|
240
233
|
def start_minitest_binding_capture
|
241
|
-
|
234
|
+
ensure_extensions_are_required
|
235
|
+
Enhanced::ExceptionContext.clear_all
|
242
236
|
@enabled = true if @enabled.nil?
|
243
237
|
return unless @enabled
|
244
238
|
mutex.synchronize do
|
@@ -246,7 +240,7 @@ class EnhancedErrors
|
|
246
240
|
next unless tp.method_id.to_s.start_with?('test_') && is_a_minitest?(tp.defined_class)
|
247
241
|
@minitest_test_binding = tp.binding
|
248
242
|
end
|
249
|
-
@minitest_trace
|
243
|
+
@minitest_trace&.enable
|
250
244
|
end
|
251
245
|
end
|
252
246
|
|
@@ -272,7 +266,8 @@ class EnhancedErrors
|
|
272
266
|
end
|
273
267
|
|
274
268
|
def start_rspec_binding_capture
|
275
|
-
|
269
|
+
ensure_extensions_are_required
|
270
|
+
Enhanced::ExceptionContext.clear_all
|
276
271
|
@enabled = true if @enabled.nil?
|
277
272
|
return unless @enabled
|
278
273
|
|
@@ -280,34 +275,43 @@ class EnhancedErrors
|
|
280
275
|
@rspec_example_binding = nil
|
281
276
|
@capture_next_binding = false
|
282
277
|
@rspec_tracepoint&.disable
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
@capture_next_binding = :next
|
292
|
-
next
|
293
|
-
end
|
294
|
-
next unless @capture_next_binding
|
295
|
-
if @capture_next_binding == :next || @capture_next_binding == :next_matching && is_rspec_example?(tp)
|
296
|
-
@capture_next_binding = false
|
297
|
-
@rspec_example_binding = tp.binding
|
298
|
-
end
|
299
|
-
elsif tp.event == :raise
|
300
|
-
class_name = tp.raised_exception.class.name
|
301
|
-
case class_name
|
302
|
-
when 'RSpec::Expectations::ExpectationNotMetError'
|
303
|
-
@capture_next_binding = :next_matching
|
304
|
-
else
|
305
|
-
handle_tracepoint_event(tp)
|
306
|
-
end
|
278
|
+
@rspec_tracepoint = TracePoint.new(:raise) do |tp|
|
279
|
+
return unless exception_is_handleable?(tp.raised_exception)
|
280
|
+
class_name = tp.raised_exception.class.name
|
281
|
+
case class_name
|
282
|
+
when 'RSpec::Expectations::ExpectationNotMetError'
|
283
|
+
start_rspec_binding_trap
|
284
|
+
else
|
285
|
+
handle_tracepoint_event(tp)
|
307
286
|
end
|
308
287
|
end
|
309
|
-
@rspec_tracepoint.enable
|
310
288
|
end
|
289
|
+
@rspec_tracepoint&.enable
|
290
|
+
end
|
291
|
+
|
292
|
+
# Behavior: Grabs the next rspec spec binding that goes by, and stops the more-expensive b_return trace.
|
293
|
+
# This part of RSpec has been stable, since 2015, so although this is kluge-y, it is stable.
|
294
|
+
# The optimization does a 2-3x on spec speed vs. opening up the Tracepoint. With it,
|
295
|
+
# things are pretty close in speed to plain rspec.
|
296
|
+
# Should the behavior change this can be updated by using a trace to print out items
|
297
|
+
# and their local variables then, find the exception or call that goes by right
|
298
|
+
# before the spec blocks with the variables, and use that to narrow-down the costly part of
|
299
|
+
# the probe to just this point in time. The good news is that
|
300
|
+
# this part is test-time only, and this optimization and kluge only applies to RSpec.
|
301
|
+
def start_rspec_binding_trap
|
302
|
+
@rspec_binding_trap = TracePoint.new(:b_return) do |tp|
|
303
|
+
# kluge-y hack and will be a pain to maintain
|
304
|
+
if tp.callee_id == :handle_matcher
|
305
|
+
@capture_next_binding = :next
|
306
|
+
next
|
307
|
+
end
|
308
|
+
next unless @capture_next_binding
|
309
|
+
@capture_next_binding = false
|
310
|
+
@rspec_example_binding = tp.binding
|
311
|
+
@rspec_binding_trap&.disable
|
312
|
+
@rspec_binding_trap = nil
|
313
|
+
end
|
314
|
+
@rspec_binding_trap&.enable
|
311
315
|
end
|
312
316
|
|
313
317
|
def stop_rspec_binding_capture
|
@@ -328,6 +332,8 @@ class EnhancedErrors
|
|
328
332
|
|
329
333
|
locals = b.local_variables.map { |var| [var, safe_local_variable_get(b, var)] }.to_h
|
330
334
|
receiver = b.receiver
|
335
|
+
return unless safe_to_inspect?(receiver)
|
336
|
+
|
331
337
|
instance_vars = receiver.instance_variables
|
332
338
|
instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
|
333
339
|
|
@@ -356,7 +362,7 @@ class EnhancedErrors
|
|
356
362
|
globals: {}
|
357
363
|
},
|
358
364
|
exception: 'NoException',
|
359
|
-
capture_event: '
|
365
|
+
capture_event: 'test_context'
|
360
366
|
}
|
361
367
|
|
362
368
|
default_on_capture(binding_info)
|
@@ -438,11 +444,7 @@ class EnhancedErrors
|
|
438
444
|
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
439
445
|
@output_format = case env
|
440
446
|
when 'development', 'test'
|
441
|
-
|
442
|
-
:plaintext
|
443
|
-
else
|
444
|
-
:terminal
|
445
|
-
end
|
447
|
+
running_in_ci? ? :plaintext : :terminal
|
446
448
|
when 'production'
|
447
449
|
:json
|
448
450
|
else
|
@@ -454,28 +456,32 @@ class EnhancedErrors
|
|
454
456
|
def running_in_ci?
|
455
457
|
mutex.synchronize do
|
456
458
|
return @running_in_ci if defined?(@running_in_ci)
|
457
|
-
|
458
459
|
@running_in_ci = CI_ENV_VARS.any? { |_, value| value.to_s.downcase == 'true' }
|
459
460
|
end
|
460
461
|
end
|
461
462
|
|
462
463
|
def apply_skip_list(binding_info)
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
variables[:
|
467
|
-
if @debug
|
468
|
-
variables[:globals]&.reject! { |var, _| skip_list.include?(var) }
|
469
|
-
end
|
464
|
+
binding_info[:variables][:instances]&.reject! { |var, _| skip_list.include?(var) || (var.to_s[0, 2] == '@_' && !@debug) }
|
465
|
+
binding_info[:variables][:locals]&.reject! { |var, _| skip_list.include?(var) }
|
466
|
+
if @debug
|
467
|
+
binding_info[:variables][:globals]&.reject! { |var, _| skip_list.include?(var) }
|
470
468
|
end
|
471
469
|
binding_info
|
472
470
|
end
|
473
471
|
|
474
472
|
def validate_binding_format(binding_info)
|
475
|
-
|
476
|
-
|
473
|
+
binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
|
474
|
+
end
|
475
|
+
|
476
|
+
# Here, we are detecting BasicObject, which is surprisingly annoying.
|
477
|
+
# We also, importantly, need to detect descendants, as they will also present with a
|
478
|
+
# lack of :respond_to? and any other useful method for us.
|
479
|
+
def safe_to_inspect?(obj)
|
480
|
+
begin
|
481
|
+
obj.class
|
482
|
+
rescue NoMethodError
|
483
|
+
return false
|
477
484
|
end
|
478
|
-
binding_info
|
479
485
|
end
|
480
486
|
|
481
487
|
def binding_info_string(binding_info)
|
@@ -525,7 +531,6 @@ class EnhancedErrors
|
|
525
531
|
|
526
532
|
private
|
527
533
|
|
528
|
-
|
529
534
|
def handle_tracepoint_event(tp)
|
530
535
|
# Check enabled outside the synchronized block for speed, but still safe due to re-check inside.
|
531
536
|
return unless enabled
|
@@ -534,9 +539,13 @@ class EnhancedErrors
|
|
534
539
|
Thread.current[:enhanced_errors_processing] = true
|
535
540
|
exception = tp.raised_exception
|
536
541
|
|
537
|
-
|
538
|
-
|
539
|
-
|
542
|
+
return if exception.frozen?
|
543
|
+
|
544
|
+
capture_me = if @eligible_for_capture
|
545
|
+
@eligible_for_capture.call(exception)
|
546
|
+
else
|
547
|
+
default_eligible_for_capture(exception)
|
548
|
+
end
|
540
549
|
|
541
550
|
unless capture_me
|
542
551
|
Thread.current[:enhanced_errors_processing] = false
|
@@ -545,32 +554,43 @@ class EnhancedErrors
|
|
545
554
|
|
546
555
|
binding_context = tp.binding
|
547
556
|
method_name = tp.method_id
|
557
|
+
|
558
|
+
locals = {}
|
559
|
+
|
560
|
+
binding_context.local_variables.each do |var|
|
561
|
+
locals[var] = safe_local_variable_get(binding_context, var)
|
562
|
+
end
|
563
|
+
|
548
564
|
method_and_args = {
|
549
565
|
object_name: determine_object_name(tp, method_name),
|
550
|
-
args: extract_arguments(tp, method_name)
|
566
|
+
args: extract_arguments(tp, method_name, locals)
|
551
567
|
}
|
552
568
|
|
553
|
-
|
554
|
-
|
555
|
-
}.to_h
|
569
|
+
instances = {}
|
570
|
+
receiver = binding_context.receiver
|
556
571
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
572
|
+
begin
|
573
|
+
if safe_to_inspect?(receiver)
|
574
|
+
receiver.instance_variables.each { |var|
|
575
|
+
instances[var] = safe_instance_variable_get(receiver, var)
|
576
|
+
}
|
577
|
+
end
|
578
|
+
rescue => e
|
579
|
+
puts "#{e.class.name} #{e.backtrace}"
|
580
|
+
end
|
561
581
|
|
562
582
|
lets = {}
|
563
583
|
|
564
584
|
globals = {}
|
565
585
|
mutex.synchronize do
|
566
586
|
if @debug
|
567
|
-
globals = (global_variables - @original_global_variables.to_a).
|
568
|
-
[var
|
569
|
-
|
587
|
+
globals = (global_variables - @original_global_variables.to_a).each do |var|
|
588
|
+
globals[var] = get_global_variable_value(var)
|
589
|
+
end
|
570
590
|
end
|
571
591
|
end
|
572
592
|
|
573
|
-
capture_event =
|
593
|
+
capture_event = tp.event.to_s
|
574
594
|
location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
|
575
595
|
binding_info = {
|
576
596
|
source: location,
|
@@ -584,17 +604,16 @@ class EnhancedErrors
|
|
584
604
|
lets: lets,
|
585
605
|
globals: globals
|
586
606
|
},
|
587
|
-
exception:
|
607
|
+
exception: exception.class.name,
|
588
608
|
capture_event: capture_event
|
589
609
|
}
|
590
610
|
|
591
611
|
binding_info = default_on_capture(binding_info)
|
592
|
-
on_capture_hook_local = mutex.synchronize { @on_capture_hook }
|
593
612
|
|
594
|
-
if
|
613
|
+
if on_capture_hook
|
595
614
|
begin
|
596
615
|
Thread.current[:on_capture] = true
|
597
|
-
binding_info =
|
616
|
+
binding_info = on_capture_hook.call(binding_info)
|
598
617
|
rescue
|
599
618
|
binding_info = nil
|
600
619
|
ensure
|
@@ -602,20 +621,16 @@ class EnhancedErrors
|
|
602
621
|
end
|
603
622
|
end
|
604
623
|
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
|
609
|
-
end
|
610
|
-
if binding_info
|
611
|
-
exception.binding_infos << binding_info
|
612
|
-
mutex.synchronize do
|
613
|
-
override_exception_message(exception, exception.binding_infos) if @override_messages
|
614
|
-
end
|
615
|
-
end
|
624
|
+
return unless binding_info && validate_binding_format(binding_info)
|
625
|
+
if exception.binding_infos.length >= MAX_BINDING_INFOS
|
626
|
+
exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
|
616
627
|
end
|
617
|
-
|
618
|
-
|
628
|
+
exception.binding_infos << binding_info
|
629
|
+
mutex.synchronize do
|
630
|
+
override_exception_message(exception, exception.binding_infos) if @override_messages
|
631
|
+
end
|
632
|
+
rescue => e
|
633
|
+
puts "Error: #{e&.class&.name} #{e&.backtrace}"
|
619
634
|
ensure
|
620
635
|
Thread.current[:enhanced_errors_processing] = false
|
621
636
|
end
|
@@ -675,17 +690,15 @@ class EnhancedErrors
|
|
675
690
|
capture_events.is_a?(Array) && capture_events.all? { |ev| [:raise, :rescue].include?(ev) }
|
676
691
|
end
|
677
692
|
|
678
|
-
def extract_arguments(tp, method_name)
|
693
|
+
def extract_arguments(tp, method_name, local_vars_hash)
|
679
694
|
return '' unless method_name
|
680
695
|
begin
|
681
|
-
bind = tp.binding
|
682
696
|
unbound_method = tp.defined_class.instance_method(method_name)
|
683
697
|
method_obj = unbound_method.bind(tp.self)
|
684
698
|
parameters = method_obj.parameters
|
685
|
-
locals = bind.local_variables
|
686
699
|
|
687
700
|
parameters.map do |(_, name)|
|
688
|
-
value =
|
701
|
+
value = local_vars_hash[name]
|
689
702
|
"#{name}=#{safe_inspect(value)}"
|
690
703
|
rescue => e
|
691
704
|
"#{name}=[Error getting argument: #{e.message}]"
|
@@ -697,17 +710,20 @@ class EnhancedErrors
|
|
697
710
|
|
698
711
|
def determine_object_name(tp, method_name = '')
|
699
712
|
begin
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
713
|
+
# Check if we're dealing with a singleton method
|
714
|
+
if (
|
715
|
+
class << tp.self
|
716
|
+
self;
|
717
|
+
end) == tp.defined_class
|
718
|
+
# Singleton method call
|
719
|
+
object_str = safe_to_s(tp.self)
|
720
|
+
method_suffix = method_name.to_s.empty? ? '' : ".#{method_name}"
|
721
|
+
"#{object_str}#{method_suffix}"
|
707
722
|
else
|
708
|
-
|
709
|
-
|
710
|
-
"
|
723
|
+
# Instance method call
|
724
|
+
klass_name = safe_to_s(tp.self.class.name || 'UnknownClass')
|
725
|
+
method_suffix = method_name.to_s.empty? ? '' : "##{method_name}"
|
726
|
+
"#{klass_name}#{method_suffix}"
|
711
727
|
end
|
712
728
|
rescue
|
713
729
|
'[ErrorGettingName]'
|
@@ -799,10 +815,42 @@ class EnhancedErrors
|
|
799
815
|
apply_skip_list(binding_info)
|
800
816
|
end
|
801
817
|
|
818
|
+
# By default, we have filtering for safety, but past that, we capture everything by default
|
819
|
+
# at the moment.
|
802
820
|
def default_eligible_for_capture(exception)
|
803
|
-
|
804
|
-
rspec = exception.class.name.start_with?('RSpec::Matchers')
|
805
|
-
!ignored && !rspec
|
821
|
+
true
|
806
822
|
end
|
823
|
+
|
824
|
+
def exception_is_handleable?(exception)
|
825
|
+
case exception
|
826
|
+
when SystemExit, SignalException, SystemStackError, NoMemoryError
|
827
|
+
# Non-actionable: Ignore these exceptions
|
828
|
+
false
|
829
|
+
when SyntaxError, LoadError, ScriptError
|
830
|
+
# Non-actionable: Structural issues so there's no useful runtime context
|
831
|
+
false
|
832
|
+
else
|
833
|
+
# Ignore internal fatal errors
|
834
|
+
exception.class.to_s != 'fatal'
|
835
|
+
end
|
836
|
+
end
|
837
|
+
|
838
|
+
# This prevents loading it for say, production, if you don't want to,
|
839
|
+
# and keeps things cleaner. It allows a path to put this behind a feature-flag
|
840
|
+
# or env variable, and dynamically enable some capture instrumentation only
|
841
|
+
# when a Heisenbug is being hunted.
|
842
|
+
def ensure_extensions_are_required
|
843
|
+
mutex.synchronize do
|
844
|
+
return if @loaded_required_extensions
|
845
|
+
require_relative 'enhanced/colors'
|
846
|
+
require_relative 'enhanced/exception'
|
847
|
+
@loaded_required_extensions = true
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
807
851
|
end
|
808
852
|
end
|
853
|
+
|
854
|
+
module Enhanced
|
855
|
+
|
856
|
+
end
|