kefka 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/app.rb +13 -9
- data/kefka.gemspec +3 -2
- data/lib/kefka.rb +117 -45
- data/public/javascripts/app.js +127 -47
- data/public/javascripts/underscore.js +1059 -0
- data/public/stylesheets/application.css +63 -1
- data/spec/kekfa_spec.rb +3 -11
- data/views/index.erb +4 -6
- metadata +22 -21
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.5
|
data/app.rb
CHANGED
@@ -18,25 +18,29 @@ get '/callgraph' do
|
|
18
18
|
@tracer.trace(file_path)
|
19
19
|
|
20
20
|
# input code
|
21
|
-
input = CodeRay.scan(@tracer.code, :ruby).div(:line_numbers => :table)
|
21
|
+
#input = CodeRay.scan(@tracer.code, :ruby).div(:line_numbers => :table)
|
22
22
|
|
23
|
-
|
23
|
+
method_graph = @tracer.method_graph
|
24
24
|
|
25
25
|
# output call graph using dot if graphviz is installed
|
26
|
-
if is_graphviz_installed = system("which dot")
|
27
|
-
|
28
|
-
end
|
26
|
+
#if is_graphviz_installed = system("which dot")
|
27
|
+
# method_graph.write_to_graphic_file("png", "#{File.expand_path(File.dirname(__FILE__))}/public/graph")
|
28
|
+
#end
|
29
29
|
|
30
30
|
# html code graph
|
31
|
-
|
31
|
+
method_graph.vertices.each { |method| method.format = :html }
|
32
|
+
|
33
|
+
# line graph
|
34
|
+
line_graph = @tracer.line_graph
|
32
35
|
|
33
36
|
# locals
|
34
37
|
locals = @tracer.local_values
|
35
38
|
|
36
39
|
{
|
37
|
-
|
38
|
-
|
39
|
-
:
|
40
|
+
#:input => input,
|
41
|
+
#:is_graphviz_installed => is_graphviz_installed,
|
42
|
+
:vertices => method_graph.vertices,
|
43
|
+
:edges => line_graph.edges,
|
40
44
|
:locals => locals
|
41
45
|
}.to_json
|
42
46
|
end
|
data/kefka.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "kefka"
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Reginald Tan"]
|
12
|
-
s.date = "2012-08-
|
12
|
+
s.date = "2012-08-27"
|
13
13
|
s.description = " It traces the execution path of a program and displays the source code of each method call in the callgraph "
|
14
14
|
s.email = "redge.tan@gmail.com"
|
15
15
|
s.executables = ["kefka"]
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
|
|
33
33
|
"lib/kefka.rb",
|
34
34
|
"public/javascripts/app.js",
|
35
35
|
"public/javascripts/jquery-1.7.2.js",
|
36
|
+
"public/javascripts/underscore.js",
|
36
37
|
"public/stylesheets/application.css",
|
37
38
|
"spec/fixture/sample_a.rb",
|
38
39
|
"spec/kekfa_spec.rb",
|
data/lib/kefka.rb
CHANGED
@@ -9,10 +9,42 @@ require 'logger'
|
|
9
9
|
|
10
10
|
class Kefka
|
11
11
|
|
12
|
+
class Call
|
13
|
+
attr_accessor :method, :line
|
14
|
+
|
15
|
+
def initialize(options={})
|
16
|
+
@method = options[:method]
|
17
|
+
@line = options[:line]
|
18
|
+
end
|
19
|
+
|
20
|
+
def file
|
21
|
+
@method.file
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"#{@method.file}:#{@line} in #{@method}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def key
|
29
|
+
[@method.id,@method.file,@line].join(":")
|
30
|
+
end
|
31
|
+
|
32
|
+
def eql?(other)
|
33
|
+
self.key == other.key
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :== :eql?
|
37
|
+
|
38
|
+
def hash
|
39
|
+
[@method.file,@line].hash
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
12
44
|
class Method
|
13
45
|
|
14
46
|
attr_reader :classname, :id, :file, :line
|
15
|
-
attr_accessor :source, :format
|
47
|
+
attr_accessor :source, :format, :depth
|
16
48
|
|
17
49
|
def initialize(options={})
|
18
50
|
raise ArgumentError, "missing file + line" unless options[:file] && options[:line]
|
@@ -32,7 +64,7 @@ class Kefka
|
|
32
64
|
end
|
33
65
|
|
34
66
|
def key
|
35
|
-
source_location ? source_location.join(":") : nil
|
67
|
+
source_location ? [id,source_location].flatten.join(":") : nil
|
36
68
|
end
|
37
69
|
|
38
70
|
def contains?(file, line)
|
@@ -80,44 +112,95 @@ class Kefka
|
|
80
112
|
def to_json(*a)
|
81
113
|
{
|
82
114
|
:classname => @classname,
|
83
|
-
:id => @id,
|
115
|
+
:id => @id.to_s,
|
84
116
|
:file => @file,
|
85
117
|
:line => @start_line,
|
86
118
|
:end_line => end_line,
|
119
|
+
:depth => depth,
|
87
120
|
:source => formatted_source
|
88
121
|
}.to_json(*a)
|
89
122
|
end
|
90
123
|
end
|
91
124
|
|
125
|
+
# shows which method called which method
|
92
126
|
class MethodGraph
|
93
127
|
extend Forwardable
|
94
|
-
def_delegators :@graph, :vertices, :edges, :
|
95
|
-
:write_to_graphic_file
|
128
|
+
def_delegators :@graph, :vertices, :edges, :write_to_graphic_file
|
96
129
|
|
97
130
|
def initialize
|
98
131
|
@graph = RGL::DirectedAdjacencyGraph.new
|
99
132
|
end
|
100
133
|
|
101
|
-
def
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
134
|
+
def assign_depth(u,v)
|
135
|
+
u.depth = 0 unless u.depth
|
136
|
+
v.depth = u.depth + 1
|
137
|
+
end
|
138
|
+
|
139
|
+
def add_edge(u,v)
|
140
|
+
raise ArgumentError unless Method === u && Method === v
|
141
|
+
assign_depth(u,v)
|
142
|
+
@graph.add_edge(u,v)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# shows which line in a method called which method
|
147
|
+
class LineGraph
|
148
|
+
extend Forwardable
|
149
|
+
def_delegators :@graph, :vertices, :write_to_graphic_file
|
150
|
+
|
151
|
+
def initialize
|
152
|
+
@graph = RGL::DirectedAdjacencyGraph.new
|
153
|
+
end
|
154
|
+
|
155
|
+
def add_edge(u,v)
|
156
|
+
raise ArgumentError unless Call === u && Method === v
|
157
|
+
@graph.add_edge(u,v)
|
158
|
+
end
|
159
|
+
|
160
|
+
def edges
|
161
|
+
@graph.edges.map { |edge|
|
162
|
+
{
|
163
|
+
:source => edge.source.key,
|
164
|
+
:target => edge.target.key
|
109
165
|
}
|
110
|
-
}
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
module LocalsHelper
|
171
|
+
# @target - binding
|
172
|
+
def self.get_locals(target, line_source)
|
173
|
+
scope_locals = target.eval("local_variables")
|
174
|
+
scope_locals.map! { |local| local.to_s }
|
175
|
+
|
176
|
+
tokens = Ripper.lex(line_source)
|
177
|
+
|
178
|
+
lvar, ivar, cvar = [], [], []
|
179
|
+
|
180
|
+
tokens.each { |token|
|
181
|
+
type = token[1]
|
182
|
+
|
183
|
+
case type
|
184
|
+
when :on_ident; lvar << token[2] if scope_locals.include? token[2]
|
185
|
+
when :on_ivar; ivar << token[2]
|
186
|
+
when :on_cvar; cvar << token[2]
|
187
|
+
else # do nothing
|
188
|
+
end
|
189
|
+
}
|
190
|
+
|
191
|
+
[lvar,ivar,cvar].flatten
|
111
192
|
end
|
112
193
|
end
|
113
194
|
|
114
195
|
class Tracer
|
115
196
|
|
116
|
-
attr_reader :local_values, :logger, :callstack,
|
197
|
+
attr_reader :local_values, :logger, :callstack,
|
198
|
+
:method_graph, :line_graph,
|
117
199
|
:code
|
118
200
|
|
119
201
|
def initialize(log_level = Logger::INFO)
|
120
202
|
@method_graph = MethodGraph.new
|
203
|
+
@line_graph = LineGraph.new
|
121
204
|
@callstack = []
|
122
205
|
@local_values = {}
|
123
206
|
@event_disable = []
|
@@ -130,7 +213,8 @@ class Kefka
|
|
130
213
|
|
131
214
|
thread = Thread.new {
|
132
215
|
@code = file.read
|
133
|
-
|
216
|
+
toplevel_method = create_toplevel_method(file_path)
|
217
|
+
@callstack << Call.new(:method => toplevel_method, :line => 1)
|
134
218
|
eval(@code, TOPLEVEL_BINDING, file.path, 1)
|
135
219
|
}
|
136
220
|
|
@@ -157,7 +241,8 @@ class Kefka
|
|
157
241
|
when "class"
|
158
242
|
@event_disable << true
|
159
243
|
when "call"
|
160
|
-
|
244
|
+
called_from = @callstack.last
|
245
|
+
caller_method = called_from.method
|
161
246
|
|
162
247
|
method = Method.new(
|
163
248
|
:classname => classname,
|
@@ -166,11 +251,10 @@ class Kefka
|
|
166
251
|
:line => line
|
167
252
|
)
|
168
253
|
|
169
|
-
|
170
|
-
|
171
|
-
end
|
254
|
+
@line_graph.add_edge(called_from.dup,method)
|
255
|
+
@method_graph.add_edge(caller_method,method)
|
172
256
|
|
173
|
-
@callstack << method
|
257
|
+
@callstack << Call.new(:method => method, :line => line)
|
174
258
|
when "line"
|
175
259
|
# skip variables that should not be tracked
|
176
260
|
# 1. anything in current lib (i.e __FILE__)
|
@@ -180,6 +264,10 @@ class Kefka
|
|
180
264
|
# vars in that program
|
181
265
|
#current_method = @callstack.last
|
182
266
|
|
267
|
+
# update callstack last method's line
|
268
|
+
if call = @callstack.last
|
269
|
+
call.line = line
|
270
|
+
end
|
183
271
|
# given current file & line, determine what method I am in
|
184
272
|
|
185
273
|
method = @method_graph.vertices
|
@@ -190,7 +278,7 @@ class Kefka
|
|
190
278
|
if method
|
191
279
|
line_source = method.source_at_line(line)
|
192
280
|
|
193
|
-
iseq_key = [file,
|
281
|
+
iseq_key = [id,file,line].join(":")
|
194
282
|
@local_values[iseq_key] = get_values_of_locals_from_binding(binding, line_source)
|
195
283
|
end
|
196
284
|
when "return"
|
@@ -204,31 +292,15 @@ class Kefka
|
|
204
292
|
end
|
205
293
|
|
206
294
|
def get_values_of_locals_from_binding(target, line_source)
|
207
|
-
locals = get_locals(target,line_source)
|
295
|
+
locals = LocalsHelper.get_locals(target,line_source)
|
208
296
|
locals.inject({}) do |result,l|
|
209
297
|
val = target.eval(l.to_s)
|
210
298
|
val = deep_copy(val)
|
211
|
-
result.merge!({ l => val })
|
299
|
+
result.merge!({ l => val.to_s })
|
212
300
|
result
|
213
301
|
end
|
214
302
|
end
|
215
303
|
|
216
|
-
def get_locals(target, line_source)
|
217
|
-
scope_locals = target.eval("local_variables")
|
218
|
-
scope_locals.map! { |local| local.to_s }
|
219
|
-
|
220
|
-
tokens = Ripper.lex(line_source)
|
221
|
-
|
222
|
-
tokens.select! { |token|
|
223
|
-
type = token[1]
|
224
|
-
type == :on_ident || type == :on_ivar || type == :on_cvar
|
225
|
-
}
|
226
|
-
|
227
|
-
possible_line_variables = tokens.map { |token| token[2] }
|
228
|
-
line_variables = scope_locals & possible_line_variables
|
229
|
-
line_variables
|
230
|
-
end
|
231
|
-
|
232
304
|
def deep_copy(val)
|
233
305
|
Marshal.load(Marshal.dump(val))
|
234
306
|
rescue TypeError
|
@@ -236,14 +308,14 @@ class Kefka
|
|
236
308
|
end
|
237
309
|
|
238
310
|
|
239
|
-
def
|
311
|
+
def create_toplevel_method(file_path)
|
240
312
|
method = Method.new(
|
241
|
-
:classname => "
|
242
|
-
:id =>
|
243
|
-
:file =>
|
313
|
+
:classname => "Toplevel",
|
314
|
+
:id => nil,
|
315
|
+
:file => file_path,
|
244
316
|
:line => 1
|
245
317
|
)
|
246
|
-
method.source = File.readlines(
|
318
|
+
method.source = File.readlines(file_path).join
|
247
319
|
return method
|
248
320
|
end
|
249
321
|
|
data/public/javascripts/app.js
CHANGED
@@ -2,13 +2,6 @@ var jqSelectorEscape = function(text) {
|
|
2
2
|
return text.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, "\\$&");
|
3
3
|
};
|
4
4
|
|
5
|
-
var outputTrace = function(data) {
|
6
|
-
displayInput(data.input);
|
7
|
-
displayGraphiz(data.is_graphviz_installed);
|
8
|
-
createCodeBubbles(data.graph);
|
9
|
-
displayLocals(data.locals);
|
10
|
-
}
|
11
|
-
|
12
5
|
var displayInput = function(input) {
|
13
6
|
$("div#input").last().append(input);
|
14
7
|
};
|
@@ -21,58 +14,137 @@ var displayGraphiz = function(is_graphviz_installed) {
|
|
21
14
|
}
|
22
15
|
};
|
23
16
|
|
24
|
-
var
|
25
|
-
|
17
|
+
var setupBubble = function(bubble, method) {
|
18
|
+
var $bubble, xPos, key, header;
|
26
19
|
|
27
|
-
|
28
|
-
var bubbleDiv, $bubble, $code,
|
29
|
-
key, header,
|
30
|
-
lineCount, column;
|
20
|
+
$bubble = bubble;
|
31
21
|
|
32
|
-
|
22
|
+
$bubble.append(method.source);
|
33
23
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
$(bubbleDiv).appendTo("#codeGraph");
|
24
|
+
// set id for bubble table
|
25
|
+
key = method.id + ":" + method.file + ":" + method.line;
|
26
|
+
$bubble.attr("id",key);
|
38
27
|
|
39
|
-
|
40
|
-
|
28
|
+
// set header of bubble div
|
29
|
+
header = method.file + ":" + method.line;
|
30
|
+
$bubble.prepend("<pre><span class='methodHeader'>" + header + "</span></pre>");
|
41
31
|
|
42
|
-
|
43
|
-
|
44
|
-
$bubble.attr("id",key);
|
32
|
+
// set column position
|
33
|
+
xPos = method.depth * 200;
|
45
34
|
|
46
|
-
|
47
|
-
|
48
|
-
|
35
|
+
// position bubble table
|
36
|
+
$bubble.css("position", "relative")
|
37
|
+
.css("left", xPos);
|
49
38
|
|
50
|
-
|
51
|
-
//$callerBubble = $("div.bubble#" + jqSelectorEscape(callerKey));
|
52
|
-
//xPos = $callerBubble.position().left + 200;
|
39
|
+
// hide bubbles in deeper levels
|
53
40
|
|
54
|
-
|
55
|
-
$bubble.
|
56
|
-
|
41
|
+
if (method.depth != 0) {
|
42
|
+
$bubble.hide();
|
43
|
+
}
|
44
|
+
};
|
57
45
|
|
58
|
-
|
59
|
-
|
46
|
+
var addExpandColumn = function(bubble,method) {
|
47
|
+
var column, $code, $bubble;
|
60
48
|
|
61
|
-
|
62
|
-
column += "<pre>";
|
49
|
+
$bubble = bubble;
|
63
50
|
|
64
|
-
|
65
|
-
|
66
|
-
column += "<span id='line" + j + "'></span>\n";
|
67
|
-
}
|
51
|
+
column = "<td class='expand'>";
|
52
|
+
column += "<pre>";
|
68
53
|
|
69
|
-
|
70
|
-
|
54
|
+
for (var j = method.line; j <= method.end_line ; j++)
|
55
|
+
{
|
56
|
+
column += "<span id='line" + j + "'></span>\n";
|
57
|
+
}
|
58
|
+
|
59
|
+
column += "</pre>";
|
60
|
+
column += "</td>";
|
61
|
+
|
62
|
+
$code = $bubble.find("td.code");
|
63
|
+
$code.after(column);
|
64
|
+
};
|
65
|
+
|
66
|
+
var addLocalsColumn = function(bubble,method) {
|
67
|
+
var column, $expand, $bubble;
|
68
|
+
|
69
|
+
$bubble = bubble;
|
71
70
|
|
72
|
-
|
73
|
-
|
71
|
+
column = "<td class='locals'>";
|
72
|
+
column += "<pre>";
|
74
73
|
|
74
|
+
for (var j = method.line; j <= method.end_line; j++)
|
75
|
+
{
|
76
|
+
column += "<span id='line" + j + "'></span>\n";
|
75
77
|
}
|
78
|
+
|
79
|
+
column += "</pre>";
|
80
|
+
column += "</td>";
|
81
|
+
|
82
|
+
$expand = $bubble.find("td.expand");
|
83
|
+
$expand.after(column);
|
84
|
+
};
|
85
|
+
|
86
|
+
var setupCall = function(call, edges) {
|
87
|
+
var keyTokens, methodName, file, line,
|
88
|
+
$bubbles, $line;
|
89
|
+
|
90
|
+
keyTokens = call.split(":");
|
91
|
+
methodName = keyTokens[0];
|
92
|
+
file = keyTokens[1];
|
93
|
+
line = keyTokens[2];
|
94
|
+
|
95
|
+
$bubbles = $("div.bubble").filter(function(){
|
96
|
+
return this.id.match(methodName) && this.id.match(file);
|
97
|
+
});
|
98
|
+
|
99
|
+
$line = $bubbles.find("td.expand span#line" + line).first();
|
100
|
+
$line.data("key",call);
|
101
|
+
|
102
|
+
$line.html("<div class='expand'>+</div>");
|
103
|
+
$line.find("div.expand").first().click(function() {
|
104
|
+
return function(that){
|
105
|
+
_.chain(edges)
|
106
|
+
.filter( function(edge){ return edge.source == $(that).parent().data("key"); })
|
107
|
+
.map( function(edge){ console.log(edge);return edge.target })
|
108
|
+
.each( function(target){
|
109
|
+
$("#codeGraph .bubble#" + jqSelectorEscape(target))
|
110
|
+
.first()
|
111
|
+
.toggle();
|
112
|
+
});
|
113
|
+
}(this);
|
114
|
+
});
|
115
|
+
|
116
|
+
};
|
117
|
+
|
118
|
+
var createCodeBubbles = function(vertices,edges) {
|
119
|
+
console.log(vertices);
|
120
|
+
console.log(edges);
|
121
|
+
|
122
|
+
var xPos = 0;
|
123
|
+
var bubbleDiv, $bubble;
|
124
|
+
|
125
|
+
var methods = vertices;
|
126
|
+
|
127
|
+
_.each(methods, function(method){
|
128
|
+
bubbleDiv = "<div class='bubble'></div>";
|
129
|
+
$(bubbleDiv).appendTo("#codeGraph");
|
130
|
+
|
131
|
+
$bubble = $("#codeGraph .bubble").last();
|
132
|
+
|
133
|
+
setupBubble($bubble, method);
|
134
|
+
addExpandColumn($bubble,method);
|
135
|
+
addLocalsColumn($bubble,method);
|
136
|
+
});
|
137
|
+
|
138
|
+
var calls = _.map(edges, function(edge) { return edge.source });
|
139
|
+
|
140
|
+
// for each call, get the source (key -> method:line)
|
141
|
+
// add an 'expand' button on the source
|
142
|
+
// on button, add click handler that will
|
143
|
+
|
144
|
+
_.each(calls, function(call){
|
145
|
+
setupCall(call, edges);
|
146
|
+
});
|
147
|
+
|
76
148
|
};
|
77
149
|
|
78
150
|
var displayLocals = function(locals) {
|
@@ -93,11 +165,12 @@ var displayLocals = function(locals) {
|
|
93
165
|
}
|
94
166
|
|
95
167
|
keys = key.split(":");
|
96
|
-
|
97
|
-
|
168
|
+
methodName = keys[0];
|
169
|
+
file = keys[1];
|
170
|
+
line = keys[2];
|
98
171
|
|
99
172
|
$bubbles = $("div.bubble").filter(function(){
|
100
|
-
return this.id.match(file);
|
173
|
+
return this.id.match(methodName) && this.id.match(file);
|
101
174
|
});
|
102
175
|
|
103
176
|
$line = $bubbles.find("td.locals span#line" + line).first();
|
@@ -105,6 +178,13 @@ var displayLocals = function(locals) {
|
|
105
178
|
}
|
106
179
|
};
|
107
180
|
|
181
|
+
var outputTrace = function(data) {
|
182
|
+
//displayInput(data.input);
|
183
|
+
//displayGraphiz(data.is_graphviz_installed);
|
184
|
+
createCodeBubbles(data.vertices,data.edges);
|
185
|
+
displayLocals(data.locals);
|
186
|
+
}
|
187
|
+
|
108
188
|
$(document).ready(function(){
|
109
189
|
|
110
190
|
$.ajax({
|