im-lost 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be7b612cd1d68529fbba18b5a9a4759e60111facc52e99a20b4da140e887f8c4
4
- data.tar.gz: c1aeff67c05a2e31c0dab72c6c5ed84bb27f9231e759de874882a082f0fac284
3
+ metadata.gz: 533f4f50596d5c6a55f674e813d7053d954a26864ea80c2e266f8c6bd92c00fd
4
+ data.tar.gz: 31a7bd884fabb3aba95789ff9771811300e3c7cb6e75ec3c762a394df603b2ed
5
5
  SHA512:
6
- metadata.gz: 9821d580f5c5e4e62133dba01fbdd9a26ec3cfd8ffe8ed6603b24760d5073184bd22bdbc6ab95317c6a39690abc56ad4e2e5afdd5df4e3d261525afc927448eb
7
- data.tar.gz: b12a64d4ee2b7e9f1764c5bcb0e79a5f9b240b9b49359c10868abc2f763cefb8ea4a7844c0ba17239346157f516b10a6fc8d368386206cb0fea0c6cc330a7601
6
+ metadata.gz: 4631c248cc8af2691d09651ed3a90b863f72ba6954e3c14172208ceb153e69b834e5f67c12555347fa474f2f97a44ad579f8ef85a6b5db4be07ea574b76a7f36
7
+ data.tar.gz: d1accd54c95db533220dd98a6a741380be91050568d16cbc3b6acdaef1ba147b98f2ad39f560b1613b7351c01dca4a44bafd4db46e7ef526b169e5821da68fc0
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Mike Blumtritt
3
+ Copyright (c) 2024-2025 Mike Blumtritt
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -10,7 +10,7 @@ ImLost helps you by analyzing function calls of objects, informing you about exc
10
10
 
11
11
  ## Description
12
12
 
13
- If you like to undertsand method call details you get a call trace with `ImLost.trace`:
13
+ If you like to understand method call details you get a call trace with `ImLost.trace`:
14
14
 
15
15
  ```ruby
16
16
  File.open('test.txt', 'w') do |file|
@@ -22,13 +22,13 @@ end
22
22
 
23
23
  # output will look like
24
24
  # > IO#<<(?)
25
- # /projects/test.rb:1
25
+ # /examples/test.rb:1
26
26
  # > IO#write(*)
27
- # /projects/test.rb:1
27
+ # /examples/test.rb:1
28
28
  # > IO#puts(*)
29
- # /projects/test.rb:2
29
+ # /examples/test.rb:2
30
30
  # > IO#write(*)
31
- # /projects/test.rb:2
31
+ # /examples/test.rb:2
32
32
  ```
33
33
 
34
34
  When you need to know if exceptions are raised and handled you can use `ImLost.trace_exceptions`:
@@ -42,11 +42,11 @@ end
42
42
 
43
43
  # output will look like
44
44
  # x Errno::EEXIST: File exists @ rb_sysopen - /
45
- # /projects/test.rb:2
45
+ # /examples/test.rb:2
46
46
  # ! Errno::EEXIST: File exists @ rb_sysopen - /
47
- # /projects/test.rb:3
47
+ # /examples/test.rb:3
48
48
  # x RuntimeError: something went wrong!
