unroller 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Readme +182 -0
- data/lib/unroller.rb +3 -0
- data/lib/unroller/unroller.rb +564 -0
- metadata +69 -0
data/Readme
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
= <i>Ruby Unroller</i> -- a code tracing tool for Ruby
|
2
|
+
|
3
|
+
[<b>Home page</b>:] http://unroller.rubyforge.org/
|
4
|
+
[<b>Project site</b>:] http://rubyforge.org/projects/unroller/
|
5
|
+
[<b>Wiki</b>:] http://whynotwiki.com/Ruby_Unroller
|
6
|
+
[<b>Author</b>:] Tyler Rick
|
7
|
+
[<b>Copyright</b>:] 2007 QualitySmith, Inc.
|
8
|
+
[<b>License</b>:] GPL
|
9
|
+
|
10
|
+
==Introduction / What it is
|
11
|
+
|
12
|
+
Ruby Unroller is a tool for generating human-readable "execution traces".
|
13
|
+
While it is enabled, it will watch every Ruby statement and method call that gets executed and will display the source code on your screen in real-time as it is being executed.
|
14
|
+
|
15
|
+
You can use it for...
|
16
|
+
|
17
|
+
===Debugging
|
18
|
+
|
19
|
+
A very efficient tool for seeing what code actually gets executed... Add it to your toolbox along with the other tricks you already know, like inspecting <tt>caller()</tt> or printing "got here" at various places in your code.
|
20
|
+
|
21
|
+
===Learning/Exploring
|
22
|
+
|
23
|
+
It's a great tool for exploring 3rd-party source code that you aren't familiar with. If you've ever found yourself wondering "How was that method implemented?" or "What's going on behind the scenes when I call such-and-such?", then this tool is for you!
|
24
|
+
|
25
|
+
|
26
|
+
==Usage
|
27
|
+
|
28
|
+
Just insert these line before the point you want to start tracing:
|
29
|
+
|
30
|
+
require 'rubygems'
|
31
|
+
require 'unroller'
|
32
|
+
Unroller::trace
|
33
|
+
|
34
|
+
And if you want the tracing to stop, just add this line somewhere:
|
35
|
+
|
36
|
+
Unroller::trace_off
|
37
|
+
|
38
|
+
You can also pass a block to <tt>Unroller::trace</tt> and it will automatically turn off tracing as soon as the block has been executed.
|
39
|
+
|
40
|
+
Unroller::trace do
|
41
|
+
...
|
42
|
+
end
|
43
|
+
|
44
|
+
===Example
|
45
|
+
|
46
|
+
Say I have an ActiveRecord model and want to know exactly what actually goes on behind the scenes when I call model.save! . All I have to do is wrap the method call in a "trace" block, like this:
|
47
|
+
|
48
|
+
Unroller::trace do
|
49
|
+
model.save!
|
50
|
+
end
|
51
|
+
|
52
|
+
, start the script, and then watch as the source code is unfolded before my very eyes!
|
53
|
+
|
54
|
+
Screenshot[link:include/screenshot1.png]
|
55
|
+
|
56
|
+
This is much more efficient and reliable than manually tracing through the execution yourself! (Trying to guess which file the <tt>save!</tt> method would be defined in, searching for the file in the depths of <tt>/usr/lib/ruby/gems</tt>, scrolling down to the right line number, and repeating a zillion times...)
|
57
|
+
|
58
|
+
|
59
|
+
===Reducing verbosity
|
60
|
+
|
61
|
+
This can generate some really *verbose* output... A couple options are available to help things under control:
|
62
|
+
|
63
|
+
Ignore a section of code that is within the block passed to +trace+.
|
64
|
+
|
65
|
+
Unroller::trace do
|
66
|
+
stuff_you_care_about
|
67
|
+
Unroller::exclude do
|
68
|
+
stuff_that_you_really_dont_care_about
|
69
|
+
end
|
70
|
+
stuff_you_care_about
|
71
|
+
end
|
72
|
+
|
73
|
+
You may find that your trace is cluttered/dominated by calls to a small set of methods and classes that you don't care about. These options help you to exclude the worst offenders in a hurry:
|
74
|
+
|
75
|
+
<tt>:exclude_classes</tt> ::
|
76
|
+
Unroller won't show a trace for any calls to methods from the given class or classes (regular expressions).
|
77
|
+
Pass [/class_name/, :recursive] to also not show the trace for any calls made *from* those uninteresting methods.
|
78
|
+
<tt>:exclude_methods</tt> (not implementd) ::
|
79
|
+
Like <tt>:exclude_classes</tt> only you give it _method_ names instead of class names. Maybe will have the ability to specify a class name as well as a method name if you want to be more specific (f.e., <tt>ThatOneClass#format</tt> if you still want to include calls to other methods named <tt>format<tt>).
|
80
|
+
|
81
|
+
These options are for more general overall control:
|
82
|
+
|
83
|
+
<tt>:omit_depth_greater_than => depth</tt> ::
|
84
|
+
Use this to prevent from going more than <tt>depth<tt> levels deep if you find that the trace is cluttered by a bunch of really deep calls.
|
85
|
+
<tt>:stop_after_lines_output</tt> ::
|
86
|
+
If you don't know where to place the trace(false) and you just want it to stop on its own after so many lines, you could use this...
|
87
|
+
|
88
|
+
Examples:
|
89
|
+
|
90
|
+
Unroller::trace :stop_after_lines_output => 100, :exclude_classes => /Boring/ { ... }
|
91
|
+
|
92
|
+
Unroller::trace :if => proc{$tracing_enabled}, :omit_depth_greater_than => 9, :exclude_classes =>
|
93
|
+
[
|
94
|
+
/Benchmark/,
|
95
|
+
/Logger/,
|
96
|
+
/MonitorMixin/,
|
97
|
+
/Set/,
|
98
|
+
/HashWithIndifferentAccess/,
|
99
|
+
/ERB/,
|
100
|
+
/ActiveRecord/,
|
101
|
+
/SQLite3/,
|
102
|
+
/Class/,
|
103
|
+
/ActiveSupport/,
|
104
|
+
/ActiveSupport::Deprecation/,
|
105
|
+
/Pathname/
|
106
|
+
] do
|
107
|
+
...
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
===How did that method even get *called*?
|
113
|
+
|
114
|
+
Some of the time, you may be trying to answer the question "How did that method even get *called*?" This tool can't directly help answer that question unless you're willing to enable tracing really early and wade through pages and pages of output.
|
115
|
+
|
116
|
+
If you enable tracing from within that method, it will only show the calls that came *after* the mysterious call to the mystery function, not what came *before*, which is what you're interested in.
|
117
|
+
|
118
|
+
So... you could always <tt>p caller(0)</tt> within your mystery function and then put a trace around one of the calls leading up to this call, a little further up the stack...
|
119
|
+
|
120
|
+
|
121
|
+
===Usage in Rails
|
122
|
+
|
123
|
+
If you want to see all code that gets executed within a certain action in your controller, you can just add this snippet to your controller, request the page, and watch the console where you have Webrick running as pages and pages of ActionController/ActionRecord code go flying by...
|
124
|
+
|
125
|
+
around_filter do |controller, action|
|
126
|
+
Unroller::trace do
|
127
|
+
action.call
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
==Installation
|
132
|
+
|
133
|
+
sudo gem install unroller
|
134
|
+
|
135
|
+
==Status
|
136
|
+
|
137
|
+
It's pretty much fully functional, but may still have a couple rough edges.
|
138
|
+
|
139
|
+
Occasionally it has caused some segmentation faults and other weirdness for me while using it, but maybe I was just doing something stupid. Generally it behaves quite reliably and stably.
|
140
|
+
|
141
|
+
(Don't even *think* about leaving this in your production code!)
|
142
|
+
|
143
|
+
==About the name
|
144
|
+
|
145
|
+
I called it "Ruby Unroller" because it visually "unrolls" a stack trace for you (like a scroll?). (And it sounds cooler than "Ruby Script Execution Tracer".)
|
146
|
+
|
147
|
+
If there are method calls (even recursion), it will sort of "unroll" the method definition for you so you can see it.
|
148
|
+
|
149
|
+
It also alludes to some ideas from compiler design. Think {"method inlining"}[http://en.wikipedia.org/wiki/Inline_expansion] and {"loop unrolling"}[http://en.wikipedia.org/wiki/Loop_unrolling] in real-time. Not quite the same thing, but close enough.
|
150
|
+
|
151
|
+
It takes something that is actually very stack-based (the path that Ruby interpreter takes while executing your source code) and "flattens" it. It still tries to keep things appearing hierarchical (by means of the indent levels), but in essence it takes code from a bunch of different files and merges them into one "flat" output stream.
|
152
|
+
|
153
|
+
Phew!
|
154
|
+
|
155
|
+
Other name ideas are welcome!
|
156
|
+
* Unfolder?
|
157
|
+
* ...
|
158
|
+
|
159
|
+
==Similarity to debuggers/profilers / What it is not
|
160
|
+
|
161
|
+
It's sort of like a cross between a Ruby debugger and a profiler.
|
162
|
+
|
163
|
+
It's like a debugger because it lets you step through code execution one line at time (similar to the "step into" / "next" commands in most debuggers). It's not a debugger, though, because it doesn't let you inspect variables, set breakpoints, etc. It lets you *watch* the execution ... and that's it! So it's not as powerful as a real debugger, but it can be a lot *faster*! Rather than pressing F8 (or whatever the shortcut is for the 'step' command) 1000 times, you can just tell it "trace from here to here" and it will.
|
164
|
+
|
165
|
+
And like a profiler, it follows every method call, using Ruby's set_trace_func. Like a profiler, it can be helpful for diagnosing performance bottlenecks in your application -- at least those that involve excessive or unwanted method calls or recursion. It's not a profiler, though, because it doesn't _time_ the method calls like a 'real' profiler would.
|
166
|
+
|
167
|
+
It's also sort of like a call stack (caller(0)). But unlike the callstack you usually see, which only shows where you've been, this one doesn't show where you've been; it only shows where you _going_ -- or more accurately, where you *go* after you enable the tracing. So while it works equally well for tracing while the call stack is being unwound (returning from calls, which pops that call off the stack), the most common use of the tracer is to see what happens "within" a method call -- that method will in turn call _other_ methods, and the stack will grow temporarily -- but then it will unwind again and you'll get back to the method where you started the trace (hopefully -- eventually?), with a net change in stack size of 0.
|
168
|
+
|
169
|
+
(If you _do_ want to watch the trace as the call stack is unwound completely, be sure to pass <tt>:depth => caller(0).size</tt> to the Unroller so that it can start the indenting at an appropriate level...)
|
170
|
+
|
171
|
+
==Possibly related projects
|
172
|
+
|
173
|
+
* http://rubyforge.org/projects/dev-utils/ : Collects utilities that aid Ruby development, e.g. testing and debugging. Version 1.0 contains simple debug logging, tracing, and escaping to IRB.
|
174
|
+
* http://rubyforge.org/projects/ruby-uml/ : Generates uml diagrams by tracing the run of an application for analysation of an existing application and to provide support for refactorisations.
|
175
|
+
|
176
|
+
==To do
|
177
|
+
|
178
|
+
* Make a GUI interface that lets you quickly collapse/nodes nodes of the tree.
|
179
|
+
* It would be nice if we could see what arguments are being passed to each method. This would be technically difficult, but I wonder if it would be possible to just wait for a 'call' event and when you see one, wrap/alias_method_chain the given classname/id with a wrapper method that has variable *args and does whatever you want to do with the args before actually doing the *real* method call....
|
180
|
+
* :include_classes option in addition to :exclude_classes?
|
181
|
+
|
182
|
+
|
data/lib/unroller.rb
ADDED
@@ -0,0 +1,564 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'facets'
|
3
|
+
require 'facets/core/module/namespace'
|
4
|
+
require 'facets/core/kernel/with'
|
5
|
+
require 'facets/core/kernel/set_with'
|
6
|
+
gem 'qualitysmith_extensions'
|
7
|
+
require 'qualitysmith_extensions/object/send_if_not_nil'
|
8
|
+
require 'qualitysmith_extensions/kernel/trap_chain'
|
9
|
+
require 'qualitysmith_extensions/kernel/capture_output'
|
10
|
+
require 'qualitysmith_extensions/string/with_knowledge_of_color'
|
11
|
+
require 'qualitysmith_extensions/exception/inspect_with_backtrace'
|
12
|
+
require 'qualitysmith_extensions/symbol/match'
|
13
|
+
gem 'colored'
|
14
|
+
require 'colored'
|
15
|
+
|
16
|
+
# To disable color, uncomment this:
|
17
|
+
#class String
|
18
|
+
# def colorize(string, options = {})
|
19
|
+
# string
|
20
|
+
# end
|
21
|
+
#end
|
22
|
+
|
23
|
+
#
|
24
|
+
class Unroller
|
25
|
+
@@instance = nil
|
26
|
+
|
27
|
+
def self.trace(options = {}, &block)
|
28
|
+
@@instance = Unroller.new(options)
|
29
|
+
@@instance.trace &block
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_accessor :depth
|
33
|
+
attr_reader :tracing
|
34
|
+
|
35
|
+
def initialize(options = {})
|
36
|
+
# Defaults
|
37
|
+
@condition = Proc.new { true } # Only trace if this condition is true. Useful if the place where you put your trace {} statement gets called a lot and you only want it to actually trace for some of those calls.
|
38
|
+
@initial_depth = 0 # ("Call stack") depth to start at. Actually, you'll probably want this set considerably lower than the current call stack depth, so that the indentation isn't way off the screen.
|
39
|
+
@stop_after_lines_output = nil # If you don't know where to place the trace(false) and you just want it to stop on its own after so many lines, you could use this...
|
40
|
+
@omit_depth_greater_than = nil # Don't go too many levels deep. (This is *relative* to the starting depth, so whatever level you start at is considered depth "1".)
|
41
|
+
@exclude_classes = []
|
42
|
+
@strip_comments = true # :todo:
|
43
|
+
@use_full_path = false # :todo:
|
44
|
+
@screen_width = 150
|
45
|
+
@column_widths = [70]
|
46
|
+
@indent_step = ' ' + '|'.magenta + ' '
|
47
|
+
@column_separator = ' ' + '|'.yellow.bold + ' '
|
48
|
+
instance_variables.each do |v|
|
49
|
+
self.class.class_eval do
|
50
|
+
attr_accessor v.gsub!(/^@/, '')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Options
|
55
|
+
options[:condition] = options.delete(:if) if options.has_key?(:if)
|
56
|
+
options[:initial_depth] = options.delete(:depth) if options.has_key?(:depth)
|
57
|
+
options[:initial_depth] = caller(0).size if options[:initial_depth] == :use_call_stack_depth
|
58
|
+
if options.has_key?(:exclude_classes)
|
59
|
+
options[:exclude_classes] = [options[:exclude_classes]] if options[:exclude_classes].is_a?(Regexp)
|
60
|
+
raise ArgumentError if !options[:exclude_classes].is_a?(Array)
|
61
|
+
end
|
62
|
+
set_with(options)
|
63
|
+
|
64
|
+
# Private
|
65
|
+
@depth = @initial_depth
|
66
|
+
@output_line = ''
|
67
|
+
@column_counter = 0
|
68
|
+
@tracing = false
|
69
|
+
@files = {}
|
70
|
+
@lines_output = 0
|
71
|
+
@excluding_calls_made_within_unintersting_call = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.exclude(*args, &block)
|
75
|
+
@@instance.exclude(*args, &block)
|
76
|
+
end
|
77
|
+
def exclude(&block)
|
78
|
+
old_tracing = @tracing
|
79
|
+
(trace_off; puts 'Suspending tracing')
|
80
|
+
yield
|
81
|
+
(trace; puts 'Resuming tracing') if old_tracing
|
82
|
+
end
|
83
|
+
|
84
|
+
def trace(&block)
|
85
|
+
(puts 'Already tracing!'; return) if @tracing # This does not work. :fixme:
|
86
|
+
@tracing = true
|
87
|
+
|
88
|
+
|
89
|
+
if @condition.call
|
90
|
+
|
91
|
+
trap_chain("INT") { set_trace_func(nil) }
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
# (This is the meat of the library right here, so let's set it off with at least 5 blank lines.)
|
102
|
+
set_trace_func( proc do |event, file, line, id, binding, klass|
|
103
|
+
begin # begin/rescue block
|
104
|
+
@event, @file, @line, @id, @binding, @klass =
|
105
|
+
event, file, line, id, binding, klass
|
106
|
+
|
107
|
+
# Sometimes klass is false and id is nil. Not sure why, but that's the way it is.
|
108
|
+
#printf "- (event=%8s) (klass=%10s) (id=%10s) (%s:%-2d)\n", event, klass, id, file, line #if klass.to_s == 'false'
|
109
|
+
#puts 'false!!!!!!!'+klass.inspect if klass.to_s == 'false'
|
110
|
+
|
111
|
+
return if ['c-call', 'c-return'].include? event
|
112
|
+
#(puts 'exclude') if @excluding_calls_made_within_unintersting_call unless event == 'return' # Until we hit a return and can break out of this uninteresting call, we don't want to do *anything*.
|
113
|
+
#return if uninteresting_class?(klass.to_s) unless (klass == false)
|
114
|
+
|
115
|
+
if too_far?
|
116
|
+
puts "We've read #{@stop_after_lines_output} (@stop_after_lines_output) lines now. Turning off tracing..."
|
117
|
+
trace_off
|
118
|
+
return
|
119
|
+
end
|
120
|
+
|
121
|
+
case event
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
when 'call'
|
126
|
+
unless @excluding_calls_made_within_unintersting_call or calling_method_in_an_uninteresting_class?(klass.to_s) or too_deep?
|
127
|
+
# :todo: use # instead of :: if klass.constantize.instance_methods.include?(id)
|
128
|
+
column sprintf(' ' + '+'.cyan + ' calling'.cyan + ' ' + '%s::%s'.underline.cyan, klass, id), @column_widths[0]
|
129
|
+
newline
|
130
|
+
|
131
|
+
column code_for(file, line, '/'.magenta, :green), @column_widths[0]
|
132
|
+
file_column file, line
|
133
|
+
newline
|
134
|
+
|
135
|
+
@lines_output += 1
|
136
|
+
end
|
137
|
+
|
138
|
+
@depth += 1
|
139
|
+
|
140
|
+
|
141
|
+
|
142
|
+
when 'line'
|
143
|
+
unless @excluding_calls_made_within_unintersting_call or calling_method_in_an_uninteresting_class?(klass.to_s) or too_deep?
|
144
|
+
column code_for(file, line, ' ', :bold), @column_widths[0]
|
145
|
+
file_column file, line
|
146
|
+
newline
|
147
|
+
|
148
|
+
@lines_output += 1
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
when 'return'
|
154
|
+
puts "Warning: @depth < 0. You may wish to call trace with a :depth => depth value greater than #{@initial_depth}" if @depth < 0
|
155
|
+
@depth -= 1 unless @depth == 0
|
156
|
+
|
157
|
+
|
158
|
+
unless @excluding_calls_made_within_unintersting_call or calling_method_in_an_uninteresting_class?(klass.to_s) or too_deep?
|
159
|
+
code = code_for(file, line, '\\'.magenta, :green)
|
160
|
+
code = code_for(file, line, '\\'.magenta + ' (returning)'.green, :green) unless code =~ /return|end/
|
161
|
+
# I've seen some really weird statements listed as "return" statements.
|
162
|
+
# I'm not really sure *why* it thinks these are returns, but let's at least identify those lines for the user. Examples:
|
163
|
+
# * must_be_open!
|
164
|
+
# * @db = db
|
165
|
+
# * stmt = @statement_factory.new( self, sql )
|
166
|
+
# I think some of the time this happens it might be because people pass the wrong line number to eval (__LINE__ instead of __LINE__ + 1, for example), so the line number is just not accurate.
|
167
|
+
# But I don't know if that explains all such cases or not...
|
168
|
+
column code, @column_widths[0]
|
169
|
+
file_column file, line
|
170
|
+
newline
|
171
|
+
|
172
|
+
@lines_output += 1
|
173
|
+
end
|
174
|
+
|
175
|
+
# Did we just get out of an uninteresting call?? Are we back in interesting land again??
|
176
|
+
if @excluding_calls_made_within_unintersting_call and @excluding_calls_made_within_unintersting_call == @depth
|
177
|
+
if @excluding_calls_made_within_unintersting_call == @depth
|
178
|
+
puts "Yay, we're back in interesting land!"
|
179
|
+
@excluding_calls_made_within_unintersting_call = nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
when 'raise'
|
186
|
+
column "Raising an error (#{$!}) from #{klass}".red.bold, @column_widths[0]
|
187
|
+
newline
|
188
|
+
|
189
|
+
column code_for(file, line, ' ').red, @column_widths[0]
|
190
|
+
file_column file, line
|
191
|
+
newline
|
192
|
+
|
193
|
+
else
|
194
|
+
printf "- (%8s) %10s %10s (%s:%-2d)", event, klass, id, file, line
|
195
|
+
newline
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
rescue Exception => exception
|
200
|
+
puts exception.inspect
|
201
|
+
raise
|
202
|
+
end # begin/rescue block
|
203
|
+
end) # set_trace_func
|
204
|
+
|
205
|
+
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
|
210
|
+
|
211
|
+
|
212
|
+
end # if @condition.call
|
213
|
+
|
214
|
+
if block_given?
|
215
|
+
yield
|
216
|
+
end
|
217
|
+
|
218
|
+
ensure
|
219
|
+
trace_off if block_given?
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.trace_off
|
223
|
+
@@instance.trace_off
|
224
|
+
end
|
225
|
+
def trace_off
|
226
|
+
@tracing = false
|
227
|
+
set_trace_func(nil)
|
228
|
+
end
|
229
|
+
|
230
|
+
protected
|
231
|
+
#----------------------------------------------------------
|
232
|
+
# Helpers
|
233
|
+
|
234
|
+
def too_deep?
|
235
|
+
# The + 1 is because if they're still at the initial depth (@depth - @initial_depth == 0), we want it treated as "depth 1" (1-based, for humans).
|
236
|
+
@omit_depth_greater_than and (@depth - @initial_depth + 1 > @omit_depth_greater_than)
|
237
|
+
end
|
238
|
+
def too_far?
|
239
|
+
@stop_after_lines_output and (@lines_output > @stop_after_lines_output)
|
240
|
+
end
|
241
|
+
def calling_method_in_an_uninteresting_class?(class_name)
|
242
|
+
( @exclude_classes + [/#{self.class.name}/] ).any? do |item|
|
243
|
+
item = item.dup
|
244
|
+
if item.is_a?(Array)
|
245
|
+
# Expect item to be in format [/class_name/, :recursive, any other flags...]
|
246
|
+
regexp = item.shift
|
247
|
+
recursive = item.include?(:recursive)
|
248
|
+
else
|
249
|
+
regexp = item
|
250
|
+
recursive = false
|
251
|
+
end
|
252
|
+
|
253
|
+
returning(class_name =~ regexp) do |uninteresting|
|
254
|
+
if uninteresting && recursive && @excluding_calls_made_within_unintersting_call.nil?
|
255
|
+
puts "Turning tracing off until we get back to depth #{@depth} again because we're calling uninteresting #{class_name}:#{@id}"
|
256
|
+
@excluding_calls_made_within_unintersting_call = @depth
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
#----------------------------------------------------------
|
263
|
+
# Formatting stuff.
|
264
|
+
def indent
|
265
|
+
@indent_step*@depth
|
266
|
+
end
|
267
|
+
|
268
|
+
def remaining_width
|
269
|
+
@screen_width - @output_line.length_without_color
|
270
|
+
end
|
271
|
+
|
272
|
+
# +width+ is the minimum width for this column. It's also a maximum if +column_overflow+ is :chop_left or :chop_right
|
273
|
+
# +color+ is only needed if you plan on doing some chopping, because if you apply the color *before* the chopping, the color code might get chopped off.
|
274
|
+
def column(string, width = nil, column_overflow = :allow, color = nil)
|
275
|
+
raise ArgumentError if ![:allow, :chop_left, :chop_right].include?(column_overflow)
|
276
|
+
raise ArgumentError if width and !(width == :remainder or width.is_a?(Fixnum))
|
277
|
+
|
278
|
+
if @column_counter == 0
|
279
|
+
@output_line << indent
|
280
|
+
width -= indent.length_without_color if width
|
281
|
+
else
|
282
|
+
@output_line << @column_separator # So the columns won't be squashed up against each other
|
283
|
+
end
|
284
|
+
|
285
|
+
if width == :remainder
|
286
|
+
width = remaining_width()
|
287
|
+
end
|
288
|
+
|
289
|
+
if width
|
290
|
+
if column_overflow =~ /chop_/
|
291
|
+
#puts "width = #{width}"
|
292
|
+
string = string.code_unroller.make_it_fit(width, column_overflow)
|
293
|
+
end
|
294
|
+
string = string.code_unroller.make_it_fit(remaining_width) # Handles maximum width
|
295
|
+
|
296
|
+
string = string.ljust_without_color(width) # Handles minimum width
|
297
|
+
end
|
298
|
+
|
299
|
+
@output_line << string.send_if_not_nil(color)
|
300
|
+
|
301
|
+
@column_counter += 1
|
302
|
+
end # def column
|
303
|
+
|
304
|
+
def newline
|
305
|
+
unless @output_line.strip.length_without_color == 0 or @output_line == @last_line_printed
|
306
|
+
Kernel.print @output_line
|
307
|
+
Kernel.puts
|
308
|
+
@last_line_printed = @output_line
|
309
|
+
end
|
310
|
+
|
311
|
+
@output_line = ''
|
312
|
+
@column_counter = 0
|
313
|
+
end
|
314
|
+
|
315
|
+
def file_column(file, line)
|
316
|
+
column "#{file}:#{line}", :remainder, :chop_left, :magenta
|
317
|
+
end
|
318
|
+
|
319
|
+
#----------------------------------------------------------
|
320
|
+
|
321
|
+
def code_for(file, line, prefix, color = nil)
|
322
|
+
if file == '(eval)'
|
323
|
+
# Can't really read the source from the 'eval' file, unfortunately!
|
324
|
+
return ' ' + prefix + ' ' + file.send_if_not_nil(color) + ''
|
325
|
+
end
|
326
|
+
|
327
|
+
line -= 1 # Adjust for the fact that line arg is 0-based, readlines line is 1-based
|
328
|
+
begin
|
329
|
+
@files[file] ||= File.readlines(file)
|
330
|
+
line = [@files[file].size - 1, line].min
|
331
|
+
' ' + prefix + ' ' + @files[file][line].strip.send_if_not_nil(color) + ''
|
332
|
+
rescue Errno::ENOENT
|
333
|
+
$stderr.puts( message = "Error Could not open #{file}" )
|
334
|
+
message
|
335
|
+
rescue Exception => exception
|
336
|
+
puts "Error while getting code for #{file}:#{line}:"
|
337
|
+
puts exception.inspect
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
class String
|
344
|
+
namespace :code_unroller do
|
345
|
+
|
346
|
+
def make_it_fit(max_width, overflow = :chop_right)
|
347
|
+
with(string = self) do
|
348
|
+
if string.length_without_color > max_width # Wider than desired column width; Needs to be chopped.
|
349
|
+
unless max_width < 4 # Is there even enough room for it if it *is* chopped?
|
350
|
+
if overflow == :chop_left
|
351
|
+
#puts "making string (#{string.length_without_color}) fit within #{max_width}"
|
352
|
+
#puts "chopping '#{string}' at -(#{max_width} - 3) .. -1!"
|
353
|
+
chopped_part = string[-(max_width - 3) .. -1]
|
354
|
+
string.replace '...' + chopped_part
|
355
|
+
elsif overflow == :chop_right
|
356
|
+
chopped_part = string[0 .. (max_width - 3)]
|
357
|
+
string.replace chopped_part + '...'
|
358
|
+
end
|
359
|
+
else
|
360
|
+
string = ''
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
|
370
|
+
|
371
|
+
|
372
|
+
if $0 == __FILE__
|
373
|
+
puts '-----------------------------------------------------------'
|
374
|
+
puts 'Simple test'
|
375
|
+
def jump!
|
376
|
+
3.times do
|
377
|
+
'jump!'
|
378
|
+
end
|
379
|
+
end
|
380
|
+
Unroller::trace
|
381
|
+
jump!
|
382
|
+
Unroller::trace_off
|
383
|
+
|
384
|
+
puts '-----------------------------------------------------------'
|
385
|
+
puts "Testing that this doesn't trace anything (condition == false proc)"
|
386
|
+
$trace = false
|
387
|
+
Unroller::trace(:condition => proc { $trace }) do
|
388
|
+
jump!
|
389
|
+
end
|
390
|
+
|
391
|
+
puts '-----------------------------------------------------------'
|
392
|
+
puts "Testing that this doesn't trace the inner method (method2), but does trace method1 and method3 (exclude)"
|
393
|
+
def method1; end
|
394
|
+
def method2
|
395
|
+
'stuff!'
|
396
|
+
end
|
397
|
+
def method3; end
|
398
|
+
Unroller::trace do
|
399
|
+
method1
|
400
|
+
Unroller::exclude do
|
401
|
+
method2
|
402
|
+
end
|
403
|
+
method3
|
404
|
+
end
|
405
|
+
|
406
|
+
puts '-----------------------------------------------------------'
|
407
|
+
puts 'Test with block; very deep (test for over-wide columns)'
|
408
|
+
('a'..last='y').each do |method_name|
|
409
|
+
next_method_name = method_name.next unless method_name == last
|
410
|
+
eval <<-End, binding, __FILE__, __LINE__ + 1
|
411
|
+
def #{method_name}
|
412
|
+
#{next_method_name}
|
413
|
+
end
|
414
|
+
End
|
415
|
+
end
|
416
|
+
Unroller::trace(:depth => 5) do
|
417
|
+
a
|
418
|
+
end
|
419
|
+
|
420
|
+
puts '-----------------------------------------------------------'
|
421
|
+
puts 'Test watching a call stack unwind (only)'
|
422
|
+
('a'..last='y').each do |method_name|
|
423
|
+
next_method_name = method_name.next unless method_name == last
|
424
|
+
eval <<-End, binding, __FILE__, __LINE__ + 1
|
425
|
+
def #{method_name}
|
426
|
+
#{next_method_name}
|
427
|
+
#{'Unroller::trace(:depth => caller(0).size)' if method_name == last }
|
428
|
+
end
|
429
|
+
End
|
430
|
+
end
|
431
|
+
a
|
432
|
+
Unroller::trace_off
|
433
|
+
|
434
|
+
|
435
|
+
puts '-----------------------------------------------------------'
|
436
|
+
puts 'Testing :depth => :use_call_stack_depth'
|
437
|
+
def go_to_depth_and_call_1(depth, &block)
|
438
|
+
#puts caller(0).size
|
439
|
+
if caller(0).size == depth
|
440
|
+
puts 'calling a'
|
441
|
+
block.call
|
442
|
+
else
|
443
|
+
go_to_depth_and_call_2(depth, &block)
|
444
|
+
end
|
445
|
+
#puts caller(0).size
|
446
|
+
end
|
447
|
+
def go_to_depth_and_call_2(depth, &block)
|
448
|
+
#puts caller(0).size
|
449
|
+
if caller(0).size == depth
|
450
|
+
puts 'calling a'
|
451
|
+
block.call
|
452
|
+
else
|
453
|
+
go_to_depth_and_call_1(depth, &block)
|
454
|
+
end
|
455
|
+
#puts caller(0).size
|
456
|
+
end
|
457
|
+
('a'..last='c').each do |method_name|
|
458
|
+
next_method_name = method_name.next unless method_name == last
|
459
|
+
eval <<-End, binding, __FILE__, __LINE__ + 1
|
460
|
+
def #{method_name}
|
461
|
+
#{next_method_name}
|
462
|
+
end
|
463
|
+
End
|
464
|
+
end
|
465
|
+
go_to_depth_and_call_1(14) do
|
466
|
+
Unroller::trace(:depth => :use_call_stack_depth) do
|
467
|
+
a
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
puts '-----------------------------------------------------------'
|
472
|
+
puts 'Testing without :depth => :use_call_stack_depth (for comparison)'
|
473
|
+
go_to_depth_and_call_1(14) do
|
474
|
+
Unroller::trace() do
|
475
|
+
a
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
puts '-----------------------------------------------------------'
|
480
|
+
puts "Test omit_depth_greater_than 5: We shouldn't see the calls to f, g, ... because their depth > 5"
|
481
|
+
('a'..last='y').each do |method_name|
|
482
|
+
next_method_name = method_name.next unless method_name == last
|
483
|
+
eval <<-End, binding, __FILE__, __LINE__ + 1
|
484
|
+
def #{method_name}
|
485
|
+
#{next_method_name}
|
486
|
+
end
|
487
|
+
End
|
488
|
+
end
|
489
|
+
Unroller::trace(:omit_depth_greater_than => 5) do
|
490
|
+
a
|
491
|
+
end
|
492
|
+
|
493
|
+
puts '-----------------------------------------------------------'
|
494
|
+
puts 'Test with long filename (make sure it chops it correctly)'
|
495
|
+
File.open(filename = '_code_unroller_test_with_really_really_really_really_really_really_really_really_really_long_filename.rb', 'w') do |file|
|
496
|
+
file.puts "
|
497
|
+
def sit!
|
498
|
+
jump!
|
499
|
+
end
|
500
|
+
"
|
501
|
+
end
|
502
|
+
load filename
|
503
|
+
Unroller::trace(:depth => 5) do
|
504
|
+
sit!
|
505
|
+
end
|
506
|
+
require 'fileutils'
|
507
|
+
FileUtils.rm filename
|
508
|
+
|
509
|
+
puts '-----------------------------------------------------------'
|
510
|
+
puts 'Test @stop_after_lines_output'
|
511
|
+
('a'..last='h').each do |method_name|
|
512
|
+
next_method_name = method_name.next unless method_name == last
|
513
|
+
eval <<-End, binding, __FILE__, __LINE__ + 1
|
514
|
+
def #{method_name}
|
515
|
+
#{next_method_name}
|
516
|
+
end
|
517
|
+
End
|
518
|
+
end
|
519
|
+
Unroller::trace(:stop_after_lines_output => 20) do
|
520
|
+
a
|
521
|
+
end
|
522
|
+
|
523
|
+
puts '-----------------------------------------------------------'
|
524
|
+
puts 'Test @exclude_classes'
|
525
|
+
puts 'Should only see calls to Interesting::...'
|
526
|
+
class Interesting # :nodoc: all
|
527
|
+
def self.method
|
528
|
+
'...'
|
529
|
+
end
|
530
|
+
def method
|
531
|
+
'...'
|
532
|
+
end
|
533
|
+
end
|
534
|
+
module Uninteresting # :nodoc: all
|
535
|
+
class ClassThatCluttersUpOnesTraces
|
536
|
+
('a'..last='h').each do |method_name|
|
537
|
+
next_method_name = method_name.next unless method_name == last
|
538
|
+
eval <<-End, binding, __FILE__, __LINE__ + 1
|
539
|
+
def #{method_name}
|
540
|
+
#{next_method_name}
|
541
|
+
#{'Interesting::method' if method_name == last }
|
542
|
+
#{'Interesting.new.method' if method_name == last }
|
543
|
+
end
|
544
|
+
End
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
def create_an_instance_of_UninterestingClassThatCluttersUpOnesTraces
|
549
|
+
Uninteresting::ClassThatCluttersUpOnesTraces.new.a
|
550
|
+
end
|
551
|
+
Unroller::trace(:exclude_classes => /Uninteresting::ClassThatCluttersUpOnesTraces/) do
|
552
|
+
create_an_instance_of_UninterestingClassThatCluttersUpOnesTraces
|
553
|
+
end
|
554
|
+
|
555
|
+
puts '-----------------------------------------------------------'
|
556
|
+
puts 'Now let\'s be recursive! We should *not* see any calls to Interesting::* this time. But after we return from it, we should see the call to jump!'
|
557
|
+
Unroller::trace(:exclude_classes => [[/Uninteresting/, :recursive]]) do
|
558
|
+
create_an_instance_of_UninterestingClassThatCluttersUpOnesTraces
|
559
|
+
jump!
|
560
|
+
end
|
561
|
+
|
562
|
+
|
563
|
+
|
564
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.2
|
3
|
+
specification_version: 1
|
4
|
+
name: unroller
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.3
|
7
|
+
date: 2007-04-13 00:00:00 -07:00
|
8
|
+
summary: A tool for generating human-readable "execution traces"
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email:
|
12
|
+
homepage: http://rdoc.qualitysmith.com/unroller
|
13
|
+
rubyforge_project: unroller
|
14
|
+
description: A debugging/tracing tool for Ruby. While it is enabled, it will watch every Ruby statement and method call that gets executed and will display the source code that is being executed in real time on your screen.
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Tyler Rick
|
31
|
+
files:
|
32
|
+
- lib/unroller.rb
|
33
|
+
- lib/unroller/unroller.rb
|
34
|
+
- Readme
|
35
|
+
test_files: []
|
36
|
+
|
37
|
+
rdoc_options:
|
38
|
+
- --title
|
39
|
+
- unroller
|
40
|
+
- --main
|
41
|
+
- Readme
|
42
|
+
- --line-numbers
|
43
|
+
extra_rdoc_files:
|
44
|
+
- Readme
|
45
|
+
executables: []
|
46
|
+
|
47
|
+
extensions: []
|
48
|
+
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
dependencies:
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: facets
|
54
|
+
version_requirement:
|
55
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 1.8.54
|
60
|
+
version:
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: qualitysmith_extensions
|
63
|
+
version_requirement:
|
64
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.0.13
|
69
|
+
version:
|