tracer 0.1.0 → 0.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.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracer
4
+ module Color
5
+ CLEAR = 0
6
+ BOLD = 1
7
+ UNDERLINE = 4
8
+ REVERSE = 7
9
+ RED = 31
10
+ GREEN = 32
11
+ YELLOW = 33
12
+ BLUE = 34
13
+ MAGENTA = 35
14
+ CYAN = 36
15
+
16
+ class << self
17
+ def colorize(text, seq)
18
+ seq = seq.map { |s| "\e[#{const_get(s)}m" }.join("")
19
+ "#{seq}#{text}#{clear}"
20
+ end
21
+
22
+ def clear
23
+ "\e[#{CLEAR}m"
24
+ end
25
+ end
26
+
27
+ def colorize(str, seq, colorize: @colorize)
28
+ !colorize ? str : Color.colorize(str, seq)
29
+ end
30
+
31
+ def colorize_cyan(str)
32
+ colorize(str, %i[CYAN BOLD])
33
+ end
34
+
35
+ def colorize_blue(str)
36
+ colorize(str, %i[BLUE BOLD])
37
+ end
38
+
39
+ def colorize_magenta(str)
40
+ colorize(str, %i[MAGENTA BOLD])
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class ExceptionTracer < Tracer::Base
6
+ def setup_tp
7
+ TracePoint.new(:raise) do |tp|
8
+ next if skip?(tp)
9
+
10
+ exc = tp.raised_exception
11
+
12
+ out tp,
13
+ " #{colorize_magenta(exc.inspect)}",
14
+ depth: caller.size - (1 + @depth_offset)
15
+ rescue Exception => e
16
+ p e
17
+ end
18
+ end
19
+
20
+ def skip_with_pattern?(tp)
21
+ super && !tp.raised_exception.inspect.match?(@pattern)
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ require "tracer"
2
+
3
+ Object.include(Tracer::Helper)
data/lib/tracer/irb.rb ADDED
@@ -0,0 +1,115 @@
1
+ require_relative "../tracer"
2
+ require "irb/cmd/nop"
3
+ require "irb"
4
+
5
+ module Tracer
6
+ def self.register_irb_commands
7
+ ec = IRB::ExtendCommandBundle.instance_variable_get(:@EXTEND_COMMANDS)
8
+
9
+ [
10
+ [:trace, :Trace, nil, [:trace, IRB::ExtendCommandBundle::OVERRIDE_ALL]],
11
+ [
12
+ :trace_call,
13
+ :TraceCall,
14
+ nil,
15
+ [:trace_call, IRB::ExtendCommandBundle::OVERRIDE_ALL]
16
+ ],
17
+ [
18
+ :trace_exception,
19
+ :TraceException,
20
+ nil,
21
+ [:trace_exception, IRB::ExtendCommandBundle::OVERRIDE_ALL]
22
+ ]
23
+ ].each do |ecconfig|
24
+ ec.push(ecconfig)
25
+ IRB::ExtendCommandBundle.def_extend_command(*ecconfig)
26
+ end
27
+ end
28
+ end
29
+
30
+ module IRB
31
+ module ExtendCommand
32
+ class TraceCommand < Nop
33
+ class << self
34
+ def transform_args(args)
35
+ # Return a string literal as is for backward compatibility
36
+ if args.empty? || string_literal?(args)
37
+ args
38
+ else # Otherwise, consider the input as a String for convenience
39
+ args.strip.dump
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ class Trace < TraceCommand
46
+ category "Tracing"
47
+ description "Trace the target object (or self) in the given expression. Usage: `trace [target,] <expression>`"
48
+
49
+ def execute(*args)
50
+ if args.empty?
51
+ puts "Please provide the expression to trace. Usage: `trace [target,] <expression>`"
52
+ return
53
+ end
54
+
55
+ args = args.first.split(/,/, 2)
56
+
57
+ case args.size
58
+ when 1
59
+ target = irb_context.workspace.main
60
+ expression = args.first
61
+ when 2
62
+ target = eval(args.first, irb_context.workspace.binding)
63
+ expression = args.last
64
+ else
65
+ puts "Please provide the expression to trace. Usage: `trace [target,] <expression>`"
66
+ return
67
+ end
68
+
69
+ unless expression
70
+ puts "Please provide the expression to trace. Usage: `trace [target,] <expression>`"
71
+ return
72
+ end
73
+
74
+ b = irb_context.workspace.binding
75
+ Tracer.trace(target) { eval(expression, b) }
76
+ end
77
+ end
78
+
79
+ class TraceCall < TraceCommand
80
+ category "Tracing"
81
+ description "Trace method calls in the given expression. Usage: `trace_call <expression>`"
82
+
83
+ def execute(*args)
84
+ expression = args.first
85
+
86
+ unless expression
87
+ puts "Please provide the expression to trace. Usage: `trace_call <expression>`"
88
+ return
89
+ end
90
+
91
+ b = irb_context.workspace.binding
92
+ Tracer.trace_call { eval(expression, b) }
93
+ end
94
+ end
95
+
96
+ class TraceException < TraceCommand
97
+ category "Tracing"
98
+ description "Trace exceptions in the given expression. Usage: `trace_exception <expression>`"
99
+
100
+ def execute(*args)
101
+ expression = args.first
102
+
103
+ unless expression
104
+ puts "Please provide the expression to trace. Usage: `trace_exception <expression>`"
105
+ return
106
+ end
107
+
108
+ b = irb_context.workspace.binding
109
+ Tracer.trace_exception { eval(expression, b) }
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ Tracer.register_irb_commands
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class LineTracer < Tracer::Base
6
+ def setup_tp
7
+ TracePoint.new(:line) do |tp|
8
+ next if skip?(tp)
9
+ # pp tp.object_id, caller(0)
10
+ out tp
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class ObjectTracer < Tracer::Base
6
+ attr_reader :target_id, :target_label
7
+
8
+ def initialize(target = nil, target_id: nil, target_label: nil, **kw)
9
+ unless target || target_id
10
+ raise ArgumentError, "target or target_id is required"
11
+ end
12
+
13
+ @target_id = target_id || M_OBJECT_ID.bind_call(target)
14
+ @target_label =
15
+ (target ? safe_inspect(target) : target_label || "<unlabelled>")
16
+ super(**kw)
17
+ end
18
+
19
+ def key
20
+ [@type, @target_id, @pattern, @into].freeze
21
+ end
22
+
23
+ def description
24
+ "for #{@target_label} #{super}"
25
+ end
26
+
27
+ def colorized_target_label
28
+ colorize_magenta(@target_label)
29
+ end
30
+
31
+ PRIMITIVE_METHOD_SOURCES = [Module, Class, Object, Kernel]
32
+
33
+ def setup_tp
34
+ TracePoint.new(:a_call) do |tp|
35
+ next if skip?(tp)
36
+
37
+ if M_OBJECT_ID.bind_call(tp.self) == @target_id
38
+ if PRIMITIVE_METHOD_SOURCES.any? { |klass| klass == tp.defined_class }
39
+ next
40
+ end
41
+
42
+ internal_depth = 2
43
+ klass = tp.defined_class
44
+ method = tp.method_id
45
+ method_info =
46
+ method_info =
47
+ if klass
48
+ if klass.singleton_class?
49
+ if M_IS_A.bind_call(tp.self, Class)
50
+ ".#{method} (#{klass}.#{method})"
51
+ else
52
+ ".#{method}"
53
+ end
54
+ else
55
+ "##{method} (#{klass}##{method})"
56
+ end
57
+ else
58
+ if method
59
+ "##{method} (<unknown>##{method})"
60
+ else
61
+ "<eval or exec with &block>"
62
+ end
63
+ end
64
+
65
+ out tp,
66
+ " #{colorized_target_label} receives #{colorize_blue(method_info)}",
67
+ location: caller_locations(internal_depth, 1).first,
68
+ depth: caller.size - internal_depth - @depth_offset
69
+ elsif !tp.parameters.empty?
70
+ b = tp.binding
71
+ method_info = colorize_blue(minfo(tp))
72
+
73
+ tp.parameters.each do |type, name|
74
+ next unless name
75
+
76
+ colorized_name = colorize_cyan(name)
77
+
78
+ case type
79
+ when :req, :opt, :key, :keyreq
80
+ if M_OBJECT_ID.bind_call(b.local_variable_get(name)) == @target_id
81
+ internal_depth = 4
82
+ out tp,
83
+ " #{colorized_target_label} is used as a parameter #{colorized_name} of #{method_info}",
84
+ location: caller_locations(internal_depth, 1).first,
85
+ depth: caller.size - internal_depth - @depth_offset
86
+ end
87
+ when :rest
88
+ next if name == :"*"
89
+
90
+ internal_depth = 6
91
+ ary = b.local_variable_get(name)
92
+ ary.each do |e|
93
+ if M_OBJECT_ID.bind_call(e) == @target_id
94
+ out tp,
95
+ " #{colorized_target_label} is used as a parameter in #{colorized_name} of #{method_info}",
96
+ location: caller_locations(internal_depth, 1).first,
97
+ depth: caller.size - internal_depth - @depth_offset
98
+ end
99
+ end
100
+ when :keyrest
101
+ next if name == :"**"
102
+ internal_depth = 6
103
+ h = b.local_variable_get(name)
104
+ h.each do |k, e|
105
+ if M_OBJECT_ID.bind_call(e) == @target_id
106
+ out tp,
107
+ " #{colorized_target_label} is used as a parameter in #{colorized_name} of #{method_info}",
108
+ location: caller_locations(internal_depth, 1).first,
109
+ depth: caller.size - internal_depth - @depth_offset
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Tracer
4
- VERSION = "0.1.0"
3
+ module Tracer
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/tracer.rb CHANGED
@@ -1,287 +1,32 @@
1
- # frozen_string_literal: false
2
- #--
3
- # $Release Version: 0.3$
4
- # $Revision: 1.12 $
1
+ # frozen_string_literal: true
5
2
 