49
- # /projects/test.rb:4
49
+ # /examples/test.rb:4
50
50
  ```
51
51
 
52
52
  When you like to know if a code point is reached, `ImLost.here` will help:
@@ -94,7 +94,6 @@ class Foo
94
94
  def bar = :bar
95
95
  end
96
96
 
97
- ImLost.trace_results = true
98
97
  ImLost.trace(Foo)
99
98
 
100
99
  my_foo = Foo.create(value: :foo!)
@@ -111,47 +110,55 @@ ImLost.vars(my_foo)
111
110
 
112
111
  # output will look like
113
112
  # > Foo.create(:foo!)
114
- # /projects/foo.rb:25
113
+ # /examples/foo.rb:24
115
114
  # > Foo.new(*)
116
- # /projects/foo.rb:6
115
+ # /examples/foo.rb:6
117
116
  # < Foo.new(*)
118
- # = #<Foo:0x00000001030810c0 @value=:foo!>
117
+ # /examples/foo.rb:6
118
+ # = #<Foo:0x00000001006448c0 @value=:foo!>
119
119
  # < Foo.create(:foo!)
120
- # = #<Foo:0x00000001030810c0 @value=:foo!>
120
+ # /examples/foo.rb:24
121
+ # = #<Foo:0x00000001006448c0 @value=:foo!>
121
122
  # > Foo#foo(1, *[], :none, **{}, &nil)
122
- # /projects/foo.rb:28
123
+ # /examples/foo.rb:27
123
124
  # > Foo#bar()
124
- # /projects/foo.rb:15
125
+ # /examples/foo.rb:15
125
126
  # < Foo#bar()
127
+ # /examples/foo.rb:15
126
128
  # = :bar
127
129
  # < Foo#foo(1, *[], :none, **{}, &nil)
130
+ # /examples/foo.rb:27
128
131
  # = "1-none-[]-{}-bar"
129
- # = /projects/foo.rb:29
130
- # instance variables:
131
- # @value: "1-none-[]-{}-bar"
132
+ # * /examples/foo.rb:28
133
+ # > instance variables
134
+ # @value: "1-none-[]-{}-bar"
132
135
  # > Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil)
133
- # /projects/foo.rb:31
136
+ # /examples/foo.rb:30
134
137
  # > Foo#bar()
135
- # /projects/foo.rb:15
138
+ # /examples/foo.rb:15
136
139
  # < Foo#bar()
140
+ # /examples/foo.rb:15
137
141
  # = :bar
138
142
  # < Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil)
143
+ # /examples/foo.rb:30
139
144
  # = "2-some-[a,b,c]-{:name=>:value}-bar"
140
- # = /projects/foo.rb:32
141
- # instance variables:
142
- # @value: "2-some-[a,b,c]-{:name=>:value}-bar"
143
- # > Foo#foo(3, *[], nil, **{}, &#<Proc:0x00000001030aee30 /projects/foo.rb:34>)
144
- # /projects/foo.rb:34
145
+ # * /examples/foo.rb:31
146
+ # > instance variables
147
+ # @value: "2-some-[a,b,c]-{:name=>:value}-bar"
148
+ # > Foo#foo(3, *[], nil, **{}, &#<Proc:0x0000000100641d28 /examples/foo.rb:33>)
149
+ # /examples/foo.rb:33
145
150
  # > Foo#bar()
146
- # /projects/foo.rb:15
151
+ # /examples/foo.rb:15
147
152
  # < Foo#bar()
153
+ # /examples/foo.rb:15
148
154
  # = :bar
149
155
  # 3--[]-{}-bar
150
- # < Foo#foo(3, *[], nil, **{}, &#<Proc:0x00000001030aee30 /projects/foo.rb:34>)
156
+ # < Foo#foo(3, *[], nil, **{}, &#<Proc:0x0000000100641d28 /examples/foo.rb:33>)
157
+ # /examples/foo.rb:33
151
158
  # = nil
152
- # = /projects/foo.rb:35
153
- # instance variables:
154
- # @value: "3--[]-{}-bar"
159
+ # * /examples/foo.rb:34
160
+ # > instance variables
161
+ # @value: "3--[]-{}-bar"
155
162
  ```
156
163
 
157
164
  See [examples dir](./examples) for more…
data/examples/foo.rb CHANGED
@@ -19,7 +19,6 @@ class Foo
19
19
  def bar = :bar
20
20
  end
21
21
 
22
- ImLost.trace_results = true
23
22
  ImLost.trace(Foo)
24
23
 
25
24
  my_foo = Foo.create(value: :foo!)
@@ -36,44 +35,52 @@ ImLost.vars(my_foo)
36
35
 
37
36
  # output will look like
38
37
  # > Foo.create(:foo!)
39
- # /projects/foo.rb:25
38
+ # /examples/foo.rb:24
40
39
  # > Foo.new(*)
41
- # /projects/foo.rb:6
40
+ # /examples/foo.rb:6
42
41
  # < Foo.new(*)
43
- # = #<Foo:0x00000001030810c0 @value=:foo!>
42
+ # /examples/foo.rb:6
43
+ # = #<Foo:0x00000001006448c0 @value=:foo!>
44
44
  # < Foo.create(:foo!)
45
- # = #<Foo:0x00000001030810c0 @value=:foo!>
45
+ # /examples/foo.rb:24
46
+ # = #<Foo:0x00000001006448c0 @value=:foo!>
46
47
  # > Foo#foo(1, *[], :none, **{}, &nil)
47
- # /projects/foo.rb:28
48
+ # /examples/foo.rb:27
48
49
  # > Foo#bar()
49
- # /projects/foo.rb:15
50
+ # /examples/foo.rb:15
50
51
  # < Foo#bar()
52
+ # /examples/foo.rb:15
51
53
  # = :bar
52
54
  # < Foo#foo(1, *[], :none, **{}, &nil)
55
+ # /examples/foo.rb:27
53
56
  # = "1-none-[]-{}-bar"
54
- # = /projects/foo.rb:29
55
- # instance variables:
56
- # @value: "1-none-[]-{}-bar"
57
+ # * /examples/foo.rb:28
58
+ # > instance variables
59
+ # @value: "1-none-[]-{}-bar"
57
60
  # > Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil)
58
- # /projects/foo.rb:31
61
+ # /examples/foo.rb:30
59
62
  # > Foo#bar()
60
- # /projects/foo.rb:15
63
+ # /examples/foo.rb:15
61
64
  # < Foo#bar()
65
+ # /examples/foo.rb:15
62
66
  # = :bar
63
67
  # < Foo#foo(2, *[:a, :b, :c], :some, **{:name=>:value}, &nil)
68
+ # /examples/foo.rb:30
64
69
  # = "2-some-[a,b,c]-{:name=>:value}-bar"
