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.
- checksums.yaml +4 -4
- data/README.md +95 -96
- data/doc/images/enhance.png +0 -0
- data/doc/images/enhanced-error.png +0 -0
- data/doc/images/enhanced-spec.png +0 -0
- data/enhanced_errors.gemspec +4 -1
- data/examples/{division_by_zero_example.rb → demo_exception_enhancement.rb} +3 -1
- data/examples/demo_minitest.rb +22 -0
- data/examples/demo_rspec.rb +41 -0
- data/lib/enhanced/minitest_patch.rb +17 -0
- data/lib/enhanced_errors.rb +370 -227
- metadata +23 -5
- data/examples/demo_spec.rb +0 -32
- data/examples/example_spec.rb +0 -47
data/lib/enhanced_errors.rb
CHANGED
@@ -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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
31
|
-
|
31
|
+
RSPEC_SKIP_LIST = [
|
32
|
+
:@__inspect_output,
|
33
|
+
:@__memoized,
|
34
|
+
:@assertion_delegator,
|
35
|
+
:@assertion_instance,
|
32
36
|
:@assertions,
|
33
|
-
:@
|
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
|
-
:@
|
41
|
-
:@
|
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
|
-
:@
|
47
|
-
|
48
|
-
]).freeze
|
47
|
+
:@saved_pool_configs
|
48
|
+
].freeze
|
49
49
|
|
50
|
-
RAILS_SKIP_LIST =
|
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,
|
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
|
-
:@
|
64
|
-
:@
|
65
|
-
:@
|
66
|
-
:@
|
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
|
-
:@
|
70
|
-
:@relation_delegate_cache,
|
71
|
+
:@new_record,
|
71
72
|
:@predicate_builder,
|
72
|
-
:@
|
73
|
-
:@
|
74
|
-
:@
|
73
|
+
:@previously_new_record,
|
74
|
+
:@primary_key,
|
75
|
+
:@readonly,
|
76
|
+
:@relation_delegate_cache,
|
77
|
+
:@response,
|
75
78
|
:@response_klass,
|
76
|
-
|
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)
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
92
|
-
|
93
|
-
@
|
94
|
-
|
95
|
-
@
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
217
|
+
@output_format = nil
|
218
|
+
@eligible_for_capture = nil
|
132
219
|
@original_global_variables = nil
|
133
|
-
@
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
@
|
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
|
-
|
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
|
-
|
172
|
-
target_class.
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
181
|
-
|
182
|
-
RSpec::Core::Example
|
183
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
-
|
417
|
+
mutex.synchronize do
|
418
|
+
@on_capture_hook = value
|
419
|
+
end
|
294
420
|
end
|
295
421
|
|
296
422
|
def on_format(&block)
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
-
|
348
|
-
:json
|
349
|
-
else
|
350
|
-
:terminal
|
351
|
-
end
|
482
|
+
end
|
352
483
|
end
|
353
484
|
|
354
485
|
def running_in_ci?
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
418
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
633
|
+
if on_capture_hook_local
|
488
634
|
begin
|
489
635
|
Thread.current[:on_capture] = true
|
490
|
-
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
|
-
|
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
|
-
|
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
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
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
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
687
|
+
mutex.synchronize do
|
688
|
+
if capture_events.nil?
|
689
|
+
@capture_events = default_capture_events
|
690
|
+
return
|
691
|
+
end
|
543
692
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
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
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
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
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
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
|
-
|
710
|
+
@capture_events = capture_events
|
711
|
+
end
|
562
712
|
end
|
563
713
|
|
564
714
|
def valid_capture_events?(capture_events)
|
565
|
-
|
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
|
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
|
-
|
654
|
-
|
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
|
-
|
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
|