6
- ##
7
- # Outputs a source level execution trace of a Ruby program.
8
- #
9
- # It does this by registering an event handler with Kernel#set_trace_func for
10
- # processing incoming events. It also provides methods for filtering unwanted
11
- # trace output (see Tracer.add_filter, Tracer.on, and Tracer.off).
12
- #
13
- # == Example
14
- #
15
- # Consider the following Ruby script
16
- #
17
- # class A
18
- # def square(a)
19
- # return a*a
20
- # end
21
- # end
22
- #
23
- # a = A.new
24
- # a.square(5)
25
- #
26
- # Running the above script using <code>ruby -r tracer example.rb</code> will
27
- # output the following trace to STDOUT (Note you can also explicitly
28
- # <code>require 'tracer'</code>)
29
- #
30
- # #0:<internal:lib/rubygems/custom_require>:38:Kernel:<: -
31
- # #0:example.rb:3::-: class A
32
- # #0:example.rb:3::C: class A
33
- # #0:example.rb:4::-: def square(a)
34
- # #0:example.rb:7::E: end
35
- # #0:example.rb:9::-: a = A.new
36
- # #0:example.rb:10::-: a.square(5)
37
- # #0:example.rb:4:A:>: def square(a)
38
- # #0:example.rb:5:A:-: return a*a
39
- # #0:example.rb:6:A:<: end
40
- # | | | | |
41
- # | | | | ---------------------+ event
42
- # | | | ------------------------+ class
43
- # | | --------------------------+ line
44
- # | ------------------------------------+ filename
45
- # ---------------------------------------+ thread
46
- #
47
- # Symbol table used for displaying incoming events:
48
- #
49
- # +}+:: call a C-language routine
50
- # +{+:: return from a C-language routine
51
- # +>+:: call a Ruby method
52
- # +C+:: start a class or module definition
53
- # +E+:: finish a class or module definition
54
- # +-+:: execute code on a new line
55
- # +^+:: raise an exception
56
- # +<+:: return from a Ruby method
57
- #
58
- # == Copyright
59
- #
60
- # by Keiju ISHITSUKA(keiju@ishitsuka.com)
61
- #
62
- class Tracer
3
+ require_relative "tracer/version"
4
+ require_relative "tracer/line_tracer"
5
+ require_relative "tracer/call_tracer"
6
+ require_relative "tracer/exception_tracer"
7
+ require_relative "tracer/object_tracer"
63
8
 