65
- # = /projects/foo.rb:32
66
- # instance variables:
67
- # @value: "2-some-[a,b,c]-{:name=>:value}-bar"
68
- # > Foo#foo(3, *[], nil, **{}, &#<Proc:0x00000001030aee30 /projects/foo.rb:34>)
69
- # /projects/foo.rb:34
70
+ # * /examples/foo.rb:31
71
+ # > instance variables
72
+ # @value: "2-some-[a,b,c]-{:name=>:value}-bar"
73
+ # > Foo#foo(3, *[], nil, **{}, &#<Proc:0x0000000100641d28 /examples/foo.rb:33>)
74
+ # /examples/foo.rb:33
70
75
  # > Foo#bar()
71
- # /projects/foo.rb:15
76
+ # /examples/foo.rb:15
72
77
  # < Foo#bar()
78
+ # /examples/foo.rb:15
73
79
  # = :bar
74
80
  # 3--[]-{}-bar
75
- # < Foo#foo(3, *[], nil, **{}, &#<Proc:0x00000001030aee30 /projects/foo.rb:34>)
81
+ # < Foo#foo(3, *[], nil, **{}, &#<Proc:0x0000000100641d28 /examples/foo.rb:33>)
82
+ # /examples/foo.rb:33
76
83
  # = nil
77
- # = /projects/foo.rb:35
78
- # instance variables:
79
- # @value: "3--[]-{}-bar"
84
+ # * /examples/foo.rb:34
85
+ # > instance variables
86
+ # @value: "3--[]-{}-bar"
@@ -9,7 +9,6 @@ INFO
9
9
 
10
10
  require 'im-lost'
11
11
 
12
- ImLost.trace_results = true
13
12
  ImLost.trace(Kernel, Object, Module, Class, self) do
14
13
  puts '=' * 79
15
14
  pp Class.new
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ImLost
4
4
  # The version number of the gem.
5
- VERSION = '1.1.0'
5
+ VERSION = '1.2.1'
6
6
  end
data/lib/im-lost.rb CHANGED
@@ -26,9 +26,10 @@ module ImLost
26
26
 
27
27
  #
28
28
  # The output device used to write information.
29
- # This should be an `IO` device or any other object responding to `#puts`.
29
+ # This should be an `IO` device or any other object responding to `#<<`
30
+ # like a Logger.
30
31
  #
31
- # `$stderr` is configured by default.
32
+ # `STDERR` is configured by default.
32
33
  #
33
34
  # @example Write to a file
34
35
  # ImLost.output = File.new('./trace', 'w')
@@ -50,8 +51,13 @@ module ImLost
50
51
  attr_reader :output
51
52
 
52
53
  def output=(value)
53
- return @output = value if value.respond_to?(:puts)
54
- raise(ArgumentError, "invalid output device - #{value.inspect}")
54
+ return @output = value if defined?(value.<<)
55
+ raise(
56
+ NoMethodError,
57
+ "undefined method '<<' for an instance of #{
58
+ Kernel.instance_method(:class).bind(value).call
59
+ }"
60
+ )
55
61
  end
56
62
 
57
63
  #
@@ -67,18 +73,18 @@ module ImLost
67
73
  # @attribute [r] trace_calls
68
74
  # @return [Boolean] whether method calls will be traced
69
75
  #
70
- def trace_calls = @trace_calls[0].enabled?
76
+ def trace_calls = @trace_calls.enabled?
71
77
 
72
78
  def trace_calls=(value)
73
79
  if value
74
- @trace_calls.each(&:enable) unless trace_calls
80
+ @trace_calls.enable unless trace_calls
75
81
  elsif trace_calls
76
- @trace_calls.each(&:disable)
82
+ @trace_calls.disable
77
83
  end
78
84
  end
79
85
 
80
86
  #
81
- # Traces execptions raised within a given block.
87
+ # Traces exceptions raised within a given block.
82
88
  #
83
89
  # @example Trace exception and rescue handling
84
90
  # ImLost.trace_exceptions do
@@ -89,13 +95,13 @@ module ImLost
89
95
  #
90
96
  # # output will look like
91
97
  # # x Errno::EEXIST: File exists @ rb_sysopen - /
92
- # # /projects/test.rb:2
98
+ # # /examples/test.rb:2
93
99
  # # ! Errno::EEXIST: File exists @ rb_sysopen - /
94
- # # /projects/test.rb:3
100
+ # # /examples/test.rb:3
95
101
  # # x RuntimeError: something went wrong!
96
- # # /projects/test.rb:4
102
+ # # /examples/test.rb:4
97
103
  #
98
- # @param with_locations [Boolean] wheter the locations should be included
104
+ # @param with_locations [Boolean] whether the locations should be included
99
105
  # into the exception trace information
100
106
  # @yieldreturn [Object] return result
101
107
  #
@@ -112,52 +118,52 @@ module ImLost
112
118
  end
113
119
 
114
120
  #
115
- # Enables/disables tracing of returned valuess of method calls.
116
- # This is disabled by default.
121
+ # Enables/disables tracing of returned values of method calls.
122
+ # This is enabled by default.
117
123
  #
118
124
  # @attribute [r] trace_results
119
125
  # @return [Boolean] whether return values will be traced
