enhanced_errors 0.1.8 → 2.0.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 +104 -29
- data/benchmark/benchmark.rb +4 -4
- data/benchmark/stackprofile.rb +3 -3
- data/enhanced_errors.gemspec +2 -2
- data/examples/demo_spec.rb +32 -0
- data/examples/division_by_zero_example.rb +2 -2
- data/examples/example_spec.rb +21 -1
- data/lib/enhanced/colors.rb +30 -0
- data/lib/enhanced/exception.rb +45 -0
- data/lib/enhanced/integrations/rspec_error_failure_message.rb +13 -0
- data/lib/enhanced_errors.rb +262 -376
- metadata +8 -7
- data/lib/binding.rb +0 -12
- data/lib/colors.rb +0 -27
- data/lib/error_enhancements.rb +0 -64
data/lib/enhanced_errors.rb
CHANGED
@@ -3,86 +3,32 @@
|
|
3
3
|
require 'set'
|
4
4
|
require 'json'
|
5
5
|
|
6
|
-
require_relative '
|
7
|
-
require_relative '
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
ScriptError,
|
17
|
-
LoadError,
|
18
|
-
NotImplementedError,
|
19
|
-
SyntaxError,
|
20
|
-
SystemStackError
|
21
|
-
]
|
22
|
-
|
23
|
-
# The EnhancedErrors class provides mechanisms to enhance exception handling by capturing
|
24
|
-
# additional context such as binding information, variables, and method arguments when exceptions are raised.
|
25
|
-
# It offers customization options for formatting and filtering captured data.
|
6
|
+
require_relative 'enhanced/integrations/rspec_error_failure_message'
|
7
|
+
require_relative 'enhanced/colors'
|
8
|
+
|
9
|
+
IGNORED_EXCEPTIONS = %w[SystemExit NoMemoryError SignalException Interrupt
|
10
|
+
ScriptError LoadError NotImplementedError SyntaxError
|
11
|
+
RSpec::Expectations::ExpectationNotMetError
|
12
|
+
RSpec::Matchers::BuiltIn::RaiseError
|
13
|
+
SystemStackError Psych::BadAlias]
|
14
|
+
|
15
|
+
|
26
16
|
class EnhancedErrors
|
17
|
+
extend ::Enhanced
|
18
|
+
|
27
19
|
class << self
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
attr_accessor :enabled
|
32
|
-
|
33
|
-
# The TracePoint object used for tracing exceptions.
|
34
|
-
#
|
35
|
-
# @return [TracePoint, nil]
|
36
|
-
attr_accessor :trace
|
37
|
-
|
38
|
-
# The configuration block provided during enhancement.
|
39
|
-
#
|
40
|
-
# @return [Proc, nil]
|
41
|
-
attr_accessor :config_block
|
42
|
-
|
43
|
-
# The maximum length of the formatted exception message.
|
44
|
-
#
|
45
|
-
# @return [Integer]
|
46
|
-
attr_accessor :max_length
|
47
|
-
|
48
|
-
# Hook to modify binding information upon capture.
|
49
|
-
#
|
50
|
-
# @return [Proc, nil]
|
51
|
-
attr_accessor :on_capture_hook
|
52
|
-
|
53
|
-
# Determines whether RSpec `let` variables are captured.
|
54
|
-
#
|
55
|
-
# @return [Boolean]
|
56
|
-
attr_accessor :capture_let_variables
|
57
|
-
|
58
|
-
# A proc that determines if an exception is eligible for capture.
|
59
|
-
#
|
60
|
-
# @return [Proc, nil]
|
61
|
-
attr_accessor :eligible_for_capture
|
62
|
-
|
63
|
-
# A set of variable names to exclude from binding information.
|
64
|
-
#
|
65
|
-
# @return [Set<Symbol>]
|
66
|
-
attr_accessor :skip_list
|
67
|
-
|
68
|
-
# Determines whether to capture :rescue events.
|
69
|
-
#
|
70
|
-
# @return [Boolean]
|
71
|
-
attr_accessor :capture_rescue
|
72
|
-
|
73
|
-
# Regular expression to identify gem paths.
|
74
|
-
#
|
75
|
-
# @return [Regexp]
|
76
|
-
GEMS_REGEX = %r{[\/\\]gems[\/\\]}
|
20
|
+
attr_accessor :enabled, :config_block, :max_length, :on_capture_hook,
|
21
|
+
:eligible_for_capture, :trace, :skip_list, :capture_rescue,
|
22
|
+
:override_messages
|
77
23
|
|
78
|
-
|
79
|
-
#
|
80
|
-
# @return [Integer]
|
24
|
+
GEMS_REGEX = %r{[\/\\]gems[\/\\]}
|
81
25
|
DEFAULT_MAX_LENGTH = 2500
|
82
26
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
|
27
|
+
# Maximum binding infos we will track per-exception instance. This is intended as an extra
|
28
|
+
# safety rail, not a normal scenario.
|
29
|
+
MAX_BINDING_INFOS = 10
|
30
|
+
|
31
|
+
# Add @__memoized and @__inspect_output to the skip list so they don't appear in output
|
86
32
|
RSPEC_SKIP_LIST = Set.new([
|
87
33
|
:@fixture_cache,
|
88
34
|
:@fixture_cache_key,
|
@@ -90,12 +36,11 @@ class EnhancedErrors
|
|
90
36
|
:@connection_subscriber,
|
91
37
|
:@saved_pool_configs,
|
92
38
|
:@loaded_fixtures,
|
93
|
-
:@matcher_definitions
|
39
|
+
:@matcher_definitions,
|
40
|
+
:@__memoized, # Added to skip from output
|
41
|
+
:@__inspect_output # Added to skip from output
|
94
42
|
])
|
95
43
|
|
96
|
-
# A set of Rails-specific instance variables to skip.
|
97
|
-
#
|
98
|
-
# @return [Set<Symbol>]
|
99
44
|
RAILS_SKIP_LIST = Set.new([
|
100
45
|
:@new_record,
|
101
46
|
:@attributes,
|
@@ -117,10 +62,8 @@ class EnhancedErrors
|
|
117
62
|
:@arel_table
|
118
63
|
])
|
119
64
|
|
120
|
-
|
121
|
-
|
122
|
-
# @param value [Integer, nil] The desired maximum length. If `nil`, returns the current value.
|
123
|
-
# @return [Integer] The maximum length for the formatted message.
|
65
|
+
@enabled = false
|
66
|
+
|
124
67
|
def max_length(value = nil)
|
125
68
|
if value.nil?
|
126
69
|
@max_length ||= DEFAULT_MAX_LENGTH
|
@@ -130,23 +73,6 @@ class EnhancedErrors
|
|
130
73
|
@max_length
|
131
74
|
end
|
132
75
|
|
133
|
-
# Gets or sets whether to capture RSpec `let` variables.
|
134
|
-
#
|
135
|
-
# @param value [Boolean, nil] The desired state. If `nil`, returns the current value.
|
136
|
-
# @return [Boolean] Whether RSpec `let` variables are being captured.
|
137
|
-
def capture_let_variables(value = nil)
|
138
|
-
if value.nil?
|
139
|
-
@capture_let_variables = @capture_let_variables.nil? ? true : @capture_let_variables
|
140
|
-
else
|
141
|
-
@capture_let_variables = value
|
142
|
-
end
|
143
|
-
@capture_let_variables
|
144
|
-
end
|
145
|
-
|
146
|
-
# Gets or sets whether to capture :rescue events.
|
147
|
-
#
|
148
|
-
# @param value [Boolean, nil] The desired state. If `nil`, returns the current value.
|
149
|
-
# @return [Boolean] Whether :rescue events are being captured.
|
150
76
|
def capture_rescue(value = nil)
|
151
77
|
if value.nil?
|
152
78
|
@capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
|
@@ -156,44 +82,52 @@ class EnhancedErrors
|
|
156
82
|
@capture_rescue
|
157
83
|
end
|
158
84
|
|
159
|
-
# Retrieves the current skip list, initializing it with default values if not already set.
|
160
|
-
#
|
161
|
-
# @return [Set<Symbol>] The current skip list.
|
162
85
|
def skip_list
|
163
86
|
@skip_list ||= default_skip_list
|
164
87
|
end
|
165
88
|
|
166
|
-
# Initializes the default skip list by merging Rails and RSpec specific variables.
|
167
|
-
#
|
168
|
-
# @return [Set<Symbol>] The default skip list.
|
169
89
|
def default_skip_list
|
170
90
|
Set.new(RAILS_SKIP_LIST).merge(RSPEC_SKIP_LIST)
|
171
91
|
end
|
172
92
|
|
173
|
-
#
|
174
|
-
#
|
175
|
-
|
176
|
-
|
93
|
+
# takes an exception and bindings, calculates the variables message
|
94
|
+
# and modifies the exceptions .message to display the variables
|
95
|
+
def override_exception_message(exception, binding_or_bindings)
|
96
|
+
# Ensure binding_or_bindings is always an array for compatibility
|
97
|
+
return exception if binding_or_bindings.nil? || binding_or_bindings.empty? || exception.respond_to?(:unaltered_message)
|
98
|
+
variable_str = EnhancedErrors.format(binding_or_bindings)
|
99
|
+
message_str = exception.message
|
100
|
+
exception.define_singleton_method(:unaltered_message) { message_str }
|
101
|
+
exception.define_singleton_method(:message) do
|
102
|
+
"#{message_str}#{variable_str}"
|
103
|
+
end
|
104
|
+
exception
|
105
|
+
end
|
106
|
+
|
177
107
|
def add_to_skip_list(*vars)
|
178
108
|
skip_list.merge(vars)
|
179
109
|
end
|
180
110
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
# @param debug [Boolean] Whether to enable debug mode.
|
185
|
-
# @param options [Hash] Additional configuration options.
|
186
|
-
# @yield [void] A block for additional configuration.
|
187
|
-
# @return [void]
|
188
|
-
def enhance!(enabled: true, debug: false, capture_events: nil, **options, &block)
|
111
|
+
def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
|
112
|
+
require_relative 'enhanced/exception'
|
113
|
+
|
189
114
|
@output_format = nil
|
190
115
|
@eligible_for_capture = nil
|
191
116
|
@original_global_variables = nil
|
117
|
+
@override_messages = override_messages
|
118
|
+
|
192
119
|
if enabled == false
|
193
120
|
@original_global_variables = nil
|
194
121
|
@enabled = false
|
195
|
-
@trace
|
122
|
+
@trace&.disable
|
123
|
+
@trace = nil
|
196
124
|
else
|
125
|
+
# if there's an old one, disable it before replacing it
|
126
|
+
# this seems to actually matter, although it seems like it
|
127
|
+
# shouldn't
|
128
|
+
@trace&.disable
|
129
|
+
@trace = nil
|
130
|
+
|
197
131
|
@enabled = true
|
198
132
|
@debug = debug
|
199
133
|
@original_global_variables = global_variables
|
@@ -205,7 +139,7 @@ class EnhancedErrors
|
|
205
139
|
elsif respond_to?(key)
|
206
140
|
send(key, value)
|
207
141
|
else
|
208
|
-
# Ignore unknown options
|
142
|
+
# Ignore unknown options
|
209
143
|
end
|
210
144
|
end
|
211
145
|
|
@@ -213,14 +147,106 @@ class EnhancedErrors
|
|
213
147
|
instance_eval(&@config_block) if @config_block
|
214
148
|
|
215
149
|
validate_and_set_capture_events(capture_events)
|
216
|
-
|
150
|
+
|
151
|
+
events = @capture_events ? @capture_events.to_a : default_capture_events
|
152
|
+
@trace = TracePoint.new(*events) do |tp|
|
153
|
+
handle_tracepoint_event(tp)
|
154
|
+
end
|
155
|
+
|
156
|
+
@trace.enable
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def safe_prepend_module(target_class, mod)
|
161
|
+
if defined?(target_class) && target_class.is_a?(Module)
|
162
|
+
target_class.prepend(mod)
|
163
|
+
true
|
164
|
+
else
|
165
|
+
false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def safely_prepend_rspec_custom_failure_message
|
170
|
+
return if @rspec_failure_message_loaded
|
171
|
+
if defined?(RSpec::Core::Example)
|
172
|
+
RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
|
173
|
+
@rspec_failure_message_loaded = true
|
174
|
+
else
|
175
|
+
|
176
|
+
end
|
177
|
+
rescue => e
|
178
|
+
puts "Failed "
|
179
|
+
end
|
180
|
+
|
181
|
+
def start_rspec_binding_capture
|
182
|
+
@rspec_example_binding = nil
|
183
|
+
|
184
|
+
# In the Exception binding infos, I observed that re-setting
|
185
|
+
# the tracepoint without disabling it seemed to accumulate traces
|
186
|
+
# in the test suite where things are disabled and re-enabled often.
|
187
|
+
@rspec_tracepoint&.disable
|
188
|
+
@rspec_tracepoint = nil
|
189
|
+
|
190
|
+
@rspec_tracepoint = TracePoint.new(:b_return) do |tp|
|
191
|
+
if tp.self.class.name =~ /RSpec::ExampleGroups::[a-zA-Z0-9]+$/ && tp.method_id.nil? && !(tp.path =~ /rspec/)
|
192
|
+
@rspec_example_binding = tp.binding
|
193
|
+
end
|
217
194
|
end
|
195
|
+
@rspec_tracepoint.enable
|
196
|
+
end
|
197
|
+
|
198
|
+
def stop_rspec_binding_capture
|
199
|
+
@rspec_tracepoint.disable if @rspec_tracepoint
|
200
|
+
binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
|
201
|
+
@rspec_example_binding = nil
|
202
|
+
binding_info
|
203
|
+
end
|
204
|
+
|
205
|
+
def convert_binding_to_binding_info(b, capture_let_variables: true)
|
206
|
+
file = b.eval("__FILE__") rescue nil
|
207
|
+
line = b.eval("__LINE__") rescue nil
|
208
|
+
location = [file, line].compact.join(":")
|
209
|
+
|
210
|
+
locals = b.local_variables.map { |var| [var, safe_local_variable_get(b, var)] }.to_h
|
211
|
+
receiver = b.receiver
|
212
|
+
instance_vars = receiver.instance_variables
|
213
|
+
instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
|
214
|
+
|
215
|
+
# Capture let variables only for RSpec captures
|
216
|
+
lets = {}
|
217
|
+
if capture_let_variables && instance_vars.include?(:@__memoized)
|
218
|
+
outer_memoized = receiver.instance_variable_get(:@__memoized)
|
219
|
+
memoized = outer_memoized.instance_variable_get(:@memoized) if outer_memoized.respond_to?(:instance_variable_get)
|
220
|
+
if memoized.is_a?(Hash)
|
221
|
+
lets = memoized.transform_keys(&:to_sym)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
binding_info = {
|
226
|
+
source: location,
|
227
|
+
object: receiver,
|
228
|
+
library: !!GEMS_REGEX.match?(location.to_s),
|
229
|
+
method_and_args: {
|
230
|
+
object_name: '',
|
231
|
+
args: ''
|
232
|
+
},
|
233
|
+
test_name: test_name,
|
234
|
+
variables: {
|
235
|
+
locals: locals,
|
236
|
+
instances: instances,
|
237
|
+
lets: lets,
|
238
|
+
globals: {}
|
239
|
+
},
|
240
|
+
exception: 'NoException',
|
241
|
+
capture_event: 'RSpecContext'
|
242
|
+
}
|
243
|
+
|
244
|
+
# Apply skip list to remove @__memoized and @__inspect_output from output
|
245
|
+
# but only after extracting let variables.
|
246
|
+
binding_info = default_on_capture(binding_info)
|
247
|
+
binding_info
|
218
248
|
end
|
219
249
|
|
220
|
-
# Sets or retrieves the eligibility criteria for capturing exceptions.
|
221
|
-
#
|
222
|
-
# @yieldparam exception [Exception] The exception to evaluate.
|
223
|
-
# @return [Proc] The current eligibility proc.
|
224
250
|
def eligible_for_capture(&block)
|
225
251
|
if block_given?
|
226
252
|
@eligible_for_capture = block
|
@@ -229,10 +255,6 @@ class EnhancedErrors
|
|
229
255
|
end
|
230
256
|
end
|
231
257
|
|
232
|
-
# Sets or retrieves the hook to modify binding information upon capture.
|
233
|
-
#
|
234
|
-
# @yieldparam binding_info [Hash] The binding information captured.
|
235
|
-
# @return [Proc] The current on_capture hook.
|
236
258
|
def on_capture(&block)
|
237
259
|
if block_given?
|
238
260
|
@on_capture_hook = block
|
@@ -241,18 +263,10 @@ class EnhancedErrors
|
|
241
263
|
end
|
242
264
|
end
|
243
265
|
|
244
|
-
# Sets the on_capture hook.
|
245
|
-
#
|
246
|
-
# @param value [Proc] The proc to set as the on_capture hook.
|
247
|
-
# @return [Proc] The newly set on_capture hook.
|
248
266
|
def on_capture=(value)
|
249
267
|
self.on_capture_hook = value
|
250
268
|
end
|
251
269
|
|
252
|
-
# Sets or retrieves the hook to modify formatted exception messages.
|
253
|
-
#
|
254
|
-
# @yieldparam formatted_string [String] The formatted exception message.
|
255
|
-
# @return [Proc] The current on_format hook.
|
256
270
|
def on_format(&block)
|
257
271
|
if block_given?
|
258
272
|
@on_format_hook = block
|
@@ -261,60 +275,39 @@ class EnhancedErrors
|
|
261
275
|
end
|
262
276
|
end
|
263
277
|
|
264
|
-
# Sets the on_format hook.
|
265
|
-
#
|
266
|
-
# @param value [Proc] The proc to set as the on_format hook.
|
267
|
-
# @return [Proc] The newly set on_format hook.
|
268
278
|
def on_format=(value)
|
269
279
|
@on_format_hook = value
|
270
280
|
end
|
271
281
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
result = binding_infos_array_to_string(
|
282
|
+
def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
|
283
|
+
return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
|
284
|
+
|
285
|
+
# If captured_binding_infos is already an array, use it directly; otherwise, wrap it in an array.
|
286
|
+
binding_infos = captured_binding_infos.is_a?(Array) ? captured_binding_infos : [captured_binding_infos]
|
287
|
+
|
288
|
+
result = binding_infos_array_to_string(binding_infos, output_format)
|
289
|
+
|
279
290
|
if @on_format_hook
|
280
291
|
begin
|
281
292
|
result = @on_format_hook.call(result)
|
282
|
-
rescue
|
283
|
-
# Since the on_format_hook failed, do not display the data
|
293
|
+
rescue
|
284
294
|
result = ''
|
285
|
-
# Optionally, log the error safely if logging is guaranteed not to raise exceptions
|
286
295
|
end
|
287
296
|
else
|
288
297
|
result = default_on_format(result)
|
289
298
|
end
|
299
|
+
|
290
300
|
result
|
291
301
|
end
|
292
302
|
|
293
|
-
# Converts an array of binding information hashes into a formatted string.
|
294
|
-
#
|
295
|
-
# @param captured_bindings [Array<Hash>] The array of binding information.
|
296
|
-
# @param format [Symbol] The format to use (:json, :plaintext, :terminal).
|
297
|
-
# @return [String] The formatted string representation of the binding information.
|
298
303
|
def binding_infos_array_to_string(captured_bindings, format = :terminal)
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
Colors.enabled = false
|
305
|
-
captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
|
306
|
-
when :terminal
|
307
|
-
Colors.enabled = true
|
308
|
-
captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
|
309
|
-
else
|
310
|
-
Colors.enabled = false
|
311
|
-
captured_bindings.map { |binding_info| binding_info_string(binding_info) }.join("\n")
|
312
|
-
end
|
304
|
+
return '' if captured_bindings.nil? || captured_bindings.empty?
|
305
|
+
captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
|
306
|
+
Colors.enabled = format == :terminal
|
307
|
+
formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
|
308
|
+
format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
|
313
309
|
end
|
314
310
|
|
315
|
-
# Determines the default output format based on the current environment.
|
316
|
-
#
|
317
|
-
# @return [Symbol] The default format (:json, :plaintext, :terminal).
|
318
311
|
def get_default_format_for_environment
|
319
312
|
return @output_format unless @output_format.nil?
|
320
313
|
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
@@ -332,9 +325,6 @@ class EnhancedErrors
|
|
332
325
|
end
|
333
326
|
end
|
334
327
|
|
335
|
-
# Checks if the code is running in a Continuous Integration (CI) environment.
|
336
|
-
#
|
337
|
-
# @return [Boolean] `true` if running in CI, otherwise `false`.
|
338
328
|
def running_in_ci?
|
339
329
|
return @running_in_ci if defined?(@running_in_ci)
|
340
330
|
ci_env_vars = {
|
@@ -349,10 +339,6 @@ class EnhancedErrors
|
|
349
339
|
@running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
|
350
340
|
end
|
351
341
|
|
352
|
-
# Applies the skip list to the captured binding information, excluding specified variables.
|
353
|
-
#
|
354
|
-
# @param binding_info [Hash] The binding information to filter.
|
355
|
-
# @return [Hash] The filtered binding information.
|
356
342
|
def apply_skip_list(binding_info)
|
357
343
|
unless @debug
|
358
344
|
variables = binding_info[:variables]
|
@@ -363,27 +349,22 @@ class EnhancedErrors
|
|
363
349
|
binding_info
|
364
350
|
end
|
365
351
|
|
366
|
-
# Validates the format of the captured binding information.
|
367
|
-
#
|
368
|
-
# @param binding_info [Hash] The binding information to validate.
|
369
|
-
# @return [Hash, nil] The validated binding information or `nil` if invalid.
|
370
352
|
def validate_binding_format(binding_info)
|
371
353
|
unless binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
|
372
|
-
# Log or handle the invalid format as needed
|
373
354
|
return nil
|
374
355
|
end
|
375
356
|
binding_info
|
376
357
|
end
|
377
358
|
|
378
|
-
# Formats a single binding information hash into a string with colorization.
|
379
|
-
#
|
380
|
-
# @param binding_info [Hash] The binding information to format.
|
381
|
-
# @return [String] The formatted string.
|
382
359
|
def binding_info_string(binding_info)
|
360
|
+
exception = safe_to_s(binding_info[:exception])
|
383
361
|
capture_event = safe_to_s(binding_info[:capture_event]).capitalize
|
384
362
|
source = safe_to_s(binding_info[:source])
|
385
|
-
result =
|
386
|
-
|
363
|
+
result = ''
|
364
|
+
unless exception.to_s == 'NoException'
|
365
|
+
origination = "#{capture_event.capitalize}d"
|
366
|
+
result += "#{Colors.green(origination)}: #{Colors.blue(source)}"
|
367
|
+
end
|
387
368
|
method_desc = method_and_args_desc(binding_info[:method_and_args])
|
388
369
|
result += method_desc
|
389
370
|
|
@@ -399,6 +380,7 @@ class EnhancedErrors
|
|
399
380
|
result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
|
400
381
|
end
|
401
382
|
|
383
|
+
# Display let variables for RSpec captures
|
402
384
|
if variables[:lets] && !variables[:lets].empty?
|
403
385
|
result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
|
404
386
|
end
|
@@ -412,128 +394,105 @@ class EnhancedErrors
|
|
412
394
|
end
|
413
395
|
result + "\n"
|
414
396
|
rescue => e
|
415
|
-
#
|
416
|
-
|
397
|
+
puts "#{e.message}"
|
398
|
+
''
|
417
399
|
end
|
418
400
|
|
419
401
|
private
|
420
402
|
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
@trace = TracePoint.new(*events) do |tp|
|
428
|
-
next if Thread.current[:enhanced_errors_processing] || ignored_exception?(tp.raised_exception)
|
429
|
-
Thread.current[:enhanced_errors_processing] = true
|
430
|
-
exception = tp.raised_exception
|
431
|
-
capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
|
432
|
-
|
433
|
-
unless capture_me
|
434
|
-
Thread.current[:enhanced_errors_processing] = false
|
435
|
-
next
|
436
|
-
end
|
403
|
+
def handle_tracepoint_event(tp)
|
404
|
+
return unless @enabled
|
405
|
+
return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
|
406
|
+
Thread.current[:enhanced_errors_processing] = true
|
407
|
+
exception = tp.raised_exception
|
408
|
+
capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
|
437
409
|
|
438
|
-
|
410
|
+
unless capture_me
|
411
|
+
Thread.current[:enhanced_errors_processing] = false
|
412
|
+
return
|
413
|
+
end
|
439
414
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
415
|
+
binding_context = tp.binding
|
416
|
+
method_name = tp.method_id
|
417
|
+
method_and_args = {
|
418
|
+
object_name: determine_object_name(tp, method_name),
|
419
|
+
args: extract_arguments(tp, method_name)
|
420
|
+
}
|
444
421
|
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
args: extract_arguments(tp, method_name)
|
449
|
-
}
|
422
|
+
locals = binding_context.local_variables.map { |var|
|
423
|
+
[var, safe_local_variable_get(binding_context, var)]
|
424
|
+
}.to_h
|
450
425
|
|
451
|
-
|
452
|
-
|
453
|
-
|
426
|
+
instance_vars = binding_context.receiver.instance_variables
|
427
|
+
instances = instance_vars.map { |var|
|
428
|
+
[var, safe_instance_variable_get(binding_context.receiver, var)]
|
429
|
+
}.to_h
|
454
430
|
|
455
|
-
|
431
|
+
# No let variables for exceptions
|
432
|
+
lets = {}
|
456
433
|
|
457
|
-
|
458
|
-
|
434
|
+
globals = {}
|
435
|
+
if @debug
|
436
|
+
globals = (global_variables - @original_global_variables).map { |var|
|
437
|
+
[var, get_global_variable_value(var)]
|
459
438
|
}.to_h
|
439
|
+
end
|
460
440
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
441
|
+
capture_event = safe_to_s(tp.event)
|
442
|
+
location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
|
443
|
+
|
444
|
+
binding_info = {
|
445
|
+
source: location,
|
446
|
+
object: tp.self,
|
447
|
+
library: !!GEMS_REGEX.match?(location),
|
448
|
+
method_and_args: method_and_args,
|
449
|
+
test_name: test_name,
|
450
|
+
variables: {
|
451
|
+
locals: locals,
|
452
|
+
instances: instances,
|
453
|
+
lets: lets,
|
454
|
+
globals: globals
|
455
|
+
},
|
456
|
+
exception: safe_to_s(exception.class.name),
|
457
|
+
capture_event: capture_event
|
458
|
+
}
|
459
|
+
|
460
|
+
binding_info = default_on_capture(binding_info)
|
470
461
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
462
|
+
if on_capture_hook
|
463
|
+
begin
|
464
|
+
Thread.current[:on_capture] = true
|
465
|
+
binding_info = on_capture_hook.call(binding_info)
|
466
|
+
rescue
|
467
|
+
binding_info = nil
|
468
|
+
ensure
|
469
|
+
Thread.current[:on_capture] = false
|
477
470
|
end
|
471
|
+
end
|
478
472
|
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
binding_info
|
483
|
-
|
484
|
-
|
485
|
-
library: !!GEMS_REGEX.match?(location),
|
486
|
-
method_and_args: method_and_args,
|
487
|
-
test_name: test_name,
|
488
|
-
variables: {
|
489
|
-
locals: locals,
|
490
|
-
instances: instances,
|
491
|
-
lets: lets,
|
492
|
-
globals: globals
|
493
|
-
},
|
494
|
-
exception: safe_to_s(exception.class.name),
|
495
|
-
capture_event: capture_event
|
496
|
-
}
|
497
|
-
|
498
|
-
binding_info = default_on_capture(binding_info) # Apply default processing
|
499
|
-
|
500
|
-
if on_capture_hook
|
501
|
-
begin
|
502
|
-
binding_info = on_capture_hook.call(binding_info)
|
503
|
-
rescue => e
|
504
|
-
# Since the on_capture_hook failed, do not capture this binding_info
|
505
|
-
binding_info = nil
|
506
|
-
# Optionally, log the error safely if logging is guaranteed not to raise exceptions
|
507
|
-
end
|
473
|
+
if binding_info
|
474
|
+
binding_info = validate_binding_format(binding_info)
|
475
|
+
|
476
|
+
if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
|
477
|
+
# delete from the middle of the array as the ends are most interesting
|
478
|
+
exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
|
508
479
|
end
|
509
480
|
|
510
|
-
# Proceed only if binding_info is valid
|
511
481
|
if binding_info
|
512
|
-
|
513
|
-
if
|
514
|
-
exception.instance_variable_get(:@binding_infos) << binding_info
|
515
|
-
end
|
482
|
+
exception.binding_infos << binding_info
|
483
|
+
override_exception_message(exception, exception.binding_infos) if @override_messages
|
516
484
|
end
|
517
|
-
rescue => e
|
518
|
-
# Avoid any code here that could raise exceptions
|
519
|
-
ensure
|
520
|
-
Thread.current[:enhanced_errors_processing] = false
|
521
485
|
end
|
522
|
-
|
523
|
-
|
486
|
+
rescue
|
487
|
+
# Avoid raising exceptions here
|
488
|
+
ensure
|
489
|
+
Thread.current[:enhanced_errors_processing] = false
|
524
490
|
end
|
525
491
|
|
526
|
-
# Checks if the exception is in the ignored exceptions list.
|
527
|
-
#
|
528
|
-
# @param exception [Exception] The exception to check.
|
529
|
-
# @return [Boolean] `true` if the exception should be ignored, otherwise `false`.
|
530
492
|
def ignored_exception?(exception)
|
531
|
-
IGNORED_EXCEPTIONS.
|
493
|
+
IGNORED_EXCEPTIONS.include?(exception.class.name)
|
532
494
|
end
|
533
495
|
|
534
|
-
# Retrieves the current test name from RSpec, if available.
|
535
|
-
#
|
536
|
-
# @return [String, nil] The current test name or `nil` if not in a test context.
|
537
496
|
def test_name
|
538
497
|
if defined?(RSpec)
|
539
498
|
RSpec&.current_example&.full_description
|
@@ -542,9 +501,6 @@ class EnhancedErrors
|
|
542
501
|
nil
|
543
502
|
end
|
544
503
|
|
545
|
-
# Helper method to determine the default capture types based on Ruby version
|
546
|
-
#
|
547
|
-
# @return [Set<Symbol>] The default set of capture types
|
548
504
|
def default_capture_events
|
549
505
|
events = [:raise]
|
550
506
|
if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
|
@@ -553,10 +509,6 @@ class EnhancedErrors
|
|
553
509
|
Set.new(events)
|
554
510
|
end
|
555
511
|
|
556
|
-
# Validates and sets the capture events for TracePoint.
|
557
|
-
#
|
558
|
-
# @param capture_events [Array<Symbol>, nil] The events to capture.
|
559
|
-
# @return [void]
|
560
512
|
def validate_and_set_capture_events(capture_events)
|
561
513
|
if capture_events.nil?
|
562
514
|
@capture_events = default_capture_events
|
@@ -570,12 +522,12 @@ class EnhancedErrors
|
|
570
522
|
end
|
571
523
|
|
572
524
|
if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
|
573
|
-
puts "EnhancedErrors: Warning: :rescue capture_event
|
525
|
+
puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
|
574
526
|
capture_events = capture_events - [:rescue]
|
575
527
|
end
|
576
528
|
|
577
529
|
if capture_events.empty?
|
578
|
-
puts "No valid capture_events provided to EnhancedErrors.
|
530
|
+
puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
|
579
531
|
@capture_events = default_capture_events
|
580
532
|
return
|
581
533
|
end
|
@@ -583,21 +535,12 @@ class EnhancedErrors
|
|
583
535
|
@capture_events = capture_events.to_set
|
584
536
|
end
|
585
537
|
|
586
|
-
# Validates the capture events.
|
587
|
-
#
|
588
|
-
# @param capture_events [Array<Symbol>] The events to validate.
|
589
|
-
# @return [Boolean] `true` if valid, otherwise `false`.
|
590
538
|
def valid_capture_events?(capture_events)
|
591
539
|
return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
|
592
540
|
valid_types = [:raise, :rescue].to_set
|
593
541
|
capture_events.to_set.subset?(valid_types)
|
594
542
|
end
|
595
543
|
|
596
|
-
# Extracts method arguments from the TracePoint binding.
|
597
|
-
#
|
598
|
-
# @param tp [TracePoint] The current TracePoint.
|
599
|
-
# @param method_name [Symbol] The name of the method.
|
600
|
-
# @return [String] A string representation of the method arguments.
|
601
544
|
def extract_arguments(tp, method_name)
|
602
545
|
return '' unless method_name
|
603
546
|
begin
|
@@ -607,7 +550,7 @@ class EnhancedErrors
|
|
607
550
|
parameters = method_obj.parameters
|
608
551
|
locals = bind.local_variables
|
609
552
|
|
610
|
-
parameters.map do |(
|
553
|
+
parameters.map do |(_, name)|
|
611
554
|
value = locals.include?(name) ? safe_local_variable_get(bind, name) : nil
|
612
555
|
"#{name}=#{safe_inspect(value)}"
|
613
556
|
rescue => e
|
@@ -618,11 +561,6 @@ class EnhancedErrors
|
|
618
561
|
end
|
619
562
|
end
|
620
563
|
|
621
|
-
# Determines the object name based on the TracePoint and method name.
|
622
|
-
#
|
623
|
-
# @param tp [TracePoint] The current TracePoint.
|
624
|
-
# @param method_name [Symbol] The name of the method.
|
625
|
-
# @return [String] The formatted object name.
|
626
564
|
def determine_object_name(tp, method_name)
|
627
565
|
if tp.self.is_a?(Class) && tp.self.singleton_class == tp.defined_class
|
628
566
|
"#{safe_to_s(tp.self)}.#{method_name}"
|
@@ -633,22 +571,12 @@ class EnhancedErrors
|
|
633
571
|
"#<Error inspecting value: #{e.message}>"
|
634
572
|
end
|
635
573
|
|
636
|
-
# Retrieves the value of a global variable by its name.
|
637
|
-
#
|
638
|
-
# @param var [Symbol] The name of the global variable.
|
639
|
-
# @return [Object, String] The value of the global variable or an error message.
|
640
574
|
def get_global_variable_value(var)
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
"#<Error getting value for #{var}>"
|
645
|
-
end
|
575
|
+
var.is_a?(Symbol) ? eval("#{var}") : nil
|
576
|
+
rescue => e
|
577
|
+
"#<Error getting value for #{var}>"
|
646
578
|
end
|
647
579
|
|
648
|
-
# Generates a description for method and arguments.
|
649
|
-
#
|
650
|
-
# @param method_info [Hash] Information about the method and its arguments.
|
651
|
-
# @return [String] The formatted description.
|
652
580
|
def method_and_args_desc(method_info)
|
653
581
|
object_name = safe_to_s(method_info[:object_name])
|
654
582
|
args = safe_to_s(method_info[:args])
|
@@ -656,26 +584,18 @@ class EnhancedErrors
|
|
656
584
|
arg_str = args.empty? ? '' : "(#{args})"
|
657
585
|
str = object_name + arg_str
|
658
586
|
"\n#{Colors.green('Method: ')}#{Colors.blue(str)}\n"
|
659
|
-
rescue
|
587
|
+
rescue
|
660
588
|
''
|
661
589
|
end
|
662
590
|
|
663
|
-
# Generates a formatted description for a set of variables.
|
664
|
-
#
|
665
|
-
# @param vars_hash [Hash] A hash of variable names and their values.
|
666
|
-
# @return [String] The formatted variables description.
|
667
591
|
def variable_description(vars_hash)
|
668
592
|
vars_hash.map do |name, value|
|
669
593
|
" #{Colors.purple(name)}: #{format_variable(value)}\n"
|
670
594
|
end.join
|
671
|
-
rescue
|
595
|
+
rescue
|
672
596
|
''
|
673
597
|
end
|
674
598
|
|
675
|
-
# Formats a variable for display, using `awesome_print` if available and enabled.
|
676
|
-
#
|
677
|
-
# @param variable [Object] The variable to format.
|
678
|
-
# @return [String] The formatted variable.
|
679
599
|
def format_variable(variable)
|
680
600
|
if awesome_print_available? && Colors.enabled?
|
681
601
|
variable.ai
|
@@ -684,35 +604,24 @@ class EnhancedErrors
|
|
684
604
|
end
|
685
605
|
rescue => e
|
686
606
|
var_str = safe_to_s(variable)
|
687
|
-
"#{var_str}: [Inspection Error]"
|
607
|
+
"#{var_str}: [Inspection Error #{e.message}]"
|
688
608
|
end
|
689
609
|
|
690
|
-
# Checks if the `AwesomePrint` gem is available.
|
691
|
-
#
|
692
|
-
# @return [Boolean] `true` if `AwesomePrint` is available, otherwise `false`.
|
693
610
|
def awesome_print_available?
|
694
611
|
return @awesome_print_available unless @awesome_print_available.nil?
|
695
612
|
@awesome_print_available = defined?(AwesomePrint)
|
696
613
|
end
|
697
614
|
|
698
|
-
# Safely calls `inspect` on a variable.
|
699
|
-
#
|
700
|
-
# @param variable [Object] The variable to inspect.
|
701
|
-
# @return [String] The inspected variable or a safe fallback.
|
702
615
|
def safe_inspect(variable)
|
703
616
|
variable.inspect
|
704
|
-
rescue
|
617
|
+
rescue
|
705
618
|
safe_to_s(variable)
|
706
619
|
end
|
707
620
|
|
708
|
-
# Safely converts a variable to a string, handling exceptions.
|
709
|
-
#
|
710
|
-
# @param variable [Object] The variable to convert.
|
711
|
-
# @return [String] The string representation or a safe fallback.
|
712
621
|
def safe_to_s(variable)
|
713
622
|
str = variable.to_s
|
714
|
-
if str.length >
|
715
|
-
str[0...
|
623
|
+
if str.length > 140
|
624
|
+
str[0...140] + '...'
|
716
625
|
else
|
717
626
|
str
|
718
627
|
end
|
@@ -720,53 +629,30 @@ class EnhancedErrors
|
|
720
629
|
"[Unprintable variable]"
|
721
630
|
end
|
722
631
|
|
723
|
-
# Safely retrieves a local variable from a binding.
|
724
|
-
#
|
725
|
-
# @param binding_context [Binding] The binding context.
|
726
|
-
# @param var_name [Symbol] The name of the local variable.
|
727
|
-
# @return [Object] The value of the local variable or a safe fallback.
|
728
632
|
def safe_local_variable_get(binding_context, var_name)
|
729
633
|
binding_context.local_variable_get(var_name)
|
730
634
|
rescue
|
731
635
|
"[Error accessing local variable #{var_name}]"
|
732
636
|
end
|
733
637
|
|
734
|
-
# Safely retrieves an instance variable from an object.
|
735
|
-
#
|
736
|
-
# @param obj [Object] The object.
|
737
|
-
# @param var_name [Symbol] The name of the instance variable.
|
738
|
-
# @return [Object] The value of the instance variable or a safe fallback.
|
739
638
|
def safe_instance_variable_get(obj, var_name)
|
740
639
|
obj.instance_variable_get(var_name)
|
741
640
|
rescue
|
742
641
|
"[Error accessing instance variable #{var_name}]"
|
743
642
|
end
|
744
643
|
|
745
|
-
# Default implementation for the on_format hook.
|
746
|
-
#
|
747
|
-
# @param string [String] The formatted exception message.
|
748
|
-
# @return [String] The unmodified exception message.
|
749
644
|
def default_on_format(string)
|
750
645
|
string
|
751
646
|
end
|
752
647
|
|
753
|
-
# Default implementation for the on_capture hook, applying the skip list.
|
754
|
-
#
|
755
|
-
# @param binding_info [Hash] The captured binding information.
|
756
|
-
# @return [Hash] The filtered binding information.
|
757
648
|
def default_on_capture(binding_info)
|
758
|
-
# Use this to clean up the captured bindings
|
759
649
|
EnhancedErrors.apply_skip_list(binding_info)
|
760
650
|
end
|
761
651
|
|
762
|
-
# Default eligibility check for capturing exceptions.
|
763
|
-
#
|
764
|
-
# @param exception [Exception] The exception to evaluate.
|
765
|
-
# @return [Boolean] `true` if the exception should be captured, otherwise `false`.
|
766
652
|
def default_eligible_for_capture(exception)
|
767
|
-
|
653
|
+
ignored = ignored_exception?(exception)
|
654
|
+
rspec = exception.class.name.start_with?('RSpec::Matchers')
|
655
|
+
!ignored && !rspec
|
768
656
|
end
|
769
|
-
|
770
|
-
@enabled = false
|
771
657
|
end
|
772
658
|
end
|