64
- class << self
65
- # display additional debug information (defaults to false)
66
- attr_accessor :verbose
67
- alias verbose? verbose
9
+ module Tracer
10
+ module Helper
11
+ DEPTH_OFFSET = 3
68
12
 
69
- # output stream used to output trace (defaults to STDOUT)
70
- attr_accessor :stdout
71
-
72
- # mutex lock used by tracer for displaying trace output
73
- attr_reader :stdout_mutex
74
-
75
- # display process id in trace output (defaults to false)
76
- attr_accessor :display_process_id
77
- alias display_process_id? display_process_id
78
-
79
- # display thread id in trace output (defaults to true)
80
- attr_accessor :display_thread_id
81
- alias display_thread_id? display_thread_id
82
-
83
- # display C-routine calls in trace output (defaults to false)
84
- attr_accessor :display_c_call
85
- alias display_c_call? display_c_call
86
- end
87
-
88
- Tracer::stdout = STDOUT
89
- Tracer::verbose = false
90
- Tracer::display_process_id = false
91
- Tracer::display_thread_id = true
92
- Tracer::display_c_call = false
93
-
94
- @stdout_mutex = Thread::Mutex.new
95
-
96
- # Symbol table used for displaying trace information
97
- EVENT_SYMBOL = {
98
- "line" => "-",
99
- "call" => ">",
100
- "return" => "<",
101
- "class" => "C",
102
- "end" => "E",
103
- "raise" => "^",
104
- "c-call" => "}",
105
- "c-return" => "{",
106
- "unknown" => "?"
107
- }
108
-
109
- def initialize # :nodoc:
110
- @threads = Hash.new
111
- if defined? Thread.main
112
- @threads[Thread.main.object_id] = 0
113
- else
114
- @threads[Thread.current.object_id] = 0
13
+ def trace_exception(&block)
14
+ tracer = ExceptionTracer.new(depth_offset: DEPTH_OFFSET)
15
+ tracer.start(&block)
115
16
  end