120
126
  #
121
- def trace_results = @trace_results[0].enabled?
127
+ def trace_results = @trace_results.enabled?
122
128
 
123
129
  def trace_results=(value)
124
130
  if value
125
- @trace_results.each(&:enable) unless trace_results
131
+ @trace_results.enable unless trace_results
126
132
  elsif trace_results
127
- @trace_results.each(&:disable)
133
+ @trace_results.disable
128
134
  end
129
135
  end
130
136
 
131
137
  #
132
138
  # Print the call location conditionally.
133
139
  #
134
- # @example simply print location
140
+ # @example Print current location
135
141
  # ImLost.here
136
142
  #
137
- # @example print location when instance variable is empty
143
+ # @example Print current location when instance variable is empty
138
144
  # ImLost.here(@name.empty?)
139
145
  #
140
- # @example print location when instance variable is nil or empty
146
+ # @example Print current location when instance variable is nil or empty
141
147
  # ImLost.here { @name.nil? || @name.empty? }
142
148
  #
143
149
  # @overload here
144
- # Prints the caller location.
150
+ # Prints the call location.
145
151
  # @return [true]
146
152
  #
147
153
  # @overload here(test)
148
- # Prints the caller location when given argument is truthy.
154
+ # Prints the call location when given argument is truthy.
149
155
  # @param test [Object]
150
156
  # @return [Object] test
151
157
  #
152
158
  # @overload here
153
- # Prints the caller location when given block returns a truthy result.
159
+ # Prints the call location when given block returns a truthy result.
154
160
  # @yield When the block returns a truthy result the location will be print
155
161
  # @yieldreturn [Object] return result
156
162
  #
157
163
  def here(test = true)
158
164
  return test if !test || (block_given? && !(test = yield))
159
165
  loc = Kernel.caller_locations(1, 1)[0]
160
- @output.puts(": #{loc.path}:#{loc.lineno}")
166
+ @output << "* #{loc.path}:#{loc.lineno}\n"
161
167
  test
162
168
  end
163
169
 
@@ -166,12 +172,12 @@ module ImLost
166
172
  #
167
173
  # The given arguments can be any object instance or module or class.
168
174
  #
169
- # @example trace method calls of an instance variable for a while
175
+ # @example Trace method calls of an instance variable for a while
170
176
  # ImLost.trace(@file)
171
177
  # # ...
172
178
  # ImLost.untrace(@file)
173
179
  #
174
- # @example temporary trace method calls
180
+ # @example Temporary trace method calls
175
181
  # File.open('test.txt', 'w') do |file|
176
182
  # ImLost.trace(file) do
177
183
  # file << 'hello '
@@ -181,17 +187,17 @@ module ImLost
181
187
  #
182
188
  # # output will look like
183
189
  # # > IO#<<(?)
184
- # # /projects/test.rb:1
190
+ # # /examples/test.rb:1
185
191
  # # > IO#write(*)
186
- # # /projects/test.rb:1
192
+ # # /examples/test.rb:1
187
193
  # # > IO#puts(*)
188
- # # /projects/test.rb:2
194
+ # # /examples/test.rb:2
189
195
  # # > IO#write(*)
190
- # # /projects/test.rb:2
196
+ # # /examples/test.rb:2
191
197
  #
192
198
  # @overload trace(*args)
193
199
  # @param args [[Object]] one or more objects to be traced
194
- # @return [[Object]] the traced object(s)
200
+ # @return [Array<Object>] the traced object(s)
195
201
  # Start tracing the given objects.
196
202
  # @see untrace
197
203
  # @see untrace_all!
@@ -209,30 +215,37 @@ module ImLost
209
215
  args.size == 1 ? _trace_b(args[0], &block) : _trace_all_b(args, &block)
210
216
  end
211
217
 
218
+ #
219
+ # Test if a given object is currently traced.
220
+ #
221
+ # @param arg [Object] object to be tested
222
+ # @return [Boolean] wheter the object is beeing traced
223
+ #
224
+ def traced?(obj) = @trace.key?(obj)
225
+
212
226
  #
213
227
  # Stop tracing objects.
214
228
  #
215
- # @example trace some objects for some code lines
216
- # traced_vars = ImLost.trace(@file, @client)
229
+ # @example Trace some objects for some code lines
230
+ # traced_obj = ImLost.trace(@file, @client)
217
231
  # # ...
218
- # ImLost.untrace(*traced_vars)
232
+ # ImLost.untrace(*traced_obj)
219
233
  #
220
234
  # @see trace
221
235
  #
222
- # @param args [[Object]] one or more objects which should not longer be
236
+ # @param args [[]Object]] one or more objects which should not longer be
223
237
  # traced
224
- # @return [[Object]] the object(s) which are not longer be traced
238
+ # @return [Array<Object>] the object(s) which are not longer be traced
225
239
  # @return [nil] when none of the objects was traced before
226
240
  #
227
241
  def untrace(*args)
