kefka 0.0.4 → 0.0.5
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.
- 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({
|