enhanced_errors 2.0.5 → 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 +93 -108
- 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 +373 -219
- metadata +23 -6
- data/examples/demo_spec.rb +0 -32
- data/examples/example_spec.rb +0 -47
- data/lib/enhanced/integrations/rspec_error_failure_message.rb +0 -13
data/lib/enhanced_errors.rb
CHANGED
@@ -2,113 +2,200 @@
|
|
2
2
|
|
3
3
|
require 'set'
|
4
4
|
require 'json'
|
5
|
+
require 'monitor'
|
5
6
|
|
6
|
-
require_relative 'enhanced/integrations/rspec_error_failure_message'
|
7
7
|
require_relative 'enhanced/colors'
|
8
|
+
require_relative 'enhanced/exception'
|
8
9
|
|
9
10
|
IGNORED_EXCEPTIONS = %w[SystemExit NoMemoryError SignalException Interrupt
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
ScriptError LoadError NotImplementedError SyntaxError
|
12
|
+
RSpec::Expectations::ExpectationNotMetError
|
13
|
+
RSpec::Matchers::BuiltIn::RaiseError
|
14
|
+
SystemStackError Psych::BadAlias]
|
14
15
|
|
15
16
|
class EnhancedErrors
|
16
17
|
extend ::Enhanced
|
17
18
|
|
18
19
|
class << self
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
22
25
|
|
23
26
|
GEMS_REGEX = %r{[\/\\]gems[\/\\]}
|
24
27
|
RSPEC_EXAMPLE_REGEXP = /RSpec::ExampleGroups::[A-Z0-9]+.*/
|
25
28
|
DEFAULT_MAX_LENGTH = 2000
|
26
|
-
|
27
|
-
# Maximum binding infos we will track per-exception instance. This is intended as an extra
|
28
|
-
# safety rail, not a normal scenario.
|
29
29
|
MAX_BINDING_INFOS = 3
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
RSPEC_SKIP_LIST = [
|
32
|
+
:@__inspect_output,
|
33
|
+
:@__memoized,
|
34
|
+
:@assertion_delegator,
|
35
|
+
:@assertion_instance,
|
33
36
|
:@assertions,
|
34
|
-
:@
|
37
|
+
:@connection_subscriber,
|
35
38
|
:@example,
|
36
|
-
:@assertion_delegator,
|
37
39
|
:@fixture_cache,
|
38
40
|
:@fixture_cache_key,
|
39
|
-
:@fixture_connections,
|
40
41
|
:@fixture_connection_pools,
|
41
|
-
:@
|
42
|
-
:@
|
43
|
-
:@saved_pool_configs,
|
44
|
-
:@assertion_instance,
|
42
|
+
:@fixture_connections,
|
43
|
+
:@integration_session,
|
45
44
|
:@legacy_saved_pool_configs,
|
45
|
+
:@loaded_fixtures,
|
46
46
|
:@matcher_definitions,
|
47
|
-
:@
|
48
|
-
|
49
|
-
]).freeze
|
47
|
+
:@saved_pool_configs
|
48
|
+
].freeze
|
50
49
|
|
51
|
-
RAILS_SKIP_LIST =
|
50
|
+
RAILS_SKIP_LIST = [
|
52
51
|
:@new_record,
|
53
52
|
:@attributes,
|
54
53
|
:@association_cache,
|
55
54
|
:@readonly,
|
56
55
|
:@previously_new_record,
|
57
|
-
:@_routes,
|
56
|
+
:@_routes,
|
58
57
|
:@routes,
|
59
58
|
:@app,
|
59
|
+
:@arel_table,
|
60
|
+
:@assertion_instance,
|
61
|
+
:@association_cache,
|
62
|
+
:@attributes,
|
60
63
|
:@destroyed,
|
61
|
-
:@response, #usually big, gets truncated anyway
|
62
|
-
:@marked_for_destruction,
|
63
64
|
:@destroyed_by_association,
|
64
|
-
:@
|
65
|
-
:@
|
66
|
-
:@
|
67
|
-
:@
|
65
|
+
:@find_by_statement_cache,
|
66
|
+
:@generated_relation_method,
|
67
|
+
:@integration_session,
|
68
|
+
:@marked_for_destruction,
|
68
69
|
:@mutations_before_last_save,
|
69
70
|
:@mutations_from_database,
|
70
|
-
:@
|
71
|
-
:@relation_delegate_cache,
|
71
|
+
:@new_record,
|
72
72
|
:@predicate_builder,
|
73
|
-
:@
|
74
|
-
:@
|
75
|
-
:@
|
73
|
+
:@previously_new_record,
|
74
|
+
:@primary_key,
|
75
|
+
:@readonly,
|
76
|
+
:@relation_delegate_cache,
|
77
|
+
:@response,
|
76
78
|
:@response_klass,
|
77
|
-
|
79
|
+
:@routes,
|
80
|
+
:@strict_loading,
|
81
|
+
:@strict_loading_mode
|
82
|
+
].freeze
|
83
|
+
|
84
|
+
MINITEST_SKIP_LIST = [:@NAME, :@failures, :@time].freeze
|
78
85
|
|
79
|
-
DEFAULT_SKIP_LIST = (RAILS_SKIP_LIST + RSPEC_SKIP_LIST)
|
86
|
+
DEFAULT_SKIP_LIST = (RAILS_SKIP_LIST + RSPEC_SKIP_LIST + MINITEST_SKIP_LIST)
|
80
87
|
|
81
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
|
82
100
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
88
147
|
end
|
89
|
-
@max_length
|
90
148
|
end
|
91
149
|
|
92
|
-
def
|
93
|
-
|
94
|
-
@
|
95
|
-
|
96
|
-
@
|
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
|
97
178
|
end
|
98
|
-
@capture_rescue
|
99
179
|
end
|
100
180
|
|
101
181
|
def skip_list
|
102
|
-
|
182
|
+
mutex.synchronize do
|
183
|
+
@skip_list ||= DEFAULT_SKIP_LIST
|
184
|
+
end
|
103
185
|
end
|
104
186
|
|
105
|
-
# takes an exception and bindings, calculates the variables message
|
106
|
-
# and modifies the exceptions .message to display the variables
|
107
187
|
def override_exception_message(exception, binding_or_bindings)
|
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,64 +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
|
189
296
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
197
306
|
|
198
|
-
|
199
|
-
|
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
|
200
314
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
315
|
+
def start_rspec_binding_capture
|
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)
|
337
|
+
end
|
206
338
|
end
|
207
339
|
end
|
208
|
-
|
340
|
+
@rspec_tracepoint.enable
|
209
341
|
end
|
210
|
-
|
211
|
-
@rspec_tracepoint.enable
|
212
342
|
end
|
213
343
|
|
214
344
|
def stop_rspec_binding_capture
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
219
353
|
end
|
220
354
|
|
221
355
|
def convert_binding_to_binding_info(b, capture_let_variables: true)
|
@@ -228,7 +362,6 @@ class EnhancedErrors
|
|
228
362
|
instance_vars = receiver.instance_variables
|
229
363
|
instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
|
230
364
|
|
231
|
-
# Capture let variables only for RSpec captures
|
232
365
|
lets = {}
|
233
366
|
if capture_let_variables && instance_vars.include?(:@__memoized)
|
234
367
|
outer_memoized = receiver.instance_variable_get(:@__memoized)
|
@@ -257,59 +390,66 @@ class EnhancedErrors
|
|
257
390
|
capture_event: 'RSpecContext'
|
258
391
|
}
|
259
392
|
|
260
|
-
# Apply skip list to remove @__memoized and @__inspect_output from output
|
261
|
-
# but only after extracting let variables.
|
262
393
|
default_on_capture(binding_info)
|
263
394
|
end
|
264
395
|
|
265
396
|
def eligible_for_capture(&block)
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
270
403
|
end
|
271
404
|
end
|
272
405
|
|
273
406
|
def on_capture(&block)
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
278
413
|
end
|
279
414
|
end
|
280
415
|
|
281
416
|
def on_capture=(value)
|
282
|
-
|
417
|
+
mutex.synchronize do
|
418
|
+
@on_capture_hook = value
|
419
|
+
end
|
283
420
|
end
|
284
421
|
|
285
422
|
def on_format(&block)
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
290
429
|
end
|
291
430
|
end
|
292
431
|
|
293
432
|
def on_format=(value)
|
294
|
-
|
433
|
+
mutex.synchronize do
|
434
|
+
@on_format_hook = value
|
435
|
+
end
|
295
436
|
end
|
296
437
|
|
297
438
|
def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
|
298
439
|
return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
|
299
440
|
|
300
|
-
|
301
|
-
binding_infos = captured_binding_infos.is_a?(Array) ? captured_binding_infos : [captured_binding_infos]
|
302
|
-
|
303
|
-
result = binding_infos_array_to_string(binding_infos, output_format)
|
441
|
+
result = binding_infos_array_to_string(captured_binding_infos, output_format)
|
304
442
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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)
|
310
452
|
end
|
311
|
-
else
|
312
|
-
result = default_on_format(result)
|
313
453
|
end
|
314
454
|
|
315
455
|
result
|
@@ -318,48 +458,55 @@ class EnhancedErrors
|
|
318
458
|
def binding_infos_array_to_string(captured_bindings, format = :terminal)
|
319
459
|
return '' if captured_bindings.nil? || captured_bindings.empty?
|
320
460
|
captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
|
321
|
-
Colors.enabled = format == :terminal
|
461
|
+
Colors.enabled = (format == :terminal)
|
322
462
|
formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
|
323
463
|
format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
|
324
464
|
end
|
325
465
|
|
326
466
|
def get_default_format_for_environment
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
333
479
|
else
|
334
480
|
:terminal
|
335
481
|
end
|
336
|
-
|
337
|
-
:json
|
338
|
-
else
|
339
|
-
:terminal
|
340
|
-
end
|
482
|
+
end
|
341
483
|
end
|
342
484
|
|
343
485
|
def running_in_ci?
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
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
|
355
499
|
end
|
356
500
|
|
357
501
|
def apply_skip_list(binding_info)
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
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
|
363
510
|
binding_info
|
364
511
|
end
|
365
512
|
|
@@ -394,7 +541,6 @@ class EnhancedErrors
|
|
394
541
|
result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
|
395
542
|
end
|
396
543
|
|
397
|
-
# Display let variables for RSpec captures
|
398
544
|
if variables[:lets] && !variables[:lets].empty?
|
399
545
|
result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
|
400
546
|
end
|
@@ -403,9 +549,13 @@ class EnhancedErrors
|
|
403
549
|
result += "\n#{Colors.green('Globals:')}\n#{variable_description(variables[:globals])}"
|
404
550
|
end
|
405
551
|
|
406
|
-
|
407
|
-
|
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
|
408
557
|
end
|
558
|
+
|
409
559
|
result + "\n"
|
410
560
|
rescue => e
|
411
561
|
puts "#{e.message}"
|
@@ -414,12 +564,18 @@ class EnhancedErrors
|
|
414
564
|
|
415
565
|
private
|
416
566
|
|
567
|
+
|
417
568
|
def handle_tracepoint_event(tp)
|
418
|
-
|
569
|
+
# Check enabled outside the synchronized block for speed, but still safe due to re-check inside.
|
570
|
+
return unless mutex.synchronize { @enabled }
|
419
571
|
return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
|
572
|
+
|
420
573
|
Thread.current[:enhanced_errors_processing] = true
|
421
574
|
exception = tp.raised_exception
|
422
|
-
|
575
|
+
|
576
|
+
capture_me = mutex.synchronize do
|
577
|
+
!exception.frozen? && (@eligible_for_capture || method(:default_eligible_for_capture)).call(exception)
|
578
|
+
end
|
423
579
|
|
424
580
|
unless capture_me
|
425
581
|
Thread.current[:enhanced_errors_processing] = false
|
@@ -442,19 +598,19 @@ class EnhancedErrors
|
|
442
598
|
[var, safe_instance_variable_get(binding_context.receiver, var)]
|
443
599
|
}.to_h
|
444
600
|
|
445
|
-
# No let variables for exceptions
|
446
601
|
lets = {}
|
447
602
|
|
448
603
|
globals = {}
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
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
|
453
610
|
end
|
454
611
|
|
455
612
|
capture_event = safe_to_s(tp.event)
|
456
613
|
location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
|
457
|
-
|
458
614
|
binding_info = {
|
459
615
|
source: location,
|
460
616
|
object: tp.self,
|
@@ -472,11 +628,12 @@ class EnhancedErrors
|
|
472
628
|
}
|
473
629
|
|
474
630
|
binding_info = default_on_capture(binding_info)
|
631
|
+
on_capture_hook_local = mutex.synchronize { @on_capture_hook }
|
475
632
|
|
476
|
-
if
|
633
|
+
if on_capture_hook_local
|
477
634
|
begin
|
478
635
|
Thread.current[:on_capture] = true
|
479
|
-
binding_info =
|
636
|
+
binding_info = on_capture_hook_local.call(binding_info)
|
480
637
|
rescue
|
481
638
|
binding_info = nil
|
482
639
|
ensure
|
@@ -486,15 +643,15 @@ class EnhancedErrors
|
|
486
643
|
|
487
644
|
if binding_info
|
488
645
|
binding_info = validate_binding_format(binding_info)
|
489
|
-
|
490
646
|
if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
|
491
|
-
# delete from the middle of the array as the ends are most interesting
|
492
647
|
exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
|
493
648
|
end
|
494
|
-
|
495
649
|
if binding_info
|
496
650
|
exception.binding_infos << binding_info
|
497
|
-
|
651
|
+
mutex.synchronize do
|
652
|
+
override_exception_message(exception, exception.binding_infos) if @override_messages
|
653
|
+
end
|
654
|
+
increment_capture_events_count
|
498
655
|
end
|
499
656
|
end
|
500
657
|
rescue
|
@@ -508,52 +665,54 @@ class EnhancedErrors
|
|
508
665
|
end
|
509
666
|
|
510
667
|
def test_name
|
511
|
-
|
512
|
-
RSpec&.current_example&.full_description
|
668
|
+
begin
|
669
|
+
defined?(RSpec) ? RSpec&.current_example&.full_description : nil
|
670
|
+
rescue
|
671
|
+
nil
|
513
672
|
end
|
514
|
-
rescue
|
515
|
-
nil
|
516
673
|
end
|
517
674
|
|
518
675
|
def default_capture_events
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
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
|
523
683
|
end
|
524
|
-
@default_capture_events = events
|
525
684
|
end
|
526
685
|
|
527
686
|
def validate_and_set_capture_events(capture_events)
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
687
|
+
mutex.synchronize do
|
688
|
+
if capture_events.nil?
|
689
|
+
@capture_events = default_capture_events
|
690
|
+
return
|
691
|
+
end
|
532
692
|
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
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
|
538
698
|
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
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
|
543
703
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
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
|
549
709
|
|
550
|
-
|
710
|
+
@capture_events = capture_events
|
711
|
+
end
|
551
712
|
end
|
552
713
|
|
553
714
|
def valid_capture_events?(capture_events)
|
554
|
-
|
555
|
-
valid_types = [:raise, :rescue].to_set
|
556
|
-
capture_events.to_set.subset?(valid_types)
|
715
|
+
capture_events.is_a?(Array) && [:raise, :rescue] && capture_events
|
557
716
|
end
|
558
717
|
|
559
718
|
def extract_arguments(tp, method_name)
|
@@ -578,14 +737,7 @@ class EnhancedErrors
|
|
578
737
|
|
579
738
|
def determine_object_name(tp, method_name = '')
|
580
739
|
begin
|
581
|
-
# These tricks are used to get around the fact that `tp.self` can be a class that is
|
582
|
-
# wired up with method_missing where every direct call alters the class. This is true
|
583
|
-
# on certain builders or config objects and caused problems.
|
584
|
-
|
585
|
-
# Directly bind and call the `class` method to avoid triggering `method_missing`
|
586
740
|
self_class = Object.instance_method(:class).bind(tp.self).call
|
587
|
-
|
588
|
-
# Similarly, bind and call `singleton_class` safely
|
589
741
|
singleton_class = Object.instance_method(:singleton_class).bind(tp.self).call
|
590
742
|
|
591
743
|
if self_class && tp.defined_class == singleton_class
|
@@ -597,7 +749,7 @@ class EnhancedErrors
|
|
597
749
|
method_suffix = method_name && !method_name.empty? ? "##{method_name}" : ""
|
598
750
|
"#{object_class_name}#{method_suffix}"
|
599
751
|
end
|
600
|
-
rescue
|
752
|
+
rescue
|
601
753
|
'[ErrorGettingName]'
|
602
754
|
end
|
603
755
|
end
|
@@ -621,7 +773,7 @@ class EnhancedErrors
|
|
621
773
|
|
622
774
|
def variable_description(vars_hash)
|
623
775
|
vars_hash.map do |name, value|
|
624
|
-
"
|
776
|
+
" #{Colors.purple(name)}: #{format_variable(value)}\n"
|
625
777
|
end.join
|
626
778
|
rescue
|
627
779
|
''
|
@@ -639,8 +791,10 @@ class EnhancedErrors
|
|
639
791
|
end
|
640
792
|
|
641
793
|
def awesome_print_available?
|
642
|
-
|
643
|
-
|
794
|
+
mutex.synchronize do
|
795
|
+
return @awesome_print_available unless @awesome_print_available.nil?
|
796
|
+
@awesome_print_available = defined?(AwesomePrint)
|
797
|
+
end
|
644
798
|
end
|
645
799
|
|
646
800
|
def safe_inspect(variable)
|
@@ -682,7 +836,7 @@ class EnhancedErrors
|
|
682
836
|
end
|
683
837
|
|
684
838
|
def default_on_capture(binding_info)
|
685
|
-
|
839
|
+
apply_skip_list(binding_info)
|
686
840
|
end
|
687
841
|
|
688
842
|
def default_eligible_for_capture(exception)
|