228
- ret = args.filter_map { @trace.delete(_1.__id__) ? _1 : nil }
229
- args.size == 1 ? ret[0] : ret
242
+ args = args.filter_map { @trace.delete(_1) }
243
+ args.size < 2 ? args[0] : args
230
244
  end
231
245
 
232
246
  #
233
- # Stop tracing any object.
234
- # (When you are really lost and just like to stop tracing of all your
235
- # objects.)
247
+ # Stop tracing any object. When you are really lost and just like to stop
248
+ # tracing of all your objects.
236
249
  #
237
250
  # @see trace
238
251
  #
@@ -244,101 +257,140 @@ module ImLost
244
257
  end
245
258
 
246
259
  #
247
- # Inspect internal variables.
260
+ # Inspect internal variables of a given object.
261
+ #
262
+ # @note The dedicated handling of `Fiber` is platform dependent!
263
+ #
264
+ # @example Inspect current instance variables
265
+ # @a = 22
266
+ # b = 20
267
+ # c = @a + b
268
+ # ImLost.vars(self)
269
+ # # => print value of `@a`
270
+ #
271
+ # @example Inspect local variables
272
+ # @a = 22
273
+ # b = 20
274
+ # c = @a + b
275
+ # ImLost.vars(binding)
276
+ # # => print values of `b` and 'c'
248
277
  #
249
- # @overload vars(binding)
250
- # Inspect local variables of given Binding.
251
- # @param binding [Binding] which local variables should be print
252
- # @return [self] itself
278
+ # @example Inspect a thread's variables
279
+ # th = Thread.new { th[:var1] += 20 }
280
+ # th[:var1] = 22
281
+ # ImLost.vars(th)
282
+ # # => print value of `var1`
283
+ # th.join
284
+ # ImLost.vars(th)
253
285
  #
254
- # @overload vars(object)
255
- # Inspect instance variables of given object.
256
- # @param object [Object] which instance variables should be print
257
- # @return [Object] the given object
286
+ # @example Inspect the current fiber's storage
287
+ # Fiber[:var1] = 22
288
+ # Fiber[:var2] = 20
289
+ # Fiber[:var3] = Fiber[:var1] + Fiber[:var2]
290
+ # ImLost.vars(Fiber.current)
291
+ #
292
+ # When the given object is
293
+ #
294
+ # - a `Binding` it prints the local variables of the binding
295
+ # - a `Thread` it prints the fiber-local and thread variables
296
+ # - the current `Fiber` it prints the fibers' storage
297
+ #
298
+ # Be aware that only the current fiber can be inspected.
299
+ #
300
+ # When the given object can not be inspected it prints an error message.
301
+ #
302
+ # @param object [Object] which instance variables should be print
303
+ # @return [Object] the given object
258
304
  #
259
305
  def vars(object)
260
- traced = @trace.delete(object.__id__)
261
- return _local_vars(object) if object.is_a?(Binding)
262
- return unless object.respond_to?(:instance_variables)
263
- _vars(object, Kernel.caller_locations(1, 1)[0])
306
+ out = LineStore.new
307
+ traced = @trace.delete(object)
308
+ return _local_vars(out, object) if Binding === object
309
+ out.location(Kernel.caller_locations(1, 1)[0])
310
+ return _thread_vars(out, object) if Thread === object
311
+ return _fiber_vars(out, object) if @fiber_support && Fiber === object
312
+ return _instance_vars(out, object) if defined?(object.instance_variables)
313
+ out << ' !!! unable to retrieve vars'
314
+ object
264
315
  ensure
265
316
  @trace[traced] = traced if traced
317
+ @output << out.to_s
266
318
  end
267
319
 
268
320
  private
269
321
 
270
- def as_sig(prefix, info, args)
271
- args = args.join(', ')
272
- case info.self
273
- when Class, Module
274
- "#{prefix} #{info.self}.#{info.method_id}(#{args})"
275
- else
276
- "#{prefix} #{info.defined_class}##{info.method_id}(#{args})"
277
- end
322
+ def _can_trace?(arg)
323
+ (id = arg.__id__) != __id__ && id != @output.__id__
278
324
  end
279
325
 
280
326
  def _trace(arg)
281
- id = arg.__id__
282
- @trace[id] = id if __id__ != id && @output.__id__ != id
327
+ @trace[arg] = arg if _can_trace?(arg)
283
328
  arg
284
329
  end
285
330
 
286
- def _trace_all(args)
287
- args.each do |arg|
288
- arg = arg.__id__
289
- @trace[arg] = arg if __id__ != arg && @output.__id__ != arg
290
- end
291
- args
292
- end
331
+ def _trace_all(args) = args.each { @trace[_1] = _1 if _can_trace?(_1) }
293
332
 
294
333
  def _trace_b(arg)
295
- id = arg.__id__
296
- return yield(arg) if __id__ == id || @output.__id__ == id
334
+ return yield(arg) if @trace.key?(arg) || !_can_trace?(arg)
297
335
  begin
298
- @trace[id] = id
336
+ @trace[arg] = arg
299
337
  yield(arg)
300
338
  ensure
301
- @trace.delete(id) if id
339
+ @trace.delete(arg)
302
340
  end
