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