enhanced_errors 1.0.0 → 2.0.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/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 +262 -414
- 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,108 +3,47 @@
|
|
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([
|
33
|
+
:@example,
|
96
34
|
:@fixture_cache,
|
97
35
|
:@fixture_cache_key,
|
36
|
+
:@fixture_connections,
|
98
37
|
:@fixture_connection_pools,
|
38
|
+
:@loaded_fixtures,
|
99
39
|
:@connection_subscriber,
|
100
40
|
:@saved_pool_configs,
|
101
|
-
:@
|
102
|
-
:@matcher_definitions
|
41
|
+
:@legacy_saved_pool_configs,
|
42
|
+
:@matcher_definitions,
|
43
|
+
:@__memoized,
|
44
|
+
:@__inspect_output
|
103
45
|
])
|
104
46
|
|
105
|
-
# A set of Rails-specific instance variables to skip.
|
106
|
-
#
|
107
|
-
# @return [Set<Symbol>]
|
108
47
|
RAILS_SKIP_LIST = Set.new([
|
109
48
|
:@new_record,
|
110
49
|
:@attributes,
|
@@ -126,10 +65,8 @@ class EnhancedErrors
|
|
126
65
|
:@arel_table
|
127
66
|
])
|
128
67
|
|
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.
|
68
|
+
@enabled = false
|
69
|
+
|
133
70
|
def max_length(value = nil)
|
134
71
|
if value.nil?
|
135
72
|
@max_length ||= DEFAULT_MAX_LENGTH
|
@@ -139,23 +76,6 @@ class EnhancedErrors
|
|
139
76
|
@max_length
|
140
77
|
end
|
141
78
|
|
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
79
|
def capture_rescue(value = nil)
|
160
80
|
if value.nil?
|
161
81
|
@capture_rescue = @capture_rescue.nil? ? false : @capture_rescue
|
@@ -165,45 +85,52 @@ class EnhancedErrors
|
|
165
85
|
@capture_rescue
|
166
86
|
end
|
167
87
|
|
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
88
|
def skip_list
|
172
89
|
@skip_list ||= default_skip_list
|
173
90
|
end
|
174
91
|
|
175
|
-
# Initializes the default skip list by merging Rails and RSpec specific variables.
|
176
|
-
#
|
177
|
-
# @return [Set<Symbol>] The default skip list.
|
178
92
|
def default_skip_list
|
179
93
|
Set.new(RAILS_SKIP_LIST).merge(RSPEC_SKIP_LIST)
|
180
94
|
end
|
181
95
|
|
182
|
-
#
|
183
|
-
#
|
184
|
-
|
185
|
-
|
96
|
+
# takes an exception and bindings, calculates the variables message
|
97
|
+
# and modifies the exceptions .message to display the variables
|
98
|
+
def override_exception_message(exception, binding_or_bindings)
|
99
|
+
# Ensure binding_or_bindings is always an array for compatibility
|
100
|
+
return exception if binding_or_bindings.nil? || binding_or_bindings.empty? || exception.respond_to?(:unaltered_message)
|
101
|
+
variable_str = EnhancedErrors.format(binding_or_bindings)
|
102
|
+
message_str = exception.message
|
103
|
+
exception.define_singleton_method(:unaltered_message) { message_str }
|
104
|
+
exception.define_singleton_method(:message) do
|
105
|
+
"#{message_str}#{variable_str}"
|
106
|
+
end
|
107
|
+
exception
|
108
|
+
end
|
109
|
+
|
186
110
|
def add_to_skip_list(*vars)
|
187
111
|
skip_list.merge(vars)
|
188
112
|
end
|
189
113
|
|
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)
|
114
|
+
def enhance_exceptions!(enabled: true, debug: false, capture_events: nil, override_messages: false, **options, &block)
|
115
|
+
require_relative 'enhanced/exception'
|
116
|
+
|
198
117
|
@output_format = nil
|
199
118
|
@eligible_for_capture = nil
|
200
119
|
@original_global_variables = nil
|
120
|
+
@override_messages = override_messages
|
121
|
+
|
201
122
|
if enabled == false
|
202
123
|
@original_global_variables = nil
|
203
124
|
@enabled = false
|
204
|
-
|
205
|
-
@
|
125
|
+
@trace&.disable
|
126
|
+
@trace = nil
|
206
127
|
else
|
128
|
+
# if there's an old one, disable it before replacing it
|
129
|
+
# this seems to actually matter, although it seems like it
|
130
|
+
# shouldn't
|
131
|
+
@trace&.disable
|
132
|
+
@trace = nil
|
133
|
+
|
207
134
|
@enabled = true
|
208
135
|
@debug = debug
|
209
136
|
@original_global_variables = global_variables
|
@@ -215,7 +142,7 @@ class EnhancedErrors
|
|
215
142
|
elsif respond_to?(key)
|
216
143
|
send(key, value)
|
217
144
|
else
|
218
|
-
# Ignore unknown options
|
145
|
+
# Ignore unknown options
|
219
146
|
end
|
220
147
|
end
|
221
148
|
|
@@ -224,26 +151,110 @@ class EnhancedErrors
|
|
224
151
|
|
225
152
|
validate_and_set_capture_events(capture_events)
|
226
153
|
|
227
|
-
|
228
|
-
@
|
229
|
-
|
230
|
-
|
154
|
+
events = @capture_events ? @capture_events.to_a : default_capture_events
|
155
|
+
@trace = TracePoint.new(*events) do |tp|
|
156
|
+
handle_tracepoint_event(tp)
|
157
|
+
end
|
158
|
+
|
159
|
+
@trace.enable
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def safe_prepend_module(target_class, mod)
|
164
|
+
if defined?(target_class) && target_class.is_a?(Module)
|
165
|
+
target_class.prepend(mod)
|
166
|
+
true
|
167
|
+
else
|
168
|
+
false
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def safely_prepend_rspec_custom_failure_message
|
173
|
+
return if @rspec_failure_message_loaded
|
174
|
+
if defined?(RSpec::Core::Example)
|
175
|
+
RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
|
176
|
+
@rspec_failure_message_loaded = true
|
177
|
+
else
|
178
|
+
|
179
|
+
end
|
180
|
+
rescue => e
|
181
|
+
puts "Failed "
|
182
|
+
end
|
183
|
+
|
184
|
+
def start_rspec_binding_capture
|
185
|
+
@rspec_example_binding = nil
|
231
186
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
187
|
+
# In the Exception binding infos, I observed that re-setting
|
188
|
+
# the tracepoint without disabling it seemed to accumulate traces
|
189
|
+
# in the test suite where things are disabled and re-enabled often.
|
190
|
+
@rspec_tracepoint&.disable
|
191
|
+
@rspec_tracepoint = nil
|
192
|
+
|
193
|
+
@rspec_tracepoint = TracePoint.new(:b_return) do |tp|
|
194
|
+
# This is super-kluge-y and should be replaced with... something TBD
|
195
|
+
if tp.self.class.name =~ /RSpec::ExampleGroups::[A-Z0-9]+.*/ &&
|
196
|
+
tp.method_id.nil? &&
|
197
|
+
!(tp.path =~ /rspec/) &&
|
198
|
+
tp.path =~ /_spec\.rb$/
|
199
|
+
@rspec_example_binding = tp.binding
|
236
200
|
end
|
201
|
+
end
|
202
|
+
@rspec_tracepoint.enable
|
203
|
+
end
|
204
|
+
|
205
|
+
def stop_rspec_binding_capture
|
206
|
+
@rspec_tracepoint&.disable
|
207
|
+
@rspec_tracepoint = nil
|
208
|
+
binding_info = convert_binding_to_binding_info(@rspec_example_binding) if @rspec_example_binding
|
209
|
+
@rspec_example_binding = nil
|
210
|
+
binding_info
|
211
|
+
end
|
237
212
|
|
238
|
-
|
239
|
-
|
213
|
+
def convert_binding_to_binding_info(b, capture_let_variables: true)
|
214
|
+
file = b.eval("__FILE__") rescue nil
|
215
|
+
line = b.eval("__LINE__") rescue nil
|
216
|
+
location = [file, line].compact.join(":")
|
217
|
+
|
218
|
+
locals = b.local_variables.map { |var| [var, safe_local_variable_get(b, var)] }.to_h
|
219
|
+
receiver = b.receiver
|
220
|
+
instance_vars = receiver.instance_variables
|
221
|
+
instances = instance_vars.map { |var| [var, safe_instance_variable_get(receiver, var)] }.to_h
|
222
|
+
|
223
|
+
# Capture let variables only for RSpec captures
|
224
|
+
lets = {}
|
225
|
+
if capture_let_variables && instance_vars.include?(:@__memoized)
|
226
|
+
outer_memoized = receiver.instance_variable_get(:@__memoized)
|
227
|
+
memoized = outer_memoized.instance_variable_get(:@memoized) if outer_memoized.respond_to?(:instance_variable_get)
|
228
|
+
if memoized.is_a?(Hash)
|
229
|
+
lets = memoized.transform_keys(&:to_sym)
|
230
|
+
end
|
240
231
|
end
|
232
|
+
|
233
|
+
binding_info = {
|
234
|
+
source: location,
|
235
|
+
object: receiver,
|
236
|
+
library: !!GEMS_REGEX.match?(location.to_s),
|
237
|
+
method_and_args: {
|
238
|
+
object_name: '',
|
239
|
+
args: ''
|
240
|
+
},
|
241
|
+
test_name: test_name,
|
242
|
+
variables: {
|
243
|
+
locals: locals,
|
244
|
+
instances: instances,
|
245
|
+
lets: lets,
|
246
|
+
globals: {}
|
247
|
+
},
|
248
|
+
exception: 'NoException',
|
249
|
+
capture_event: 'RSpecContext'
|
250
|
+
}
|
251
|
+
|
252
|
+
# Apply skip list to remove @__memoized and @__inspect_output from output
|
253
|
+
# but only after extracting let variables.
|
254
|
+
binding_info = default_on_capture(binding_info)
|
255
|
+
binding_info
|
241
256
|
end
|
242
257
|
|
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
258
|
def eligible_for_capture(&block)
|
248
259
|
if block_given?
|
249
260
|
@eligible_for_capture = block
|
@@ -252,10 +263,6 @@ class EnhancedErrors
|
|
252
263
|
end
|
253
264
|
end
|
254
265
|
|
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
266
|
def on_capture(&block)
|
260
267
|
if block_given?
|
261
268
|
@on_capture_hook = block
|
@@ -264,18 +271,10 @@ class EnhancedErrors
|
|
264
271
|
end
|
265
272
|
end
|
266
273
|
|
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
274
|
def on_capture=(value)
|
272
275
|
self.on_capture_hook = value
|
273
276
|
end
|
274
277
|
|
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
278
|
def on_format(&block)
|
280
279
|
if block_given?
|
281
280
|
@on_format_hook = block
|
@@ -284,50 +283,39 @@ class EnhancedErrors
|
|
284
283
|
end
|
285
284
|
end
|
286
285
|
|
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
286
|
def on_format=(value)
|
292
287
|
@on_format_hook = value
|
293
288
|
end
|
294
289
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
result = binding_infos_array_to_string(
|
290
|
+
def format(captured_binding_infos = [], output_format = get_default_format_for_environment)
|
291
|
+
return '' if captured_binding_infos.nil? || captured_binding_infos.empty?
|
292
|
+
|
293
|
+
# If captured_binding_infos is already an array, use it directly; otherwise, wrap it in an array.
|
294
|
+
binding_infos = captured_binding_infos.is_a?(Array) ? captured_binding_infos : [captured_binding_infos]
|
295
|
+
|
296
|
+
result = binding_infos_array_to_string(binding_infos, output_format)
|
297
|
+
|
302
298
|
if @on_format_hook
|
303
299
|
begin
|
304
300
|
result = @on_format_hook.call(result)
|
305
|
-
rescue
|
306
|
-
# Since the on_format_hook failed, do not display the data
|
301
|
+
rescue
|
307
302
|
result = ''
|
308
|
-
# Optionally, log the error safely if logging is guaranteed not to raise exceptions
|
309
303
|
end
|
310
304
|
else
|
311
305
|
result = default_on_format(result)
|
312
306
|
end
|
307
|
+
|
313
308
|
result
|
314
309
|
end
|
315
310
|
|
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
311
|
def binding_infos_array_to_string(captured_bindings, format = :terminal)
|
312
|
+
return '' if captured_bindings.nil? || captured_bindings.empty?
|
313
|
+
captured_bindings = [captured_bindings] unless captured_bindings.is_a?(Array)
|
322
314
|
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")
|
315
|
+
formatted_bindings = captured_bindings.to_a.map { |binding_info| binding_info_string(binding_info) }
|
316
|
+
format == :json ? JSON.pretty_generate(captured_bindings) : "\n#{formatted_bindings.join("\n")}"
|
326
317
|
end
|
327
318
|
|
328
|
-
# Determines the default output format based on the current environment.
|
329
|
-
#
|
330
|
-
# @return [Symbol] The default format (:json, :plaintext, :terminal).
|
331
319
|
def get_default_format_for_environment
|
332
320
|
return @output_format unless @output_format.nil?
|
333
321
|
env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
@@ -345,9 +333,6 @@ class EnhancedErrors
|
|
345
333
|
end
|
346
334
|
end
|
347
335
|
|
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
336
|
def running_in_ci?
|
352
337
|
return @running_in_ci if defined?(@running_in_ci)
|
353
338
|
ci_env_vars = {
|
@@ -362,10 +347,6 @@ class EnhancedErrors
|
|
362
347
|
@running_in_ci = ci_env_vars.any? { |_, value| value.to_s.downcase == 'true' }
|
363
348
|
end
|
364
349
|
|
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
350
|
def apply_skip_list(binding_info)
|
370
351
|
unless @debug
|
371
352
|
variables = binding_info[:variables]
|
@@ -376,27 +357,22 @@ class EnhancedErrors
|
|
376
357
|
binding_info
|
377
358
|
end
|
378
359
|
|
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
360
|
def validate_binding_format(binding_info)
|
384
361
|
unless binding_info.keys.include?(:capture_event) && binding_info[:variables].is_a?(Hash)
|
385
|
-
# Log or handle the invalid format as needed
|
386
362
|
return nil
|
387
363
|
end
|
388
364
|
binding_info
|
389
365
|
end
|
390
366
|
|
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
367
|
def binding_info_string(binding_info)
|
368
|
+
exception = safe_to_s(binding_info[:exception])
|
396
369
|
capture_event = safe_to_s(binding_info[:capture_event]).capitalize
|
397
370
|
source = safe_to_s(binding_info[:source])
|
398
|
-
result =
|
399
|
-
|
371
|
+
result = ''
|
372
|
+
unless exception.to_s == 'NoException'
|
373
|
+
origination = "#{capture_event.capitalize}d"
|
374
|
+
result += "#{Colors.green(origination)}: #{Colors.blue(source)}"
|
375
|
+
end
|
400
376
|
method_desc = method_and_args_desc(binding_info[:method_and_args])
|
401
377
|
result += method_desc
|
402
378
|
|
@@ -412,6 +388,7 @@ class EnhancedErrors
|
|
412
388
|
result += "\n#{Colors.green('Instances:')}\n#{variable_description(instance_vars_to_display)}"
|
413
389
|
end
|
414
390
|
|
391
|
+
# Display let variables for RSpec captures
|
415
392
|
if variables[:lets] && !variables[:lets].empty?
|
416
393
|
result += "\n#{Colors.green('Let Variables:')}\n#{variable_description(variables[:lets])}"
|
417
394
|
end
|
@@ -425,161 +402,105 @@ class EnhancedErrors
|
|
425
402
|
end
|
426
403
|
result + "\n"
|
427
404
|
rescue => e
|
428
|
-
#
|
429
|
-
|
405
|
+
puts "#{e.message}"
|
406
|
+
''
|
430
407
|
end
|
431
408
|
|
432
409
|
private
|
433
410
|
|
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
|
411
|
+
def handle_tracepoint_event(tp)
|
412
|
+
return unless @enabled
|
413
|
+
return if Thread.current[:enhanced_errors_processing] || Thread.current[:on_capture] || ignored_exception?(tp.raised_exception)
|
414
|
+
Thread.current[:enhanced_errors_processing] = true
|
415
|
+
exception = tp.raised_exception
|
416
|
+
capture_me = !exception.frozen? && EnhancedErrors.eligible_for_capture.call(exception)
|
451
417
|
|
452
|
-
|
418
|
+
unless capture_me
|
419
|
+
Thread.current[:enhanced_errors_processing] = false
|
420
|
+
return
|
421
|
+
end
|
453
422
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
423
|
+
binding_context = tp.binding
|
424
|
+
method_name = tp.method_id
|
425
|
+
method_and_args = {
|
426
|
+
object_name: determine_object_name(tp, method_name),
|
427
|
+
args: extract_arguments(tp, method_name)
|
428
|
+
}
|
458
429
|
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
args: extract_arguments(tp, method_name)
|
463
|
-
}
|
430
|
+
locals = binding_context.local_variables.map { |var|
|
431
|
+
[var, safe_local_variable_get(binding_context, var)]
|
432
|
+
}.to_h
|
464
433
|
|
465
|
-
|
466
|
-
|
467
|
-
|
434
|
+
instance_vars = binding_context.receiver.instance_variables
|
435
|
+
instances = instance_vars.map { |var|
|
436
|
+
[var, safe_instance_variable_get(binding_context.receiver, var)]
|
437
|
+
}.to_h
|
468
438
|
|
469
|
-
|
439
|
+
# No let variables for exceptions
|
440
|
+
lets = {}
|
470
441
|
|
471
|
-
|
472
|
-
|
442
|
+
globals = {}
|
443
|
+
if @debug
|
444
|
+
globals = (global_variables - @original_global_variables).map { |var|
|
445
|
+
[var, get_global_variable_value(var)]
|
473
446
|
}.to_h
|
447
|
+
end
|
474
448
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
globals
|
489
|
-
|
490
|
-
|
491
|
-
|
449
|
+
capture_event = safe_to_s(tp.event)
|
450
|
+
location = "#{safe_to_s(tp.path)}:#{safe_to_s(tp.lineno)}"
|
451
|
+
|
452
|
+
binding_info = {
|
453
|
+
source: location,
|
454
|
+
object: tp.self,
|
455
|
+
library: !!GEMS_REGEX.match?(location),
|
456
|
+
method_and_args: method_and_args,
|
457
|
+
test_name: test_name,
|
458
|
+
variables: {
|
459
|
+
locals: locals,
|
460
|
+
instances: instances,
|
461
|
+
lets: lets,
|
462
|
+
globals: globals
|
463
|
+
},
|
464
|
+
exception: safe_to_s(exception.class.name),
|
465
|
+
capture_event: capture_event
|
466
|
+
}
|
492
467
|
|
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
|
468
|
+
binding_info = default_on_capture(binding_info)
|
526
469
|
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
470
|
+
if on_capture_hook
|
471
|
+
begin
|
472
|
+
Thread.current[:on_capture] = true
|
473
|
+
binding_info = on_capture_hook.call(binding_info)
|
474
|
+
rescue
|
475
|
+
binding_info = nil
|
476
|
+
ensure
|
477
|
+
Thread.current[:on_capture] = false
|
533
478
|
end
|
534
|
-
rescue => e
|
535
|
-
# Avoid any code here that could raise exceptions
|
536
|
-
ensure
|
537
|
-
Thread.current[:enhanced_errors_processing] = false
|
538
479
|
end
|
539
480
|
|
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
|
481
|
+
if binding_info
|
482
|
+
binding_info = validate_binding_format(binding_info)
|
550
483
|
|
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
|
484
|
+
if binding_info && exception.binding_infos.length >= MAX_BINDING_INFOS
|
485
|
+
# delete from the middle of the array as the ends are most interesting
|
486
|
+
exception.binding_infos.delete_at(MAX_BINDING_INFOS / 2.round)
|
559
487
|
end
|
560
488
|
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
original_start(*args) do |*block_args|
|
565
|
-
EnhancedErrors.send(:start_tracing, Thread.current)
|
566
|
-
block.call(*block_args)
|
567
|
-
end
|
489
|
+
if binding_info
|
490
|
+
exception.binding_infos << binding_info
|
491
|
+
override_exception_message(exception, exception.binding_infos) if @override_messages
|
568
492
|
end
|
569
493
|
end
|
494
|
+
rescue
|
495
|
+
# Avoid raising exceptions here
|
496
|
+
ensure
|
497
|
+
Thread.current[:enhanced_errors_processing] = false
|
570
498
|
end
|
571
499
|
|
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
500
|
def ignored_exception?(exception)
|
577
|
-
IGNORED_EXCEPTIONS.
|
501
|
+
IGNORED_EXCEPTIONS.include?(exception.class.name)
|
578
502
|
end
|
579
503
|
|
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
504
|
def test_name
|
584
505
|
if defined?(RSpec)
|
585
506
|
RSpec&.current_example&.full_description
|
@@ -588,9 +509,6 @@ class EnhancedErrors
|
|
588
509
|
nil
|
589
510
|
end
|
590
511
|
|
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
512
|
def default_capture_events
|
595
513
|
events = [:raise]
|
596
514
|
if capture_rescue && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3.0')
|
@@ -599,10 +517,6 @@ class EnhancedErrors
|
|
599
517
|
Set.new(events)
|
600
518
|
end
|
601
519
|
|
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
520
|
def validate_and_set_capture_events(capture_events)
|
607
521
|
if capture_events.nil?
|
608
522
|
@capture_events = default_capture_events
|
@@ -616,12 +530,12 @@ class EnhancedErrors
|
|
616
530
|
end
|
617
531
|
|
618
532
|
if capture_events.include?(:rescue) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.3.0')
|
619
|
-
puts "EnhancedErrors: Warning: :rescue capture_event
|
533
|
+
puts "EnhancedErrors: Warning: :rescue capture_event not supported below Ruby 3.3.0, ignoring it."
|
620
534
|
capture_events = capture_events - [:rescue]
|
621
535
|
end
|
622
536
|
|
623
537
|
if capture_events.empty?
|
624
|
-
puts "No valid capture_events provided to EnhancedErrors.
|
538
|
+
puts "No valid capture_events provided to EnhancedErrors.enhance_exceptions! Falling back to defaults."
|
625
539
|
@capture_events = default_capture_events
|
626
540
|
return
|
627
541
|
end
|
@@ -629,21 +543,12 @@ class EnhancedErrors
|
|
629
543
|
@capture_events = capture_events.to_set
|
630
544
|
end
|
631
545
|
|
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
546
|
def valid_capture_events?(capture_events)
|
637
547
|
return false unless capture_events.is_a?(Array) || capture_events.is_a?(Set)
|
638
548
|
valid_types = [:raise, :rescue].to_set
|
639
549
|
capture_events.to_set.subset?(valid_types)
|
640
550
|
end
|
641
551
|
|
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
552
|
def extract_arguments(tp, method_name)
|
648
553
|
return '' unless method_name
|
649
554
|
begin
|
@@ -653,7 +558,7 @@ class EnhancedErrors
|
|
653
558
|
parameters = method_obj.parameters
|
654
559
|
locals = bind.local_variables
|
655
560
|
|
656
|
-
parameters.map do |(
|
561
|
+
parameters.map do |(_, name)|
|
657
562
|
value = locals.include?(name) ? safe_local_variable_get(bind, name) : nil
|
658
563
|
"#{name}=#{safe_inspect(value)}"
|
659
564
|
rescue => e
|
@@ -664,11 +569,6 @@ class EnhancedErrors
|
|
664
569
|
end
|
665
570
|
end
|
666
571
|
|
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
572
|
def determine_object_name(tp, method_name)
|
673
573
|
if tp.self.is_a?(Class) && tp.self.singleton_class == tp.defined_class
|
674
574
|
"#{safe_to_s(tp.self)}.#{method_name}"
|
@@ -679,22 +579,12 @@ class EnhancedErrors
|
|
679
579
|
"#<Error inspecting value: #{e.message}>"
|
680
580
|
end
|
681
581
|
|
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
582
|
def get_global_variable_value(var)
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
"#<Error getting value for #{var}>"
|
691
|
-
end
|
583
|
+
var.is_a?(Symbol) ? eval("#{var}") : nil
|
584
|
+
rescue => e
|
585
|
+
"#<Error getting value for #{var}>"
|
692
586
|
end
|
693
587
|
|
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
588
|
def method_and_args_desc(method_info)
|
699
589
|
object_name = safe_to_s(method_info[:object_name])
|
700
590
|
args = safe_to_s(method_info[:args])
|
@@ -702,26 +592,18 @@ class EnhancedErrors
|
|
702
592
|
arg_str = args.empty? ? '' : "(#{args})"
|
703
593
|
str = object_name + arg_str
|
704
594
|
"\n#{Colors.green('Method: ')}#{Colors.blue(str)}\n"
|
705
|
-
rescue
|
595
|
+
rescue
|
706
596
|
''
|
707
597
|
end
|
708
598
|
|
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
599
|
def variable_description(vars_hash)
|
714
600
|
vars_hash.map do |name, value|
|
715
601
|
" #{Colors.purple(name)}: #{format_variable(value)}\n"
|
716
602
|
end.join
|
717
|
-
rescue
|
603
|
+
rescue
|
718
604
|
''
|
719
605
|
end
|
720
606
|
|
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
607
|
def format_variable(variable)
|
726
608
|
if awesome_print_available? && Colors.enabled?
|
727
609
|
variable.ai
|
@@ -730,35 +612,24 @@ class EnhancedErrors
|
|
730
612
|
end
|
731
613
|
rescue => e
|
732
614
|
var_str = safe_to_s(variable)
|
733
|
-
"#{var_str}: [Inspection Error]"
|
615
|
+
"#{var_str}: [Inspection Error #{e.message}]"
|
734
616
|
end
|
735
617
|
|
736
|
-
# Checks if the `AwesomePrint` gem is available.
|
737
|
-
#
|
738
|
-
# @return [Boolean] `true` if `AwesomePrint` is available, otherwise `false`.
|
739
618
|
def awesome_print_available?
|
740
619
|
return @awesome_print_available unless @awesome_print_available.nil?
|
741
620
|
@awesome_print_available = defined?(AwesomePrint)
|
742
621
|
end
|
743
622
|
|
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
623
|
def safe_inspect(variable)
|
749
624
|
variable.inspect
|
750
|
-
rescue
|
625
|
+
rescue
|
751
626
|
safe_to_s(variable)
|
752
627
|
end
|
753
628
|
|
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
629
|
def safe_to_s(variable)
|
759
630
|
str = variable.to_s
|
760
|
-
if str.length >
|
761
|
-
str[0...
|
631
|
+
if str.length > 140
|
632
|
+
str[0...140] + '...'
|
762
633
|
else
|
763
634
|
str
|
764
635
|
end
|
@@ -766,53 +637,30 @@ class EnhancedErrors
|
|
766
637
|
"[Unprintable variable]"
|
767
638
|
end
|
768
639
|
|
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
640
|
def safe_local_variable_get(binding_context, var_name)
|
775
641
|
binding_context.local_variable_get(var_name)
|
776
642
|
rescue
|
777
643
|
"[Error accessing local variable #{var_name}]"
|
778
644
|
end
|
779
645
|
|
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
646
|
def safe_instance_variable_get(obj, var_name)
|
786
647
|
obj.instance_variable_get(var_name)
|
787
648
|
rescue
|
788
649
|
"[Error accessing instance variable #{var_name}]"
|
789
650
|
end
|
790
651
|
|
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
652
|
def default_on_format(string)
|
796
653
|
string
|
797
654
|
end
|
798
655
|
|
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
656
|
def default_on_capture(binding_info)
|
804
|
-
# Use this to clean up the captured bindings
|
805
657
|
EnhancedErrors.apply_skip_list(binding_info)
|
806
658
|
end
|
807
659
|
|
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
660
|
def default_eligible_for_capture(exception)
|
813
|
-
|
661
|
+
ignored = ignored_exception?(exception)
|
662
|
+
rspec = exception.class.name.start_with?('RSpec::Matchers')
|
663
|
+
!ignored && !rspec
|
814
664
|
end
|
815
|
-
|
816
|
-
@enabled = false
|
817
665
|
end
|
818
666
|
end
|