303
341
  end
304
342
 
305
343
  def _trace_all_b(args)
306
- ids =
344
+ temp =
307
345
  args.filter_map do |arg|
308
- arg = arg.__id__
309
- @trace[arg] = arg if __id__ != arg && @output.__id__ != arg
346
+ @trace[arg] = arg if !@trace.key?(arg) && _can_trace?(arg)
310
347
  end
311
348
  yield(args)
312
349
  ensure
313
- ids.each { @trace.delete(_1) }
350
+ temp.each { @trace.delete(_1) }
314
351
  end
315
352
 
316
- def _vars(obj, location)
317
- @output.puts("= #{location.path}:#{location.lineno}")
318
- vars = obj.instance_variables
319
- if vars.empty?
320
- @output.puts(' <no instance variables defined>')
321
- return obj
353
+ def _local_vars(out, binding)
354
+ out << "* #{binding.source_location.join(':')}"
355
+ out.vars('local variables', binding.local_variables) do |name|
356
+ binding.local_variable_get(name)
322
357
  end
323
- @output.puts(' instance variables:')
324
- vars.sort!.each do |name|
325
- @output.puts(" #{name}: #{obj.instance_variable_get(name).inspect}")
358
+ binding
359
+ end
360
+
361
+ def _thread_vars(out, thread)
362
+ out << " #{_thread_identifier(thread)}"
363
+ flv = thread.keys
364
+ out.vars('fiber-local variables', flv) { thread[_1] } unless flv.empty?
365
+ out.vars('thread variables', thread.thread_variables) do |name|
366
+ thread.thread_variable_get(name)
326
367
  end
327
- obj
368
+ thread
328
369
  end
329
370
 
330
- def _local_vars(binding)
331
- @output.puts("= #{binding.source_location.join(':')}")
332
- vars = binding.local_variables
333
- if vars.empty?
334
- @output.puts(' <no local variables>')
335
- return self
371
+ def _fiber_vars(out, fiber)
372
+ if Fiber.current == fiber
373
+ storage = fiber.storage || {}
374
+ out.vars('fiber storage', storage.keys) { storage[_1] }
375
+ else
376
+ out << ' !!! given Fiber is not the current Fiber' <<
377
+ " #{fiber.inspect}"
336
378
  end
337
- @output.puts(' local variables:')
338
- vars.sort!.each do |name|
339
- @output.puts(" #{name}: #{binding.local_variable_get(name).inspect}")
379
+ fiber
380
+ end
381
+
382
+ def _instance_vars(out, object)
383
+ out.vars('instance variables', object.instance_variables) do |n|
384
+ object.instance_variable_get(n)
340
385
  end
341
- self
386
+ object
387
+ end
388
+
389
+ def _thread_identifier(thr)
390
+ state = thr.status
391
+ "#{state || (state.nil? ? 'aborted' : 'terminated')} Thread #{
392
+ defined?(thr.native_thread_id) ? thr.native_thread_id : thr.__id__
393
+ } #{thr.name}".rstrip
342
394
  end
343
395
  end
344
396
 
@@ -365,7 +417,7 @@ module ImLost
365
417
  # # the timer with name 'my_test' is not longer valid now
366
418
  #
367
419
  #
368
- # @example Use an anonymous timer (identified by ID)
420
+ # @example Use an anonymous timer
369
421
  # tmr = ImLost.timer.create
370
422
  #
371
423
  # # ...your code here...
@@ -397,12 +449,12 @@ module ImLost
397
449
  def count = ids.size
398
450
 
399
451
  # @attribute [r] empty?
400
- # @return [Boolean] wheter the timer store is empty or not
452
+ # @return [Boolean] whether the timer store is empty or not
401
453
  def empty? = ids.empty?
402
454
 
403
455
  # @attribute [r] ids
404
456
  # @return [Array<Integer>] IDs of all registered timers
405
- def ids = (@ll.keys.keep_if { _1.is_a?(Integer) })
457
+ def ids = (@ll.keys.keep_if { Integer === _1 })
406
458
 
407
459
  #
408
460
  # Create and register a new named or anonymous timer.
@@ -428,7 +480,7 @@ module ImLost
428
480
  #
429
481
  def delete(*id_or_names)
430
482
  id_or_names.flatten.each do |id|
431
- if id.is_a?(Integer)
483
+ if Integer === id
432
484
  del = @ll.delete(id)
433
485
  @ll.delete(del[0]) if del
434
486
  else
@@ -440,7 +492,7 @@ module ImLost
440
492
  end
441
493
 
442
494
  #
443
- # Print the ID or name and the runtime since timer was created.
495
+ # Print the ID or name and the runtime since a timer was created.
444
496
  # It includes the location.
445
497
  #
446
498
  # @param id_or_name [Integer, #to_s] the identifier or the name of the timer
@@ -449,10 +501,10 @@ module ImLost
449
501
  # identifier or name
450
502
  #
451
503
  def [](id_or_name)
