rubydeps 0.2.0 → 0.9.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +32 -0
- data/README.md +62 -22
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/bin/rubydeps +26 -5
- data/ext/call_site_analyzer/call_site_analyzer.c +147 -0
- data/ext/call_site_analyzer/extconf.rb +11 -0
- data/lib/call_site_analyzer.bundle +0 -0
- data/lib/rubydeps.rb +56 -64
- data/rake-deps.png +0 -0
- data/rubydeps.gemspec +57 -0
- data/spec/rubydeps_spec.rb +64 -17
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +1 -1
- metadata +48 -65
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
archive-tar-minitar (0.5.2)
|
5
|
+
diff-lcs (1.1.3)
|
6
|
+
file_test_helper (1.0.2)
|
7
|
+
rake (0.9.2.2)
|
8
|
+
rake-compiler (0.8.0)
|
9
|
+
rake
|
10
|
+
rspec (2.8.0)
|
11
|
+
rspec-core (~> 2.8.0)
|
12
|
+
rspec-expectations (~> 2.8.0)
|
13
|
+
rspec-mocks (~> 2.8.0)
|
14
|
+
rspec-core (2.8.0)
|
15
|
+
rspec-expectations (2.8.0)
|
16
|
+
diff-lcs (~> 1.1.2)
|
17
|
+
rspec-mocks (2.8.0)
|
18
|
+
ruby-graphviz (1.0.5)
|
19
|
+
ruby_core_source (0.1.5)
|
20
|
+
archive-tar-minitar (>= 0.5.2)
|
21
|
+
thor (0.14.6)
|
22
|
+
|
23
|
+
PLATFORMS
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
file_test_helper (~> 1.0.0)
|
28
|
+
rake-compiler (~> 0.8.0)
|
29
|
+
rspec (~> 2.8.0)
|
30
|
+
ruby-graphviz (~> 1.0.5)
|
31
|
+
ruby_core_source (~> 0.1.5)
|
32
|
+
thor (~> 0.14.2)
|
data/README.md
CHANGED
@@ -1,8 +1,21 @@
|
|
1
|
+
[![Build Status](https://secure.travis-ci.org/dcadenas/rubydeps.png?branch=master)](http://travis-ci.org/dcadenas/rubydeps)
|
1
2
|
rubydeps
|
2
3
|
========
|
3
4
|
|
4
5
|
A tool to create class dependency graphs from test suites
|
5
6
|
|
7
|
+
Sample output
|
8
|
+
-------------
|
9
|
+
|
10
|
+
This is the result of running rubydeps on the [Rake](https://github.com/jimweirich/rake) tests:
|
11
|
+
|
12
|
+
```bash
|
13
|
+
rubydeps testunit --class_name_filter='^Rake'
|
14
|
+
```
|
15
|
+
|
16
|
+
![Rake dependencies](https://github.com/dcadenas/rubydeps/raw/master/rake-deps.png)
|
17
|
+
|
18
|
+
|
6
19
|
Command line usage
|
7
20
|
------------------
|
8
21
|
|
@@ -12,51 +25,78 @@ Rubydeps will run your test suite to record the call graph of your project and u
|
|
12
25
|
First of all, be sure to step into the root directory of your project, rubydeps searches for ./spec or ./test dirs from there.
|
13
26
|
For example, if we want to graph the Rails activemodel dependency graph we'd cd to rails/activemodel and from there we'd write:
|
14
27
|
|
15
|
-
|
28
|
+
```bash
|
29
|
+
rubydeps testunit #to run Test::Unit tests
|
30
|
+
```
|
31
|
+
|
16
32
|
or
|
17
|
-
|
33
|
+
|
34
|
+
```bash
|
35
|
+
rubydeps rspec #to run RSpec tests
|
36
|
+
```
|
37
|
+
|
18
38
|
or
|
19
|
-
|
39
|
+
|
40
|
+
```bash
|
41
|
+
rubydeps rspec2 #to run RSpec 2 tests
|
42
|
+
```
|
20
43
|
|
21
44
|
This will output a rubydeps.dot. You can convert the dot file to any image format you like using the dot utility that comes with the graphviz installation e.g.:
|
22
45
|
|
23
|
-
|
46
|
+
```bash
|
47
|
+
dot -Tsvg rubydeps.dot > rubydeps.svg
|
48
|
+
```
|
24
49
|
|
25
|
-
|
50
|
+
Notice that sometimes you may have missing dependencies as we graph the dependencies exercised by your tests so it's a quick bird's eye view to check your project coverage.
|
26
51
|
|
27
|
-
|
52
|
+
### Command line options
|
28
53
|
|
29
|
-
The
|
54
|
+
The `--path_filter` option specifies a regexp that matches the path of the files you are interested in analyzing. For example you could have filters like `'project_name/app|project_name/lib'` to analyze only code that is located in the `app` and `lib` dirs or as an alternative you could just exclude some directory you are not interested using a negative regexp like `'project_name(?!.*test)'`
|
30
55
|
|
31
|
-
|
32
|
-
-------------
|
56
|
+
The `--class_name_filter` option is similar to the `--path_filter` options except that the regexp is matched against the class names (i.e. graph node names).
|
33
57
|
|
34
|
-
|
58
|
+
The `--to_file` option dumps the dependency graph data to a file so you can do filtering later, it does not create a dot file.
|
35
59
|
|
36
|
-
|
60
|
+
The `--from_file` option is only available when you don't specify a test command. Its argument is the file dumped through `--to_file` in a previous run. When you use this option the tests (or block) are not ran, the dependency graph is loaded directly from the file. This is useful to avoid rerunning code that didn't change just for the purpose of filtering with different combinations e.g.:
|
37
61
|
|
38
|
-
|
39
|
-
|
40
|
-
|
62
|
+
```bash
|
63
|
+
rubydeps rspec2 --to_file='dependencies.dump'
|
64
|
+
rubydeps --from_file='dependencies.dump' --path_filter='app/models'
|
65
|
+
rubydeps --from_file='dependencies.dump' --path_filter='app/models|app/controllers'
|
66
|
+
```
|
41
67
|
|
42
|
-
|
68
|
+
Library usage
|
43
69
|
-------------
|
44
70
|
|
45
|
-
|
71
|
+
Just require rubydeps and pass a block to analyze to the `analyze` method.
|
46
72
|
|
47
|
-
|
73
|
+
```ruby
|
74
|
+
require 'rubydeps'
|
48
75
|
|
49
|
-
|
76
|
+
Rubydeps.analyze(:path_filter => path_filter_regexp, :class_name_filter => class_name_filter_regexp, :to_file => "dependencies.dump") do
|
77
|
+
# your code goes here
|
78
|
+
end
|
79
|
+
```
|
50
80
|
|
51
81
|
Installation
|
52
82
|
------------
|
53
83
|
|
54
|
-
|
84
|
+
```bash
|
85
|
+
gem install rubydeps
|
86
|
+
```
|
87
|
+
|
88
|
+
Rubydeps now only supports ruby 1.9. If you need 1.8.x support then:
|
89
|
+
|
90
|
+
```bash
|
91
|
+
gem install rubydeps -v0.2.0
|
92
|
+
```
|
93
|
+
|
94
|
+
Notice that in 0.2.0 you should use dot_for instead of analyze.
|
55
95
|
|
56
96
|
Dependencies
|
57
97
|
------------
|
58
98
|
|
59
|
-
* rcov
|
99
|
+
* rcov (only for version 0.2.0)
|
60
100
|
* graphviz
|
61
101
|
* ruby-graphviz
|
62
102
|
|
@@ -74,6 +114,6 @@ Note on Patches/Pull Requests
|
|
74
114
|
Copyright
|
75
115
|
---------
|
76
116
|
|
77
|
-
Copyright (c)
|
117
|
+
Copyright (c) 2012 Daniel Cadenas. See LICENSE for details.
|
78
118
|
|
79
|
-
Development sponsored by [Cubox](http://www.
|
119
|
+
Development sponsored by [Cubox](http://www.cuboxlabs.com)
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
spec = Gem::Specification.new do |s|
|
5
|
+
s.extensions = FileList["ext/**/extconf.rb"]
|
6
|
+
s.name = "rubydeps"
|
7
|
+
s.summary = %Q{A tool to create class dependency graphs from test suites}
|
8
|
+
s.description = %Q{A tool to create class dependency graphs from test suites}
|
9
|
+
s.email = "dcadenas@gmail.com"
|
10
|
+
s.homepage = "http://github.com/dcadenas/rubydeps"
|
11
|
+
s.authors = ["Daniel Cadenas"]
|
12
|
+
s.executables = ["rubydeps"]
|
13
|
+
|
14
|
+
s.add_development_dependency(%q<rake-compiler>, ["~> 0.8.0"])
|
15
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
|
16
|
+
s.add_development_dependency(%q<file_test_helper>, ["~> 1.0.0"])
|
17
|
+
s.add_dependency(%q<ruby_core_source>, ["~> 0.1.5"])
|
18
|
+
s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
|
19
|
+
s.add_dependency(%q<thor>, ["~> 0.14.2"])
|
20
|
+
|
21
|
+
s.version = File.read("VERSION")
|
22
|
+
s.files = `git ls-files`.split
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'rake/extensiontask'
|
26
|
+
Gem::PackageTask.new(spec) do |pkg|
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::ExtensionTask.new('call_site_analyzer', spec)
|
30
|
+
|
31
|
+
require 'rspec/core/rake_task'
|
32
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
33
|
+
t.rspec_opts = ["-f progress", "-r ./spec/spec_helper.rb"]
|
34
|
+
t.pattern = 'spec/*_spec.rb'
|
35
|
+
end
|
36
|
+
|
37
|
+
task :spec => :compile
|
38
|
+
|
39
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.9.0.pre
|
data/bin/rubydeps
CHANGED
@@ -5,19 +5,28 @@ require 'thor'
|
|
5
5
|
module Rubydeps
|
6
6
|
class Runner < Thor
|
7
7
|
desc "testunit", "Create the dependency graph after runnning the testunit tests"
|
8
|
+
method_option :to_file, :type => :string
|
8
9
|
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
9
10
|
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
10
11
|
def testunit
|
11
|
-
require '
|
12
|
+
require 'minitest/unit'
|
13
|
+
require 'test/unit/assertions'
|
14
|
+
require 'test/unit/testcase'
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift("#{`pwd`.chomp}/lib")
|
17
|
+
|
18
|
+
#dirty hack so that minitest doesn't install the at_exit hook and we can run the tests in this same process
|
19
|
+
::MiniTest::Unit.class_variable_set("@@installed_at_exit", true)
|
12
20
|
|
13
21
|
(Dir["./test/**/*_test.rb"] + Dir["./test/**/test_*.rb"]).each { |f| load f unless f =~ /^-/ }
|
14
22
|
|
15
23
|
create_dependencies_dot_for(options) do
|
16
|
-
::
|
24
|
+
::MiniTest::Unit.new.run([])
|
17
25
|
end
|
18
26
|
end
|
19
27
|
|
20
28
|
desc "rspec", "Create the dependency graph after runnning the rspec tests"
|
29
|
+
method_option :to_file, :type => :string
|
21
30
|
#TODO: this breaks when using underscores, investigate
|
22
31
|
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
23
32
|
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
@@ -33,7 +42,17 @@ module Rubydeps
|
|
33
42
|
end
|
34
43
|
end
|
35
44
|
|
45
|
+
desc "", "Loads dependencies saved by a --to_file option in a previous run. Doesn't run tests"
|
46
|
+
method_option :from_file, :type => :string, :required => true
|
47
|
+
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
48
|
+
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
49
|
+
default_task :load_deps
|
50
|
+
def load_deps
|
51
|
+
puts options[:from_file]
|
52
|
+
end
|
53
|
+
|
36
54
|
desc "rspec2", "Create the dependency graph after runnning the rspec 2 tests"
|
55
|
+
method_option :to_file, :type => :string
|
37
56
|
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
38
57
|
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
39
58
|
def rspec2
|
@@ -47,11 +66,14 @@ module Rubydeps
|
|
47
66
|
end
|
48
67
|
end
|
49
68
|
|
50
|
-
|
69
|
+
private
|
51
70
|
|
52
71
|
def create_dependencies_dot_for(options)
|
53
72
|
ARGV.clear
|
54
|
-
Rubydeps.
|
73
|
+
Rubydeps.analyze(:path_filter => Regexp.new(options[:path_filter]),
|
74
|
+
:class_name_filter => Regexp.new(options[:class_name_filter]),
|
75
|
+
:to_file => options[:to_file],
|
76
|
+
:from_file => options[:from_file]) do
|
55
77
|
yield
|
56
78
|
end
|
57
79
|
end
|
@@ -59,4 +81,3 @@ module Rubydeps
|
|
59
81
|
end
|
60
82
|
|
61
83
|
Rubydeps::Runner.start(ARGV)
|
62
|
-
|
@@ -0,0 +1,147 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <vm_core.h>
|
3
|
+
#include <iseq.h>
|
4
|
+
|
5
|
+
// Fix compile error in ruby 1.9.3
|
6
|
+
#ifdef RTYPEDDATA_DATA
|
7
|
+
#define ruby_current_thread ((rb_thread_t *)RTYPEDDATA_DATA(rb_thread_current()))
|
8
|
+
#endif
|
9
|
+
|
10
|
+
inline static rb_control_frame_t*
|
11
|
+
callsite_cfp(rb_control_frame_t* cfp){
|
12
|
+
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
|
13
|
+
if (cfp->iseq != 0 && cfp->pc != 0) {
|
14
|
+
return cfp;
|
15
|
+
}
|
16
|
+
else if (cfp->block_iseq) {
|
17
|
+
while (!cfp->iseq) cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
|
18
|
+
return cfp;
|
19
|
+
}
|
20
|
+
|
21
|
+
return NULL;
|
22
|
+
}
|
23
|
+
|
24
|
+
inline static VALUE
|
25
|
+
get_real_class(VALUE klass){
|
26
|
+
if (FL_TEST(klass, FL_SINGLETON)) {
|
27
|
+
VALUE v = rb_iv_get(klass, "__attached__");
|
28
|
+
|
29
|
+
switch (TYPE(v)) {
|
30
|
+
case T_CLASS: case T_MODULE:
|
31
|
+
return v;
|
32
|
+
default:
|
33
|
+
return rb_class_real(klass);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
return rb_class_real(klass);
|
37
|
+
}
|
38
|
+
|
39
|
+
inline static VALUE
|
40
|
+
class_of_obj_or_class(VALUE obj_or_class){
|
41
|
+
switch(TYPE(obj_or_class)){
|
42
|
+
case T_CLASS: case T_MODULE:
|
43
|
+
return obj_or_class;
|
44
|
+
default:
|
45
|
+
return rb_obj_class(obj_or_class);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
static VALUE dependency_array;
|
50
|
+
|
51
|
+
inline static void
|
52
|
+
add_dependency(VALUE calling_class, VALUE called_class, VALUE called_class_file_path, VALUE is_guess){
|
53
|
+
if(called_class == calling_class){
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
|
57
|
+
const char* called_class_name = rb_class2name(called_class);
|
58
|
+
const char* calling_class_name= rb_class2name(calling_class);
|
59
|
+
|
60
|
+
//update depedency_hash
|
61
|
+
VALUE dependency_hash = rb_ary_entry(dependency_array, 0);
|
62
|
+
VALUE calling_class_array = rb_hash_aref(dependency_hash, rb_str_new2(called_class_name));
|
63
|
+
if(NIL_P(calling_class_array)){
|
64
|
+
calling_class_array = rb_ary_new();
|
65
|
+
rb_hash_aset(dependency_hash, rb_str_new2(called_class_name), calling_class_array);
|
66
|
+
}
|
67
|
+
rb_ary_push(calling_class_array, rb_str_new2(calling_class_name));
|
68
|
+
|
69
|
+
//update class_location_hash
|
70
|
+
if(!NIL_P(called_class_file_path)){
|
71
|
+
VALUE class_location_hash = rb_ary_entry(dependency_array, 1);
|
72
|
+
|
73
|
+
VALUE file_path_array = rb_hash_aref(class_location_hash, rb_str_new2(called_class_name));
|
74
|
+
if(NIL_P(file_path_array)){
|
75
|
+
file_path_array = rb_ary_new();
|
76
|
+
rb_hash_aset(class_location_hash, rb_str_new2(called_class_name), file_path_array);
|
77
|
+
}
|
78
|
+
|
79
|
+
VALUE last_guess = rb_ary_entry(file_path_array, 1);
|
80
|
+
if(last_guess == Qnil || last_guess == Qtrue){
|
81
|
+
rb_ary_store(file_path_array, 0, called_class_file_path);
|
82
|
+
rb_ary_store(file_path_array, 1, is_guess);
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
//NOTE: this function should be as optimized as possible as it's being called on each ruby method call
|
88
|
+
static void
|
89
|
+
event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass){
|
90
|
+
rb_control_frame_t* cfp = GET_THREAD()->cfp;
|
91
|
+
VALUE class_of_called_object = class_of_obj_or_class(self);
|
92
|
+
VALUE called_class = get_real_class(cfp->iseq->klass);
|
93
|
+
|
94
|
+
rb_control_frame_t* previous_cfp = callsite_cfp(cfp);
|
95
|
+
if(previous_cfp != NULL){
|
96
|
+
VALUE calling_class = get_real_class(previous_cfp->iseq->klass);
|
97
|
+
|
98
|
+
if(class_of_called_object != calling_class){
|
99
|
+
if(class_of_called_object != called_class){
|
100
|
+
//we can't assume that the location of class_of_called_object is the same as called_class, so guess == true
|
101
|
+
add_dependency(calling_class, class_of_called_object, cfp->iseq->filepath, Qtrue);
|
102
|
+
} else {
|
103
|
+
add_dependency(calling_class, called_class, cfp->iseq->filepath, Qfalse);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
//this dependency represents inheritance/inclusion/extension
|
109
|
+
if(class_of_called_object != called_class){
|
110
|
+
add_dependency(class_of_called_object, called_class, cfp->iseq->filepath, Qfalse);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
static int uniq_calling_arrays(VALUE called_class, VALUE calling_class_array, VALUE extra){
|
115
|
+
rb_funcall(calling_class_array, rb_intern("uniq!"), 0);
|
116
|
+
return ST_CONTINUE;
|
117
|
+
}
|
118
|
+
|
119
|
+
static VALUE analyze(VALUE self){
|
120
|
+
if(rb_block_given_p()) {
|
121
|
+
dependency_array = rb_ary_new();
|
122
|
+
rb_global_variable(&dependency_array);
|
123
|
+
|
124
|
+
VALUE dependency_hash = rb_hash_new();
|
125
|
+
rb_ary_push(dependency_array, dependency_hash);
|
126
|
+
|
127
|
+
VALUE class_location_hash = rb_hash_new();
|
128
|
+
rb_ary_push(dependency_array, class_location_hash);
|
129
|
+
|
130
|
+
rb_add_event_hook(event_hook, RUBY_EVENT_CALL, Qnil);
|
131
|
+
rb_yield(Qnil);
|
132
|
+
rb_remove_event_hook(event_hook);
|
133
|
+
|
134
|
+
rb_hash_foreach(rb_ary_entry(dependency_array, 0), uniq_calling_arrays, 0);
|
135
|
+
} else {
|
136
|
+
rb_raise(rb_eArgError, "a block is required");
|
137
|
+
}
|
138
|
+
|
139
|
+
return dependency_array;
|
140
|
+
}
|
141
|
+
|
142
|
+
static VALUE rb_cCallSiteAnalyzer;
|
143
|
+
|
144
|
+
void Init_call_site_analyzer(){
|
145
|
+
rb_cCallSiteAnalyzer = rb_define_module("CallSiteAnalyzer");
|
146
|
+
rb_define_singleton_method(rb_cCallSiteAnalyzer, "analyze", analyze, 0);
|
147
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
require "ruby_core_source"
|
3
|
+
|
4
|
+
#comment this line if not debugging
|
5
|
+
$CFLAGS='-ggdb -Wall -O0 -pipe'
|
6
|
+
|
7
|
+
hdrs = proc { have_header("vm_core.h") and have_header("iseq.h") }
|
8
|
+
|
9
|
+
if !Ruby_core_source::create_makefile_with_core(hdrs, "call_site_analyzer")
|
10
|
+
STDERR.print("Makefile creation failed\n")
|
11
|
+
end
|
Binary file
|
data/lib/rubydeps.rb
CHANGED
@@ -1,99 +1,91 @@
|
|
1
1
|
require 'graphviz'
|
2
2
|
require 'set'
|
3
|
-
require '
|
4
|
-
require 'rcov'
|
3
|
+
require 'call_site_analyzer'
|
5
4
|
|
6
5
|
module Rubydeps
|
7
|
-
def self.
|
8
|
-
|
6
|
+
def self.analyze(options = {}, &block_to_analyze)
|
7
|
+
dependency_hash, class_location_hash = dependency_hash_for(options, &block_to_analyze)
|
9
8
|
|
10
|
-
if
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
9
|
+
if options[:to_file]
|
10
|
+
File.open(options[:to_file], 'wb') do |f|
|
11
|
+
f.write Marshal.dump([dependency_hash, class_location_hash])
|
12
|
+
end
|
13
|
+
else
|
14
|
+
if dependency_hash
|
15
|
+
g = GraphViz::new( "G", :use => 'dot', :mode => 'major', :rankdir => 'LR', :concentrate => 'true', :fontname => 'Arial')
|
16
|
+
dependency_hash.each do |k,vs|
|
17
|
+
if !k.empty? && !vs.empty?
|
18
|
+
n1 = g.add_nodes(k.to_s)
|
19
|
+
if vs.respond_to?(:each)
|
20
|
+
vs.each do |v|
|
21
|
+
unless v.empty?
|
22
|
+
n2 = g.add_nodes(v.to_s)
|
23
|
+
g.add_edges(n2, n1)
|
24
|
+
end
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
24
|
-
end
|
25
29
|
|
26
|
-
|
30
|
+
g.output( :dot => "rubydeps.dot" )
|
31
|
+
end
|
27
32
|
end
|
28
33
|
end
|
29
34
|
|
30
|
-
def self.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
+
def self.dependency_hash_for(options = {}, &block_to_analyze)
|
36
|
+
dependency_hash, class_location_hash = if options[:from_file]
|
37
|
+
Marshal.load(File.binread(options[:from_file]))
|
38
|
+
else
|
39
|
+
CallSiteAnalyzer.analyze(&block_to_analyze)
|
40
|
+
end
|
35
41
|
|
36
42
|
path_filter = options.fetch(:path_filter, /.*/)
|
37
43
|
class_name_filter = options.fetch(:class_name_filter, /.*/)
|
44
|
+
classes_to_remove = get_classes_to_remove(dependency_hash, class_location_hash, path_filter, class_name_filter)
|
38
45
|
|
39
|
-
|
40
|
-
|
41
|
-
|
46
|
+
while(!classes_to_remove.empty?) do
|
47
|
+
klass_to_remove = classes_to_remove.pop
|
48
|
+
classes_calling_class_to_remove = dependency_hash[klass_to_remove]
|
49
|
+
classes_called_by_class_to_remove = dependency_hash.keys.select do |called_class|
|
50
|
+
dependency_hash[called_class].member? klass_to_remove
|
51
|
+
end
|
42
52
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
53
|
+
#transitive dependencies, hmmm, not sure is a good idea
|
54
|
+
#if classes_calling_class_to_remove && !classes_calling_class_to_remove.empty?
|
55
|
+
# classes_called_by_class_to_remove.each do |called_class|
|
56
|
+
# dependency_hash[called_class] |= classes_calling_class_to_remove
|
57
|
+
# end
|
58
|
+
#end
|
47
59
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
called_class_method = "#{c}##{m}"
|
56
|
-
def_site = analyzer.defsite(called_class_method)
|
57
|
-
if path_filtered_site?(def_site, path_filter)
|
58
|
-
calling_class_names = Set.new
|
59
|
-
analyzer.callsites(called_class_method).each do |call_site, _|
|
60
|
-
if path_filtered_site?(call_site, path_filter)
|
61
|
-
calling_class = call_site.calling_class
|
62
|
-
calling_class_name = normalize_class_name(calling_class.to_s)
|
63
|
-
calling_class_names << calling_class_name
|
64
|
-
end
|
60
|
+
dependency_hash.delete(klass_to_remove)
|
61
|
+
classes_called_by_class_to_remove.each do |called_class|
|
62
|
+
if dependency_hash[called_class]
|
63
|
+
dependency_hash[called_class].delete(klass_to_remove)
|
64
|
+
|
65
|
+
if dependency_hash[called_class].empty?
|
66
|
+
dependency_hash.delete(called_class)
|
65
67
|
end
|
66
|
-
dependency_hash[called_class_name] ||= Set.new
|
67
|
-
dependency_hash[called_class_name] += calling_class_names
|
68
68
|
end
|
69
69
|
end
|
70
|
+
|
70
71
|
end
|
71
72
|
|
72
|
-
dependency_hash
|
73
|
+
[normalize_class_names(dependency_hash), class_location_hash]
|
73
74
|
end
|
74
75
|
|
75
|
-
def self.
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
cleaned_hash[called_class_name] = (calling_class_names - [nil]).select do |c|
|
80
|
-
interesting_class_name(c) &&
|
81
|
-
c != called_class_name &&
|
82
|
-
c =~ class_name_filter
|
83
|
-
end
|
84
|
-
cleaned_hash.delete(called_class_name) if cleaned_hash[called_class_name].empty?
|
85
|
-
end
|
76
|
+
def self.get_classes_to_remove(dependency_hash, class_location_hash, path_filter, class_name_filter)
|
77
|
+
(dependency_hash.keys | dependency_hash.values.flatten).reject do |klass|
|
78
|
+
class_name_filter =~ klass &&
|
79
|
+
class_location_hash[klass] && !class_location_hash[klass].empty? && class_location_hash[klass].first =~ path_filter
|
86
80
|
end
|
87
|
-
|
88
|
-
cleaned_hash
|
89
81
|
end
|
90
82
|
|
91
|
-
def self.
|
92
|
-
|
83
|
+
def self.normalize_class_names(dependency_hash)
|
84
|
+
Hash[dependency_hash.map { |k,v| [normalize_class_name(k), v.map{|c| c == k ? nil : normalize_class_name(c)}.compact] }]
|
93
85
|
end
|
94
86
|
|
95
87
|
def self.normalize_class_name(klass)
|
96
|
-
good_class_name = klass.gsub(/#<
|
88
|
+
good_class_name = klass.gsub(/#<(.+):(.+)>/, 'Instance of \1')
|
97
89
|
good_class_name.gsub!(/\([^\)]*\)/, "")
|
98
90
|
good_class_name.gsub(/0x[\da-fA-F]+/, '(hex number)')
|
99
91
|
end
|
data/rake-deps.png
ADDED
Binary file
|
data/rubydeps.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{rubydeps}
|
3
|
+
s.version = "0.9.0.pre"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Daniel Cadenas"]
|
7
|
+
s.date = %q{2010-09-28}
|
8
|
+
s.description = %q{Graphs ruby dependencies}
|
9
|
+
s.email = %q{dcadenas@gmail.com}
|
10
|
+
s.executables = ["rubydeps"]
|
11
|
+
s.extra_rdoc_files = [
|
12
|
+
"LICENSE"
|
13
|
+
]
|
14
|
+
s.files = [
|
15
|
+
".document",
|
16
|
+
".gitignore",
|
17
|
+
"LICENSE",
|
18
|
+
"README.md",
|
19
|
+
"bin/rubydeps",
|
20
|
+
"lib/rubydeps.rb"
|
21
|
+
]
|
22
|
+
s.homepage = %q{http://github.com/dcadenas/rubydeps}
|
23
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
s.rubygems_version = %q{1.3.7}
|
26
|
+
s.summary = %q{Graphs ruby depencencies}
|
27
|
+
s.test_files = [
|
28
|
+
"spec/rubydeps_spec.rb",
|
29
|
+
"spec/spec_helper.rb"
|
30
|
+
]
|
31
|
+
|
32
|
+
if s.respond_to? :specification_version then
|
33
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
34
|
+
s.specification_version = 3
|
35
|
+
|
36
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
37
|
+
s.add_development_dependency(%q<rake-compiler>, ["~> 0.8.0"])
|
38
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
|
39
|
+
s.add_development_dependency(%q<file_test_helper>, ["~> 1.0.0"])
|
40
|
+
s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
|
41
|
+
s.add_dependency(%q<thor>, ["~> 0.14.2"])
|
42
|
+
else
|
43
|
+
s.add_dependency(%q<rake-compiler>, ["~> 0.8.0"])
|
44
|
+
s.add_dependency(%q<rspec>, [">= 2.8.0"])
|
45
|
+
s.add_dependency(%q<file_test_helper>, ["~> 1.0.0"])
|
46
|
+
s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
|
47
|
+
s.add_dependency(%q<thor>, ["~> 0.14.2"])
|
48
|
+
end
|
49
|
+
else
|
50
|
+
s.add_dependency(%q<rake-compiler>, ["~> 0.8.0"])
|
51
|
+
s.add_dependency(%q<rspec>, [">= 2.8.0"])
|
52
|
+
s.add_dependency(%q<file_test_helper>, ["~> 1.0.0"])
|
53
|
+
s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
|
54
|
+
s.add_dependency(%q<thor>, ["~> 0.14.2"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/spec/rubydeps_spec.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'file_test_helper'
|
2
2
|
|
3
|
-
|
4
|
-
def
|
3
|
+
module GrandparentModule
|
4
|
+
def class_method
|
5
5
|
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Grandparent
|
9
|
+
extend GrandparentModule
|
6
10
|
|
7
11
|
def instance_method
|
8
12
|
end
|
@@ -29,6 +33,14 @@ class Son
|
|
29
33
|
def self.class_method2
|
30
34
|
end
|
31
35
|
|
36
|
+
def instance_method_that_calls_parent_class_method
|
37
|
+
Parent.class_method
|
38
|
+
end
|
39
|
+
|
40
|
+
def instance_method_calling_another_instance_method(second_receiver)
|
41
|
+
second_receiver.instance_method
|
42
|
+
end
|
43
|
+
|
32
44
|
def instance_method
|
33
45
|
Parent.class_method
|
34
46
|
Grandparent.class_method
|
@@ -38,7 +50,7 @@ end
|
|
38
50
|
describe "Rubydeps" do
|
39
51
|
include FileTestHelper
|
40
52
|
it "should show the class level dependencies" do
|
41
|
-
dependencies = Rubydeps.
|
53
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for do
|
42
54
|
class IHaveAClassLevelDependency
|
43
55
|
Son.class_method
|
44
56
|
end
|
@@ -49,7 +61,7 @@ describe "Rubydeps" do
|
|
49
61
|
|
50
62
|
it "should create a dot file" do
|
51
63
|
with_files do
|
52
|
-
|
64
|
+
::Rubydeps.analyze do
|
53
65
|
class IHaveAClassLevelDependency
|
54
66
|
Son.class_method
|
55
67
|
end
|
@@ -60,13 +72,13 @@ describe "Rubydeps" do
|
|
60
72
|
end
|
61
73
|
|
62
74
|
it "should be idempotent" do
|
63
|
-
Rubydeps.
|
75
|
+
::Rubydeps.dependency_hash_for do
|
64
76
|
class IHaveAClassLevelDependency
|
65
77
|
Son.class_method
|
66
78
|
end
|
67
79
|
end
|
68
80
|
|
69
|
-
dependencies = Rubydeps.
|
81
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for do
|
70
82
|
class IHaveAClassLevelDependency
|
71
83
|
Son.class_method
|
72
84
|
end
|
@@ -75,20 +87,34 @@ describe "Rubydeps" do
|
|
75
87
|
dependencies.should == {"Parent"=>["Son"]}
|
76
88
|
end
|
77
89
|
|
90
|
+
it "should show the dependency from an object singleton method" do
|
91
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for do
|
92
|
+
s = Son.new
|
93
|
+
def s.attached_method
|
94
|
+
Grandparent.class_method
|
95
|
+
end
|
96
|
+
s.attached_method
|
97
|
+
end
|
98
|
+
|
99
|
+
dependencies.keys.should == ["Grandparent", "GrandparentModule"]
|
100
|
+
dependencies["Grandparent"].should == ["Son"]
|
101
|
+
dependencies["GrandparentModule"].should == ["Grandparent"]
|
102
|
+
end
|
103
|
+
|
78
104
|
it "should show the dependencies between the classes inside the block" do
|
79
|
-
dependencies = Rubydeps.
|
80
|
-
Son.class_method
|
105
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for do
|
81
106
|
Son.new.instance_method
|
82
107
|
end
|
83
108
|
|
84
|
-
dependencies.keys.should =~ ["Parent", "Grandparent"]
|
109
|
+
dependencies.keys.should =~ ["Parent", "Grandparent", "GrandparentModule"]
|
85
110
|
dependencies["Parent"].should == ["Son"]
|
86
111
|
dependencies["Grandparent"].should =~ ["Son", "Parent"]
|
112
|
+
dependencies["GrandparentModule"].should == ["Grandparent"]
|
87
113
|
end
|
88
114
|
|
89
115
|
sample_dir_structure = {'path1/class_a.rb' => <<-CLASSA,
|
90
|
-
require 'path1/class_b'
|
91
|
-
require 'path2/class_c'
|
116
|
+
require './path1/class_b'
|
117
|
+
require './path2/class_c'
|
92
118
|
class A
|
93
119
|
def depend_on_b_and_c
|
94
120
|
B.new.b
|
@@ -101,9 +127,9 @@ describe "Rubydeps" do
|
|
101
127
|
|
102
128
|
it "should not filter classes when no filter is specified" do
|
103
129
|
with_files(sample_dir_structure) do
|
104
|
-
load 'path1/class_a.rb'
|
130
|
+
load './path1/class_a.rb'
|
105
131
|
|
106
|
-
dependencies = Rubydeps.
|
132
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for do
|
107
133
|
A.new.depend_on_b_and_c
|
108
134
|
end
|
109
135
|
|
@@ -113,9 +139,9 @@ describe "Rubydeps" do
|
|
113
139
|
|
114
140
|
it "should filter classes when a path filter is specified" do
|
115
141
|
with_files(sample_dir_structure) do
|
116
|
-
load 'path1/class_a.rb'
|
142
|
+
load './path1/class_a.rb'
|
117
143
|
|
118
|
-
dependencies = Rubydeps.
|
144
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for(:path_filter => /path1/) do
|
119
145
|
A.new.depend_on_b_and_c
|
120
146
|
end
|
121
147
|
|
@@ -125,13 +151,34 @@ describe "Rubydeps" do
|
|
125
151
|
|
126
152
|
it "should filter classes when a class name filter is specified" do
|
127
153
|
with_files(sample_dir_structure) do
|
128
|
-
load 'path1/class_a.rb'
|
154
|
+
load './path1/class_a.rb'
|
129
155
|
|
130
|
-
dependencies = Rubydeps.
|
156
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for(:class_name_filter => /C|A/) do
|
131
157
|
A.new.depend_on_b_and_c
|
132
158
|
end
|
133
159
|
|
134
160
|
dependencies.should == {"C"=>["A"]}
|
135
161
|
end
|
136
162
|
end
|
163
|
+
|
164
|
+
it "should be capable of dumping the whole dependency data into a file for later filtering" do
|
165
|
+
with_files(sample_dir_structure) do
|
166
|
+
load './path1/class_a.rb'
|
167
|
+
|
168
|
+
::Rubydeps.analyze(:to_file => 'dependencies.file') do
|
169
|
+
A.new.depend_on_b_and_c
|
170
|
+
end
|
171
|
+
|
172
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for(:from_file => 'dependencies.file', :class_name_filter => /C|A/)
|
173
|
+
dependencies.should == {"C"=>["A"]}
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should create correct dependencies for 2 instance methods called in a row" do
|
178
|
+
dependencies, _ = ::Rubydeps.dependency_hash_for do
|
179
|
+
Son.new.instance_method_calling_another_instance_method(Parent.new)
|
180
|
+
end
|
181
|
+
|
182
|
+
dependencies.should == {"Parent"=>["Son"]}
|
183
|
+
end
|
137
184
|
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubydeps
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 2
|
9
|
-
- 0
|
10
|
-
version: 0.2.0
|
4
|
+
prerelease: 6
|
5
|
+
version: 0.9.0.pre
|
11
6
|
platform: ruby
|
12
7
|
authors:
|
13
8
|
- Daniel Cadenas
|
@@ -15,113 +10,108 @@ autorequire:
|
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
12
|
|
18
|
-
date:
|
19
|
-
default_executable:
|
13
|
+
date: 2012-03-08 00:00:00 Z
|
20
14
|
dependencies:
|
21
15
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
16
|
+
name: rake-compiler
|
23
17
|
prerelease: false
|
24
18
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
19
|
none: false
|
26
20
|
requirements:
|
27
21
|
- - ~>
|
28
22
|
- !ruby/object:Gem::Version
|
29
|
-
|
30
|
-
segments:
|
31
|
-
- 2
|
32
|
-
- 5
|
33
|
-
- 1
|
34
|
-
version: 2.5.1
|
23
|
+
version: 0.8.0
|
35
24
|
type: :development
|
36
25
|
version_requirements: *id001
|
37
26
|
- !ruby/object:Gem::Dependency
|
38
|
-
name:
|
27
|
+
name: rspec
|
39
28
|
prerelease: false
|
40
29
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
30
|
none: false
|
42
31
|
requirements:
|
43
32
|
- - ~>
|
44
33
|
- !ruby/object:Gem::Version
|
45
|
-
|
46
|
-
segments:
|
47
|
-
- 1
|
48
|
-
- 0
|
49
|
-
- 0
|
50
|
-
version: 1.0.0
|
34
|
+
version: 2.8.0
|
51
35
|
type: :development
|
52
36
|
version_requirements: *id002
|
53
37
|
- !ruby/object:Gem::Dependency
|
54
|
-
name:
|
38
|
+
name: file_test_helper
|
55
39
|
prerelease: false
|
56
40
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
41
|
none: false
|
58
42
|
requirements:
|
59
43
|
- - ~>
|
60
44
|
- !ruby/object:Gem::Version
|
61
|
-
|
62
|
-
|
63
|
-
- 0
|
64
|
-
- 9
|
65
|
-
- 17
|
66
|
-
version: 0.9.17
|
67
|
-
type: :runtime
|
45
|
+
version: 1.0.0
|
46
|
+
type: :development
|
68
47
|
version_requirements: *id003
|
69
48
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
49
|
+
name: ruby_core_source
|
71
50
|
prerelease: false
|
72
51
|
requirement: &id004 !ruby/object:Gem::Requirement
|
73
52
|
none: false
|
74
53
|
requirements:
|
75
54
|
- - ~>
|
76
55
|
- !ruby/object:Gem::Version
|
77
|
-
|
78
|
-
segments:
|
79
|
-
- 0
|
80
|
-
- 9
|
81
|
-
- 8
|
82
|
-
version: 0.9.8
|
56
|
+
version: 0.1.5
|
83
57
|
type: :runtime
|
84
58
|
version_requirements: *id004
|
85
59
|
- !ruby/object:Gem::Dependency
|
86
|
-
name:
|
60
|
+
name: ruby-graphviz
|
87
61
|
prerelease: false
|
88
62
|
requirement: &id005 !ruby/object:Gem::Requirement
|
89
63
|
none: false
|
90
64
|
requirements:
|
91
65
|
- - ~>
|
92
66
|
- !ruby/object:Gem::Version
|
93
|
-
|
94
|
-
segments:
|
95
|
-
- 0
|
96
|
-
- 14
|
97
|
-
- 2
|
98
|
-
version: 0.14.2
|
67
|
+
version: 1.0.5
|
99
68
|
type: :runtime
|
100
69
|
version_requirements: *id005
|
101
|
-
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: thor
|
72
|
+
prerelease: false
|
73
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 0.14.2
|
79
|
+
type: :runtime
|
80
|
+
version_requirements: *id006
|
81
|
+
description: A tool to create class dependency graphs from test suites
|
102
82
|
email: dcadenas@gmail.com
|
103
83
|
executables:
|
104
84
|
- rubydeps
|
105
|
-
extensions:
|
85
|
+
extensions:
|
86
|
+
- ext/call_site_analyzer/extconf.rb
|
87
|
+
extra_rdoc_files: []
|
106
88
|
|
107
|
-
extra_rdoc_files:
|
108
|
-
- LICENSE
|
109
89
|
files:
|
110
90
|
- .document
|
111
91
|
- .gitignore
|
92
|
+
- .travis.yml
|
93
|
+
- Gemfile
|
94
|
+
- Gemfile.lock
|
112
95
|
- LICENSE
|
113
96
|
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- VERSION
|
114
99
|
- bin/rubydeps
|
100
|
+
- ext/call_site_analyzer/call_site_analyzer.c
|
101
|
+
- ext/call_site_analyzer/extconf.rb
|
102
|
+
- lib/call_site_analyzer.bundle
|
115
103
|
- lib/rubydeps.rb
|
104
|
+
- rake-deps.png
|
105
|
+
- rubydeps.gemspec
|
116
106
|
- spec/rubydeps_spec.rb
|
107
|
+
- spec/spec.opts
|
117
108
|
- spec/spec_helper.rb
|
118
|
-
has_rdoc: true
|
119
109
|
homepage: http://github.com/dcadenas/rubydeps
|
120
110
|
licenses: []
|
121
111
|
|
122
112
|
post_install_message:
|
123
|
-
rdoc_options:
|
124
|
-
|
113
|
+
rdoc_options: []
|
114
|
+
|
125
115
|
require_paths:
|
126
116
|
- lib
|
127
117
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -129,26 +119,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
129
119
|
requirements:
|
130
120
|
- - ">="
|
131
121
|
- !ruby/object:Gem::Version
|
132
|
-
hash: 3
|
133
|
-
segments:
|
134
|
-
- 0
|
135
122
|
version: "0"
|
136
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
124
|
none: false
|
138
125
|
requirements:
|
139
|
-
- - "
|
126
|
+
- - ">"
|
140
127
|
- !ruby/object:Gem::Version
|
141
|
-
|
142
|
-
segments:
|
143
|
-
- 0
|
144
|
-
version: "0"
|
128
|
+
version: 1.3.1
|
145
129
|
requirements: []
|
146
130
|
|
147
131
|
rubyforge_project:
|
148
|
-
rubygems_version: 1.
|
132
|
+
rubygems_version: 1.8.16
|
149
133
|
signing_key:
|
150
134
|
specification_version: 3
|
151
|
-
summary:
|
152
|
-
test_files:
|
153
|
-
|
154
|
-
- spec/spec_helper.rb
|
135
|
+
summary: A tool to create class dependency graphs from test suites
|
136
|
+
test_files: []
|
137
|
+
|