tracer 0.1.1 → 0.2.0

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,90 @@
1
+ require "irb/cmd/nop"
2
+ require "irb"
3
+
4
+ module Tracer
5
+ def self.register_irb_commands
6
+ ec = IRB::ExtendCommandBundle.instance_variable_get(:@EXTEND_COMMANDS)
7
+
8
+ [
9
+ [:trace, :Trace, nil, [:trace, IRB::ExtendCommandBundle::OVERRIDE_ALL]],
10
+ [
11
+ :trace_call,
12
+ :TraceCall,
13
+ nil,
14
+ [:trace_call, IRB::ExtendCommandBundle::OVERRIDE_ALL]
15
+ ],
16
+ [
17
+ :trace_exception,
18
+ :TraceException,
19
+ nil,
20
+ [:trace_exception, IRB::ExtendCommandBundle::OVERRIDE_ALL]
21
+ ]
22
+ ].each do |ecconfig|
23
+ ec.push(ecconfig)
24
+ IRB::ExtendCommandBundle.def_extend_command(*ecconfig)
25
+ end
26
+ end
27
+ end
28
+
29
+ module IRB
30
+ module ExtendCommand
31
+ class TraceCommand < Nop
32
+ class << self
33
+ def transform_args(args)
34
+ # Return a string literal as is for backward compatibility
35
+ if args.empty? || string_literal?(args)
36
+ args
37
+ else # Otherwise, consider the input as a String for convenience
38
+ args.strip.dump
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ class Trace < TraceCommand
45
+ category "Tracing"
46
+ description "Trace the target object (or self) in the given expression. Usage: `trace [target,] <expression>`"
47
+
48
+ def execute(*args)
49
+ args = args.first.split(/,/, 2)
50
+
51
+ case args.size
52
+ when 1
53
+ target = irb_context.workspace.main
54
+ expression = args.first
55
+ when 2
56
+ target = eval(args.first, irb_context.workspace.binding)
57
+ expression = args.last
58
+ else
59
+ raise ArgumentError,
60
+ "wrong number of arguments (given #{args.size}, expected 1..2)"
61
+ end
62
+
63
+ b = irb_context.workspace.binding
64
+ Tracer.trace(target) { eval(expression, b) }
65
+ end
66
+ end
67
+
68
+ class TraceCall < TraceCommand
69
+ category "Tracing"
70
+ description "Trace method calls in the given expression. Usage: `trace_call <expression>`"
71
+
72
+ def execute(expression)
73
+ b = irb_context.workspace.binding
74
+ Tracer.trace_call { eval(expression, b) }
75
+ end
76
+ end
77
+
78
+ class TraceException < TraceCommand
79
+ category "Tracing"
80
+ description "Trace exceptions in the given expression. Usage: `trace_exception <expression>`"
81
+
82
+ def execute(expression)
83
+ b = irb_context.workspace.binding
84
+ Tracer.trace_exception { eval(expression, b) }
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracer
4
+ VERSION = "0.2.0"
5
+ end
data/lib/tracer.rb CHANGED
@@ -1,292 +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
63
- VERSION = "0.1.1"
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"
64
8
 
65
- class << self
66
- # display additional debug information (defaults to false)
67
- attr_accessor :verbose
68
- alias verbose? verbose
9
+ module Tracer
10
+ module Helper
11
+ DEPTH_OFFSET = 3
69
12
 
70
- # output stream used to output trace (defaults to STDOUT)
71
- attr_accessor :stdout
72
-
73
- # mutex lock used by tracer for displaying trace output
74
- attr_reader :stdout_mutex
75
-
76
- # display process id in trace output (defaults to false)
77
- attr_accessor :display_process_id
78
- alias display_process_id? display_process_id
79
-
80
- # display thread id in trace output (defaults to true)
81
- attr_accessor :display_thread_id
82
- alias display_thread_id? display_thread_id
83
-
84
- # display C-routine calls in trace output (defaults to false)
85
- attr_accessor :display_c_call
86
- alias display_c_call? display_c_call
87
- end
88
-
89
- Tracer::stdout = STDOUT
90
- Tracer::verbose = false
91
- Tracer::display_process_id = false
92
- Tracer::display_thread_id = true
93
- Tracer::display_c_call = false
94
-
95
- @stdout_mutex = Thread::Mutex.new
96
-
97
- # Symbol table used for displaying trace information
98
- EVENT_SYMBOL = {
99
- "line" => "-",
100
- "call" => ">",
101
- "return" => "<",
102
- "class" => "C",
103
- "end" => "E",
104
- "raise" => "^",
105
- "c-call" => "}",
106
- "c-return" => "{",
107
- "unknown" => "?"
108
- }
109
-
110
- def initialize # :nodoc:
111
- @threads = Hash.new
112
- if defined? Thread.main
113
- @threads[Thread.main.object_id] = 0
114
- else
115
- @threads[Thread.current.object_id] = 0
13
+ def trace_exception(&block)
14
+ tracer = ExceptionTracer.new(depth_offset: DEPTH_OFFSET)
15
+ tracer.start(&block)
116
16
  end
117
17
 
118
- @get_line_procs = {}
119
-
120
- @filters = []
121
- end
122
-
123
- def stdout # :nodoc:
124
- Tracer.stdout
125
- end
126
-
127
- def on # :nodoc:
128
- if block_given?
129
- on
130
- begin
131
- yield
132
- ensure
133
- off
134
- end
135
- else
136
- set_trace_func method(:trace_func).to_proc
137
- 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)
138
21
  end