452
- time = self.class.now
453
- timer = @ll[id_or_name.is_a?(Integer) ? id_or_name : id_or_name.to_s]
504
+ now = self.class.now
505
+ timer = @ll[Integer === id_or_name ? id_or_name : id_or_name.to_s]
454
506
  raise(ArgumentError, "not a timer - #{id_or_name.inspect}") unless timer
455
- @cb[timer[0], Kernel.caller_locations(1, 1)[0], time - timer[1]]
507
+ @cb[timer[0], Kernel.caller_locations(1, 1)[0], (now - timer[1]).round(4)]
456
508
  timer.__id__
457
509
  end
458
510
 
@@ -469,87 +521,132 @@ module ImLost
469
521
  nil
470
522
  end
471
523
 
472
- # @!visibility private
524
+ private
525
+
473
526
  def initialize(&block)
474
527
  @cb = block
475
528
  @ll = {}
476
529
  end
477
530
  end
478
531
 
479
- ARG_SIG = { rest: '*', keyrest: '**', block: '&' }.compare_by_identity.freeze
480
- NO_NAME = { :* => 1, :** => 1, :& => 1 }.compare_by_identity.freeze
481
- private_constant :ARG_SIG, :NO_NAME
532
+ class LineStore
533
+ def initialize(*lines) = (@lines = lines)
534
+ def to_s = (@lines << nil).join("\n")
535
+ def <<(str) = @lines << str
536
+ def location(loc) = @lines << "* #{loc.path}:#{loc.lineno}"
482
537
 
483
- @trace = {}.compare_by_identity
484
- @caller_locations = true
485
- @output = $stderr.respond_to?(:puts) ? $stderr : STDERR
486
-
487
- @timer =
488
- TimerStore.new do |title, location, time|
489
- @output.puts(
490
- "T #{title}: #{time ? "#{time} sec." : 'created'}",
491
- " #{location.path}:#{location.lineno}"
492
- )
538
+ def vars(kind, names)
539
+ return @lines << " <no #{kind} defined>" if names.empty?
540
+ @lines << " > #{kind}"
541
+ names.sort!.each { @lines << " #{_1}: #{yield(_1).inspect}" }
493
542
  end
494
- TimerStore.private_class_method(:new)
543
+ end
544
+ private_constant :LineStore
495
545
 
496
- @trace_calls = [
497
- TracePoint.new(:c_call) do |tp|
498
- next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__
499
- @output.puts(as_sig('>', tp, tp.parameters.map { ARG_SIG[_1[0]] || '?' }))
500
- @output.puts(" #{tp.path}:#{tp.lineno}") if @caller_locations
501
- end,
502
- TracePoint.new(:call) do |tp|
503
- next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__
504
- ctx = tp.binding
505
- @output.puts(
506
- as_sig(
507
- '>',
508
- tp,
509
- tp.parameters.map do |kind, name|
510
- next name if NO_NAME.key?(name)
511
- "#{ARG_SIG[kind]}#{ctx.local_variable_get(name).inspect}"
512
- end
513
- )
514
- )
515
- next unless @caller_locations
516
- loc = ctx.eval('caller_locations(4,1)')[0]
517
- @output.puts(" #{loc.path}:#{loc.lineno}")
518
- end
519
- ]
520
-
521
- @trace_results = [
522
- TracePoint.new(:c_return) do |tp|
523
- next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__
524
- @output.puts(as_sig('<', tp, tp.parameters.map { ARG_SIG[_1[0]] || '?' }))
525
- @output.puts(" = #{tp.return_value.inspect}")
526
- end,
527
- TracePoint.new(:return) do |tp|
528
- next if !@trace.key?(tp.self.__id__) || tp.path == __FILE__
529
- ctx = tp.binding
530
- @output.puts(
531
- as_sig(
532
- '<',
533
- tp,
534
- tp.parameters.map do |kind, name|
535
- next name if NO_NAME.key?(name)
536
- "#{ARG_SIG[kind]}#{ctx.local_variable_get(name).inspect}"
537
- end
538
- )
539
- )
540
- @output.puts(" = #{tp.return_value.inspect}")
546
+ class CCallLineStore < LineStore
547
+ def initialize(prefix, info, include_location)
548
+ @info = info
549
+ sig = "#{info.method_id}(#{args.join(', ')})"
550
+ case info.self
551
+ when Class, Module
552
+ super("#{prefix} #{info.self}.#{sig}")
553
+ else
554
+ super("#{prefix} #{info.defined_class}##{sig}")
555
+ end
556
+ return unless include_location
557
+ loc = location
558
+ path = loc.path
559
+ path = path[10..-2] if path.start_with?('<internal:')
560
+ @lines << " #{path}:#{loc.lineno}"
541
561
  end
542
- ]
543
562
 
