enhanced_errors 2.0.6 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|