139
- end
140
-
141
- def off # :nodoc:
142
- set_trace_func nil
143
- stdout.print "Trace off\n" if Tracer.verbose?
144
- end
145
-
146
- def add_filter(p = nil, &b) # :nodoc:
147
- p ||= b
148
- @filters.push p
149
- end
150
-
151
- def set_get_line_procs(file, p = nil, &b) # :nodoc:
152
- p ||= b
153
- @get_line_procs[file] = p
154
- end
155
22
 
156
- def get_line(file, line) # :nodoc:
157
- if p = @get_line_procs[file]
158
- return p.call(line)
159
- end
160
-
161
- unless list = SCRIPT_LINES__[file]
162
- list = File.readlines(file) rescue []
163
- SCRIPT_LINES__[file] = list
164
- end
165
-
166
- if l = list[line - 1]
167
- l
168
- else
169
- "-\n"
23
+ def trace(target, &block)
24
+ tracer = ObjectTracer.new(target, depth_offset: DEPTH_OFFSET)
25
+ tracer.start(&block)
170
26
  end
171
27
  end
172
28
 
173
- def get_thread_no # :nodoc:
174
- if no = @threads[Thread.current.object_id]
175
- no
176
- else
177
- @threads[Thread.current.object_id] = @threads.size
178
- end
179
- end
180
-
181
- def trace_func(event, file, line, id, binding, klass, *) # :nodoc:
182
- return if file == __FILE__
183
-
184
- for p in @filters
185
- return unless p.call event, file, line, id, binding, klass
186
- end
187
-
188
- return unless Tracer::display_c_call? or
189
- event != "c-call" && event != "c-return"
190
-
191
- Tracer::stdout_mutex.synchronize do
192
- if EVENT_SYMBOL[event]
193
- stdout.printf("<%d>", $$) if Tracer::display_process_id?
194
- stdout.printf("#%d:", get_thread_no) if Tracer::display_thread_id?
195
- if line == 0
196
- source = "?\n"
197
- else
198
- source = get_line(file, line)
199
- end
200
- stdout.printf("%s:%d:%s:%s: %s",
201
- file,
202
- line,
203
- klass || '',
204
- EVENT_SYMBOL[event],
205
- source)
206
- end
207
- end
208
-
209
- end
210
-
211
- # Reference to singleton instance of Tracer
212
- Single = new
213
-
214
- ##
215
- # Start tracing
216
- #
217
- # === Example
218
- #
219
- # Tracer.on
220
- # # code to trace here
221
- # Tracer.off
222
- #
223
- # You can also pass a block:
224
- #
225
- # Tracer.on {
226
- # # trace everything in this block
227
- # }
228
-
229
- def Tracer.on
230
- if block_given?
231
- Single.on{yield}
232
- else
233
- Single.on
234
- end
235
- end
236
-
237
- ##
238
- # Disable tracing
239
-
240
- def Tracer.off
241
- Single.off
242
- end
243
-
244
- ##
245
- # Register an event handler <code>p</code> which is called every time a line
246
- # in +file_name+ is executed.
247
- #
248
- # Example:
249
- #
250
- # Tracer.set_get_line_procs("example.rb", lambda { |line|
251
- # puts "line number executed is #{line}"
252
- # })
253
-
254
- def Tracer.set_get_line_procs(file_name, p = nil, &b)
255
- p ||= b
256
- Single.set_get_line_procs(file_name, p)
257
- end
258
-
259
- ##
260
- # Used to filter unwanted trace output
261
- #
262
- # Example which only outputs lines of code executed within the Kernel class:
263
- #
264
- # Tracer.add_filter do |event, file, line, id, binding, klass, *rest|
265
- # "Kernel" == klass.to_s
266
- # end
267
-
268
- def Tracer.add_filter(p = nil, &b)
269
- p ||= b
270
- Single.add_filter(p)
271
- end
29
+ extend Helper
272
30
  end
273
31
 
274
- # :stopdoc:
275
- SCRIPT_LINES__ = {} unless defined? SCRIPT_LINES__
276
-
277
- if $0 == __FILE__
278
- # direct call
279
-
280
- $0 = ARGV[0]
281
- ARGV.shift
282
- Tracer.on
283
- require $0
284
- else
285
- # call Tracer.on only if required by -r command-line option
286
- count = caller.count {|bt| %r%/rubygems/core_ext/kernel_require\.rb:% !~ bt}
287
- if (defined?(Gem) and count == 0) or
288
- (!defined?(Gem) and count <= 1)
289
- Tracer.on
290
- end
291
- end
292
- # :startdoc:
32
+ require_relative "tracer/irb"
data/tracer.gemspec CHANGED
@@ -1,25 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- name = File.basename(__FILE__, ".gemspec")
4
- version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir|
5
- break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line|
6
- /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1
7
- end rescue nil
8
- end
3
+ require_relative "lib/tracer/version"
9
4
 
10
5
  Gem::Specification.new do |spec|
11
- spec.name = name
12
- spec.version = version
13
- spec.authors = ["Keiju ISHITSUKA"]
14
- 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]
10
+
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"
15
16
 
16
- spec.summary = %q{Outputs a source level execution trace of a Ruby program.}
17
- spec.description = %q{Outputs a source level execution trace of a Ruby program.}
18
- spec.homepage = "https://github.com/ruby/tracer"
19
- spec.licenses = ["Ruby", "BSD-2-Clause"]
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"
20
22
 
21
- spec.files = ["Gemfile", "LICENSE.txt", "README.md", "Rakefile", "lib/tracer.rb", "tracer.gemspec"]
22
- spec.bindir = "exe"
23
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
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) }
24
37
  spec.require_paths = ["lib"]
38
+
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
25
43
  end