tracer 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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