116
17
 
117
- @get_line_procs = {}
118
-
119
- @filters = []
120
- end
121
-
122
- def stdout # :nodoc:
123
- Tracer.stdout
124
- end
125
-
126
- def on # :nodoc:
127
- if block_given?
128
- on
129
- begin
130
- yield
131
- ensure
132
- off
133
- end
134
- else
135
- set_trace_func method(:trace_func).to_proc
136
- stdout.print "Trace on\n" if Tracer.verbose?
18
+ def trace_call(&block)
19
+ tracer = CallTracer.new(depth_offset: DEPTH_OFFSET)
20
+ tracer.start(&block)
137
21
  end
138
- end
139
-
140
- def off # :nodoc:
141
- set_trace_func nil
142
- stdout.print "Trace off\n" if Tracer.verbose?
143
- end
144
-
145
- def add_filter(p = proc) # :nodoc:
146
- @filters.push p
147
- end
148
-
149
- def set_get_line_procs(file, p = proc) # :nodoc:
150
- @get_line_procs[file] = p
151
- end
152
22
 
153
- def get_line(file, line) # :nodoc:
154
- if p = @get_line_procs[file]
155
- return p.call(line)
156
- end
157
-
158
- unless list = SCRIPT_LINES__[file]
159
- list = File.readlines(file) rescue []
160
- SCRIPT_LINES__[file] = list
161
- end
162
-
163
- if l = list[line - 1]
164
- l
165
- else
166
- "-\n"
23
+ def trace(target, &block)
24
+ tracer = ObjectTracer.new(target, depth_offset: DEPTH_OFFSET)
25
+ tracer.start(&block)
167
26
  end
168
27
  end
169
28
 
