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