callgrapher 0.0.1
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/LICENSE-CC-BY-SA +359 -0
- data/LICENSE-GPLv3 +674 -0
- data/README.md +41 -0
- data/lib/callgrapher.rb +87 -0
- data/test/test.rb +150 -0
- metadata +52 -0
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
CallGrapher
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
Produce class dependency graphs by tracing method calls while code runs. Outputs
|
|
5
|
+
a .dot file for use with [GraphViz](http://www.graphviz.org/).
|
|
6
|
+
|
|
7
|
+
Requirements
|
|
8
|
+
-----------
|
|
9
|
+
Ruby 1.9 or later & Graphviz on your $PATH
|
|
10
|
+
|
|
11
|
+
Usage
|
|
12
|
+
-----
|
|
13
|
+
Require 'callgrapher' and call trace_class_dependencies, passing in a block
|
|
14
|
+
containing the code you want to graph. A Hash of sets will be returned, ready
|
|
15
|
+
for proccesing with make_class_graph. Calling make_class_graph uses the `dot`
|
|
16
|
+
command to generate a graph & save it to /tmp/graph.png.
|
|
17
|
+
|
|
18
|
+
Known Limitations
|
|
19
|
+
-----------------
|
|
20
|
+
It ignores dependencies created by C function calls as the filename doesn't seem
|
|
21
|
+
to reported correctly in these cases.
|
|
22
|
+
|
|
23
|
+
It does not pick up dependencies created by calling methods defined by the
|
|
24
|
+
attr_reader, attr_writer and attr_accessor methods. (see [this][bug] bug report)
|
|
25
|
+
). There's probably various other meta-programming techniques that will interact
|
|
26
|
+
poorly with this as well.
|
|
27
|
+
|
|
28
|
+
[bug]: http://bugs.ruby-lang.org/issues/4583
|
|
29
|
+
|
|
30
|
+
License
|
|
31
|
+
-------
|
|
32
|
+
Copyright (c) 2010-2012 Luke Andrew
|
|
33
|
+
|
|
34
|
+
All code is distributed under the terms of the [GPL version 3][gpl]. All other
|
|
35
|
+
copyrightable material is distributed under the terms of the [Creative Commons
|
|
36
|
+
Attribution-ShareAlike 3.0 Unported License][cc]
|
|
37
|
+
|
|
38
|
+
Copies of these licenses are available in the files `LICENSE-GPLv3` and `LICENSE-CC-BY-SA`.
|
|
39
|
+
|
|
40
|
+
[gpl]: http://www.gnu.org/licenses/gpl.html
|
|
41
|
+
[cc]: http://creativecommons.org/licenses/by-sa/3.0/
|
data/lib/callgrapher.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright (C) 2012 Luke Andrew
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
# This is the main executable for the whysynth-controller project. It also
|
|
17
|
+
# contains most of the code that relies on external libraries.
|
|
18
|
+
|
|
19
|
+
require 'set'
|
|
20
|
+
|
|
21
|
+
module CallGrapher
|
|
22
|
+
|
|
23
|
+
# @param [Array<String>] file_whitelist
|
|
24
|
+
# Only include classes defined in these files.
|
|
25
|
+
# @param [Integer] namespace_depth
|
|
26
|
+
# The number of namespaces to report. For example, a class name of
|
|
27
|
+
# Foo::Bar::Baz, when processed with namespace_depth == 1, would be reported
|
|
28
|
+
# as just Foo. With namespace_depth == 2, as Foo::Bar, and so on. A
|
|
29
|
+
# namespace_depth of 0 indicates that the entire class name should be
|
|
30
|
+
# reported.
|
|
31
|
+
def self.trace_class_dependencies(namespace_depth = 0,
|
|
32
|
+
file_whitelist = nil,
|
|
33
|
+
class_blacklist = nil)
|
|
34
|
+
callstack = []
|
|
35
|
+
classgraph = Hash.new{ |hash, key| hash[key] = Set.new }
|
|
36
|
+
|
|
37
|
+
set_trace_func proc{ |event, file, line, id, binding, klass|
|
|
38
|
+
next if file_whitelist && !file_whitelist.include?(file)
|
|
39
|
+
next if class_blacklist && class_blacklist.include?(klass.to_s)
|
|
40
|
+
case event
|
|
41
|
+
when 'call'
|
|
42
|
+
caller = callstack[-1]
|
|
43
|
+
|
|
44
|
+
klass =
|
|
45
|
+
if namespace_depth > 0
|
|
46
|
+
klass.name.split('::').first(namespace_depth).join('::')
|
|
47
|
+
else
|
|
48
|
+
klass.name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
classgraph[caller].add klass if caller && caller != klass
|
|
52
|
+
callstack.push klass
|
|
53
|
+
when 'return'
|
|
54
|
+
callstack.pop
|
|
55
|
+
end
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
yield
|
|
59
|
+
|
|
60
|
+
set_trace_func nil
|
|
61
|
+
classgraph
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param [Hash<Class, Set>] call_graph
|
|
65
|
+
# the graph to transform into a Graphviz digraph
|
|
66
|
+
def self.make_graphviz_graph(call_graph)
|
|
67
|
+
|
|
68
|
+
graph = ''
|
|
69
|
+
graph << 'digraph callgraph {'
|
|
70
|
+
|
|
71
|
+
call_graph.each do |klass, dependencies|
|
|
72
|
+
graph << "\"#{klass}\" [shape=box];\n"
|
|
73
|
+
dependencies.each do |dependency|
|
|
74
|
+
graph << "\"#{dependency}\" [shape=box];\n"
|
|
75
|
+
graph << "\"#{klass}\" -> \"#{dependency}\";\n"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
graph << '}'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param graph a string containing a dot format digraph
|
|
83
|
+
def self.make_graph(graph, path)
|
|
84
|
+
IO.popen("dot -Tpng -o#{path}", 'w') { |out| out.write graph }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
data/test/test.rb
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright (C) 2012 Luke Andrew
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
# This is the main executable for the whysynth-controller project. It also
|
|
17
|
+
# contains most of the code that relies on external libraries.
|
|
18
|
+
|
|
19
|
+
require 'callgrapher'
|
|
20
|
+
require 'minitest/autorun'
|
|
21
|
+
|
|
22
|
+
class Class1
|
|
23
|
+
def initialize
|
|
24
|
+
end
|
|
25
|
+
def test
|
|
26
|
+
Class2.new.test
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Class2
|
|
31
|
+
def test
|
|
32
|
+
M1::M2::Class3.new.test
|
|
33
|
+
Class4.new.test
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module M1
|
|
38
|
+
module M2
|
|
39
|
+
class Class3
|
|
40
|
+
def test
|
|
41
|
+
M3::Class5.test
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Class4
|
|
48
|
+
def test
|
|
49
|
+
M3::Class5.test
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module M3
|
|
54
|
+
class Class5
|
|
55
|
+
|
|
56
|
+
def self.test
|
|
57
|
+
self.self_call
|
|
58
|
+
Class1.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.self_call
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Test < MiniTest::Unit::TestCase
|
|
68
|
+
|
|
69
|
+
ExpectedTestGraph = {
|
|
70
|
+
'Class1' => Set.new(['Class2']) ,
|
|
71
|
+
'Class2' => Set.new(['M1::M2::Class3', 'Class4']) ,
|
|
72
|
+
'M1::M2::Class3' => Set.new(['M3::Class5']) ,
|
|
73
|
+
'Class4' => Set.new(['M3::Class5']) ,
|
|
74
|
+
'M3::Class5' => Set.new(['Class1']) }
|
|
75
|
+
|
|
76
|
+
ExpectedTestGraph_NamespaceDepth1 = {
|
|
77
|
+
'Class1' => Set.new(['Class2']) ,
|
|
78
|
+
'Class2' => Set.new(['M1', 'Class4']) ,
|
|
79
|
+
'M1' => Set.new(['M3']) ,
|
|
80
|
+
'Class4' => Set.new(['M3']) ,
|
|
81
|
+
'M3' => Set.new(['Class1']) }
|
|
82
|
+
|
|
83
|
+
ExpectedTestGraph_NamespaceDepth2 = {
|
|
84
|
+
'Class1' => Set.new(['Class2']) ,
|
|
85
|
+
'Class2' => Set.new(['M1::M2', 'Class4']) ,
|
|
86
|
+
'M1::M2' => Set.new(['M3::Class5']) ,
|
|
87
|
+
'Class4' => Set.new(['M3::Class5']) ,
|
|
88
|
+
'M3::Class5' => Set.new(['Class1']) }
|
|
89
|
+
|
|
90
|
+
ExpectedTestGraph_WithoutClass4 = {
|
|
91
|
+
'Class1' => Set.new(['Class2']) ,
|
|
92
|
+
'Class2' => Set.new(['M1::M2::Class3', 'M3::Class5']) ,
|
|
93
|
+
'M1::M2::Class3' => Set.new(['M3::Class5']) ,
|
|
94
|
+
'M3::Class5' => Set.new(['Class1']) }
|
|
95
|
+
|
|
96
|
+
ExpectedGraphvizGraph = 'digraph callgraph {"Class1" [shape=box];
|
|
97
|
+
"Class2" [shape=box];
|
|
98
|
+
"Class1" -> "Class2";
|
|
99
|
+
"Class2" [shape=box];
|
|
100
|
+
"M1::M2::Class3" [shape=box];
|
|
101
|
+
"Class2" -> "M1::M2::Class3";
|
|
102
|
+
"Class4" [shape=box];
|
|
103
|
+
"Class2" -> "Class4";
|
|
104
|
+
"M1::M2::Class3" [shape=box];
|
|
105
|
+
"M3::Class5" [shape=box];
|
|
106
|
+
"M1::M2::Class3" -> "M3::Class5";
|
|
107
|
+
"Class4" [shape=box];
|
|
108
|
+
"M3::Class5" [shape=box];
|
|
109
|
+
"Class4" -> "M3::Class5";
|
|
110
|
+
"M3::Class5" [shape=box];
|
|
111
|
+
"Class1" [shape=box];
|
|
112
|
+
"M3::Class5" -> "Class1";
|
|
113
|
+
}'
|
|
114
|
+
|
|
115
|
+
EmptyHash = {}
|
|
116
|
+
|
|
117
|
+
def test_class_dependency_tracing
|
|
118
|
+
assert_equal ExpectedTestGraph, CallGrapher.trace_class_dependencies{ Class1.new.test }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_graphviz_output
|
|
122
|
+
assert_equal ExpectedGraphvizGraph, CallGrapher.make_graphviz_graph(ExpectedTestGraph)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_file_whitelist
|
|
126
|
+
assert_equal EmptyHash,
|
|
127
|
+
CallGrapher.trace_class_dependencies(0, []){ Class1.new.test }
|
|
128
|
+
|
|
129
|
+
assert_equal ExpectedTestGraph,
|
|
130
|
+
CallGrapher.trace_class_dependencies(0, [__FILE__]) { Class1.new.test}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_namespace_depth
|
|
134
|
+
assert_equal ExpectedTestGraph_NamespaceDepth1,
|
|
135
|
+
CallGrapher.trace_class_dependencies(1) { Class1.new.test }
|
|
136
|
+
|
|
137
|
+
assert_equal ExpectedTestGraph_NamespaceDepth2,
|
|
138
|
+
CallGrapher.trace_class_dependencies(2) { Class1.new.test }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_class_blacklist
|
|
142
|
+
assert_equal ExpectedTestGraph_WithoutClass4, CallGrapher.trace_class_dependencies(0, [__FILE__], ['Class4']) { Class1.new.test }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# This test doesn't assert anything, it's just convenient to have the tests
|
|
146
|
+
# call Graphviz for you so you can manually inspect the output.
|
|
147
|
+
def test_make_graph
|
|
148
|
+
CallGrapher.make_graph ExpectedGraphvizGraph, './graph.png'
|
|
149
|
+
end
|
|
150
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: callgrapher
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Luke Andrew
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-09-02 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: Produce a call graph graph by tracing the method calls of running code.
|
|
15
|
+
The output is a .dot file for use with GraphViz
|
|
16
|
+
email:
|
|
17
|
+
- luke.callgrapher@la.id.au
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- lib/callgrapher.rb
|
|
23
|
+
- test/test.rb
|
|
24
|
+
- LICENSE-CC-BY-SA
|
|
25
|
+
- LICENSE-GPLv3
|
|
26
|
+
- README.md
|
|
27
|
+
homepage:
|
|
28
|
+
licenses: []
|
|
29
|
+
post_install_message:
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
none: false
|
|
35
|
+
requirements:
|
|
36
|
+
- - ! '>='
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.9'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
none: false
|
|
41
|
+
requirements:
|
|
42
|
+
- - ! '>='
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '0'
|
|
45
|
+
requirements: []
|
|
46
|
+
rubyforge_project:
|
|
47
|
+
rubygems_version: 1.8.23
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 3
|
|
50
|
+
summary: Produce call graphs of your Ruby code
|
|
51
|
+
test_files: []
|
|
52
|
+
has_rdoc:
|