170
- def get_thread_no # :nodoc:
171
- if no = @threads[Thread.current.object_id]
172
- no
173
- else
174
- @threads[Thread.current.object_id] = @threads.size
175
- end
176
- end
177
-
178
- def trace_func(event, file, line, id, binding, klass, *) # :nodoc:
179
- return if file == __FILE__
180
-
181
- for p in @filters
182
- return unless p.call event, file, line, id, binding, klass
183
- end
184
-
185
- return unless Tracer::display_c_call? or
186
- event != "c-call" && event != "c-return"
187
-
188
- Tracer::stdout_mutex.synchronize do
189
- if EVENT_SYMBOL[event]
190
- stdout.printf("<%d>", $$) if Tracer::display_process_id?
191
- stdout.printf("#%d:", get_thread_no) if Tracer::display_thread_id?
192
- if line == 0
193
- source = "?\n"
194
- else
195
- source = get_line(file, line)
196
- end
197
- stdout.printf("%s:%d:%s:%s: %s",
198
- file,
199
- line,
200
- klass || '',
201
- EVENT_SYMBOL[event],
202
- source)
203
- end
204
- end
205
-
206
- end
207
-
208
- # Reference to singleton instance of Tracer
209
- Single = new
210
-
211
- ##
212
- # Start tracing
213
- #
214
- # === Example
215
- #
216
- # Tracer.on
217
- # # code to trace here
218
- # Tracer.off
219
- #
220
- # You can also pass a block:
221
- #
222
- # Tracer.on {
223
- # # trace everything in this block
224
- # }
225
-
226
- def Tracer.on
227
- if block_given?
228
- Single.on{yield}
229
- else
230
- Single.on
231
- end
232
- end
233
-
234
- ##
235
- # Disable tracing
236
-
237
- def Tracer.off
238
- Single.off
239
- end
240
-
241
- ##
242
- # Register an event handler <code>p</code> which is called everytime a line
243
- # in +file_name+ is executed.
244
- #
245
- # Example:
246
- #
247
- # Tracer.set_get_line_procs("example.rb", lambda { |line|
248
- # puts "line number executed is #{line}"
249
- # })
250
-
251
- def Tracer.set_get_line_procs(file_name, p = proc)
252
- Single.set_get_line_procs(file_name, p)
253
- end
254
-
255
- ##
256
- # Used to filter unwanted trace output
257
- #
258
- # Example which only outputs lines of code executed within the Kernel class:
259
- #
260
- # Tracer.add_filter do |event, file, line, id, binding, klass, *rest|
261
- # "Kernel" == klass.to_s
262
- # end
263
-
264
- def Tracer.add_filter(p = proc)
265
- Single.add_filter(p)
266
- end
29
+ extend Helper
267
30
  end
268
31
 
269
- # :stopdoc:
270
- SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
271
-
272
- if $0 == __FILE__
273
- # direct call
274
-
275
- $0 = ARGV[0]
276
- ARGV.shift
277
- Tracer.on
278
- require $0
279
- else
280
- # call Tracer.on only if required by -r command-line option
281
- count = caller.count {|bt| %r%/rubygems/core_ext/kernel_require\.rb:% !~ bt}
282
- if (defined?(Gem) and count == 0) or
283
- (!defined?(Gem) and count <= 1)
284
- Tracer.on
285
- end
286
- end
287
- # :startdoc:
32
+ require_relative "tracer/irb"
data/tracer.gemspec CHANGED
@@ -1,26 +1,43 @@
1
- begin
2
- require_relative "lib/tracer/version"
3
- rescue LoadError
4
- # for Ruby core repository
5
- require_relative "version"
6
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/tracer/version"
7
4
 
8
5
  Gem::Specification.new do |spec|
9
- spec.name = "tracer"
10
- spec.version = Tracer::VERSION
11
- spec.authors = ["Keiju ISHITSUKA"]
12
- spec.email = ["keiju@ruby-lang.org"]
6
+ spec.name = "tracer"
7
+ spec.version = Tracer::VERSION
8
+ spec.authors = ["Stan Lo", "Keiju ISHITSUKA"]
9
+ spec.email = %w[stan001212@gmail.com keiju@ruby-lang.org]
13
10
 
14
- spec.summary = %q{Outputs a source level execution trace of a Ruby program.}
15
- spec.description = %q{Outputs a source level execution trace of a Ruby program.}
16
- spec.homepage = "https://github.com/ruby/tracer"
17
- spec.license = "BSD-2-Clause"
11
+ spec.summary = "A Ruby tracer"
12
+ spec.description = "A Ruby tracer"
13
+ spec.homepage = "https://github.com/ruby/tracer"
14
+ spec.licenses = %w[Ruby BSD-2-Clause]
15
+ spec.required_ruby_version = ">= 2.7.0"
18
16
 
19
- spec.files = [".gitignore", ".travis.yml", "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "bin/console", "bin/setup", "lib/tracer.rb", "lib/tracer/version.rb", "tracer.gemspec"]
20
- spec.bindir = "exe"
21
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/ruby/tracer"
19
+ spec.metadata[
20
+ "changelog_uri"
21
+ ] = "https://github.com/ruby/tracer/blob/main/CHANGELOG.md"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files =
26
+ Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0")
28
+ .reject do |f|
29
+ (f == __FILE__) ||
30
+ f.match(
31
+ %r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)}
32
+ )
33
+ end
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
22
37
  spec.require_paths = ["lib"]
23
38
 
24
- spec.add_development_dependency "bundler"
25
- spec.add_development_dependency "rake"
39
+ # Uncomment to register a new dependency of your gem
40
+
41
+ # For more information and examples about making a new gem, check out our
42
+ # guide at: https://bundler.io/guides/creating_gem.html
26
43
  end