544
- supported = RUBY_VERSION.to_f < 3.3 ? %i[raise] : %i[raise rescue]
563
+ def location = @info
564
+
565
+ def args
566
+ argc = -1
567
+ @info.parameters.map do |kind, _|
568
+ argc += 1
569
+ CARG_TYPE[kind] || "arg#{argc}"
570
+ end
571
+ end
572
+
573
+ CARG_TYPE = {
574
+ rest: '*args',
575
+ keyrest: '**kwargs',
576
+ block: '&block'
577
+ }.compare_by_identity.freeze
578
+ end
579
+ private_constant :CCallLineStore
580
+
581
+ class RbCallLineStore < CCallLineStore
582
+ def initialize(prefix, info, include_location)
583
+ @ctx = info.binding
584
+ super
585
+ @ctx = nil
586
+ end
587
+
588
+ def location
589
+ locations = @ctx.eval('caller_locations(7,3)')
590
+ unless locations[1].path.start_with?('<internal:prelude')
591
+ return locations[1]
592
+ end
593
+ locations[0].path.start_with?('<internal:') ? locations[0] : locations[2]
594
+ end
595
+
596
+ def args
597
+ @info.parameters.map do |kind, name|
598
+ next name if ARG_SKIP.key?(name)
599
+ "#{ARG_TYPE[kind]}#{@ctx.local_variable_get(name).inspect}"
600
+ end
601
+ end
602
+
603
+ ARG_TYPE = { rest: :*, keyrest: :**, block: :& }.compare_by_identity.freeze
604
+ ARG_SKIP = ARG_TYPE.invert.compare_by_identity.freeze
605
+ end
606
+ private_constant :RbCallLineStore
607
+
608
+ @trace_calls =
609
+ TracePoint.new(:c_call, :call) do |tp|
610
+ next if !@trace.key?(tp.self) || tp.path == __FILE__
611
+ klass = tp.event == :c_call ? CCallLineStore : RbCallLineStore
612
+ @output << klass.new('>', tp, @caller_locations).to_s
613
+ end
614
+
615
+ @trace_results =
616
+ TracePoint.new(:c_return, :return) do |tp|
617
+ next if !@trace.key?(tp.self) || tp.path == __FILE__
618
+ klass = tp.event == :c_return ? CCallLineStore : RbCallLineStore
619
+ out = klass.new('<', tp, @caller_locations)
620
+ out << " = #{tp.return_value.inspect}"
621
+ @output << out.to_s
622
+ end
623
+
624
+ exception_support = RUBY_VERSION.to_f < 3.3 ? %i[raise] : %i[raise rescue]
545
625
  @trace_exceptions =
546
- TracePoint.new(*supported) do |tp|
547
- ex = tp.raised_exception.inspect
548
- @output.puts(
549
- "#{tp.event == :raise ? 'x' : '!'} #{ex[0] == '#' ? ex[2..-2] : ex}"
550
- )
551
- @output.puts(" #{tp.path}:#{tp.lineno}") if @exception_locations
626
+ TracePoint.new(*exception_support) do |tp|
627
+ ex = tp.raised_exception
628
+ mark, parent = tp.event == :rescue ? ['!', ex.cause] : 'x'
629
+ ex = ex.inspect
630
+ out = LineStore.new("#{mark} #{ex[0] == '#' ? ex[2..-2] : ex}")
631
+ while parent
632
+ ex = parent.inspect
633
+ out << " [#{ex[0] == '#' ? ex[2..-2] : ex}]"
634
+ parent = parent.cause
635
+ end
636
+ out << " #{tp.path}:#{tp.lineno}" if @exception_locations
637
+ @output << out.to_s
552
638
  end
553
639
 
554
- self.trace_calls = true
640
+ @timer = TimerStore.new { |title, location, time| @output << <<~TIMER_MSG }
641
+ T #{title}: #{time ? "#{time} sec." : 'created'}
642
+ #{location.path}:#{location.lineno}
643
+ TIMER_MSG
644
+ TimerStore.private_class_method(:new)
645
+
646
+ @fiber_support = !!defined?(Fiber.current.storage)
647
+
648
+ @output = STDERR
649
+ @trace = {}.compare_by_identity
650
+ @caller_locations = @exception_locations = true
651
+ self.trace_calls = self.trace_results = true
555
652
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: im-lost
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-05-29 00:00:00.000000000 Z
10
+ date: 2025-01-30 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: |
14
13
  If you have overlooked something again and don't really understand what
@@ -19,7 +18,6 @@ description: |
19
18
  ImLost helps you by analyzing function calls of objects, informing you
20
19
  about exceptions and logging your way through your code. In short, ImLost
21
20
  is your debugging helper!
22
- email:
23
21
  executables: []
24
22
  extensions: []
25
23
  extra_rdoc_files:
@@ -42,7 +40,6 @@ metadata:
42
40
  bug_tracker_uri: https://github.com/mblumtritt/im-lost/issues
43
41
  documentation_uri: https://rubydoc.info/gems/im-lost
44
42
  rubygems_mfa_required: 'true'
45
- post_install_message:
46
43
  rdoc_options: []
47
44
  require_paths:
48
45
  - lib
@@ -57,8 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
54
  - !ruby/object:Gem::Version
58
55
  version: '0'
59
56
  requirements: []
60
- rubygems_version: 3.5.11
61
- signing_key:
57
+ rubygems_version: 3.6.3
62
58
  specification_version: 4
63
59
  summary: Your debugging helper.
64
60
  test_files: []