rubydeps 0.1.0 → 0.2.0
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/README.md +17 -6
- data/bin/rubydeps +15 -6
- data/lib/rubydeps.rb +17 -9
- data/spec/rubydeps_spec.rb +49 -1
- metadata +3 -3
data/README.md
CHANGED
@@ -12,24 +12,30 @@ Rubydeps will run your test suite to record the call graph of your project and u
|
|
12
12
|
First of all, be sure to step into the root directory of your project, rubydeps searches for ./spec or ./test dirs from there.
|
13
13
|
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
14
|
|
15
|
-
rubydeps testunit
|
15
|
+
rubydeps testunit #to run Test::Unit tests
|
16
16
|
or
|
17
|
-
rubydeps rspec
|
17
|
+
rubydeps rspec #to run RSpec tests
|
18
18
|
or
|
19
|
-
rubydeps rspec2
|
19
|
+
rubydeps rspec2 #to run RSpec 2 tests
|
20
20
|
|
21
|
-
This will output a rubydeps.dot.
|
22
|
-
You can convert the dot file to any image format you like using the dot utility that comes with the graphviz installation e.g.:
|
21
|
+
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.:
|
23
22
|
|
24
23
|
dot -Tsvg rubydeps.dot > rubydeps.svg
|
25
24
|
|
25
|
+
### Command line options
|
26
|
+
|
27
|
+
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)'
|
28
|
+
|
29
|
+
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).
|
26
30
|
|
27
31
|
Library usage
|
28
32
|
-------------
|
29
33
|
|
30
34
|
Just require rubydeps and pass a block to analyze to the dot_for method.
|
31
35
|
|
32
|
-
|
36
|
+
require 'rubydeps'
|
37
|
+
|
38
|
+
Rubydeps.dot_for(:path_filter => path_filter_regexp, :class_name_filter) do
|
33
39
|
//your code goes here
|
34
40
|
end
|
35
41
|
|
@@ -42,6 +48,11 @@ This is the result of running rubydeps on the [Mechanize](http://github.com/tend
|
|
42
48
|
|
43
49
|
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.
|
44
50
|
|
51
|
+
Installation
|
52
|
+
------------
|
53
|
+
|
54
|
+
gem install rubydeps
|
55
|
+
|
45
56
|
Dependencies
|
46
57
|
------------
|
47
58
|
|
data/bin/rubydeps
CHANGED
@@ -6,13 +6,13 @@ module Rubydeps
|
|
6
6
|
class Runner < Thor
|
7
7
|
desc "testunit", "Create the dependency graph after runnning the testunit tests"
|
8
8
|
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
9
|
+
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
9
10
|
def testunit
|
10
11
|
require 'test/unit'
|
11
12
|
|
12
13
|
(Dir["./test/**/*_test.rb"] + Dir["./test/**/test_*.rb"]).each { |f| load f unless f =~ /^-/ }
|
13
14
|
|
14
|
-
|
15
|
-
Rubydeps.dot_for(Regexp.new(options[:path_filter])) do
|
15
|
+
create_dependencies_dot_for(options) do
|
16
16
|
::Test::Unit::AutoRunner.run
|
17
17
|
end
|
18
18
|
end
|
@@ -20,6 +20,7 @@ module Rubydeps
|
|
20
20
|
desc "rspec", "Create the dependency graph after runnning the rspec tests"
|
21
21
|
#TODO: this breaks when using underscores, investigate
|
22
22
|
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
23
|
+
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
23
24
|
def rspec
|
24
25
|
require 'spec'
|
25
26
|
|
@@ -27,25 +28,33 @@ module Rubydeps
|
|
27
28
|
p.parse(Dir["./spec/**/*_spec.rb"])
|
28
29
|
op = p.options
|
29
30
|
|
30
|
-
|
31
|
-
Rubydeps.dot_for(Regexp.new(options[:path_filter])) do
|
31
|
+
create_dependencies_dot_for(options) do
|
32
32
|
::Spec::Runner::CommandLine.run(op)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
desc "rspec2", "Create the dependency graph after runnning the rspec 2 tests"
|
37
37
|
method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
|
38
|
+
method_option :class_name_filter, :type => :string, :default => '', :required => true
|
38
39
|
def rspec2
|
39
40
|
require 'rspec'
|
40
41
|
|
41
42
|
op = ::RSpec::Core::ConfigurationOptions.new(Dir["./spec/**/*_spec.rb"])
|
42
43
|
op.parse_options
|
43
44
|
|
44
|
-
|
45
|
-
Rubydeps.dot_for(Regexp.new(options[:path_filter])) do
|
45
|
+
create_dependencies_dot_for(options) do
|
46
46
|
::RSpec::Core::CommandLine.new(op).run($stderr, $stdout)
|
47
47
|
end
|
48
48
|
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def create_dependencies_dot_for(options)
|
53
|
+
ARGV.clear
|
54
|
+
Rubydeps.dot_for(:path_filter => Regexp.new(options[:path_filter]), :class_name_filter => Regexp.new(options[:class_name_filter])) do
|
55
|
+
yield
|
56
|
+
end
|
57
|
+
end
|
49
58
|
end
|
50
59
|
end
|
51
60
|
|
data/lib/rubydeps.rb
CHANGED
@@ -4,8 +4,8 @@ require 'rcovrt'
|
|
4
4
|
require 'rcov'
|
5
5
|
|
6
6
|
module Rubydeps
|
7
|
-
def self.dot_for(
|
8
|
-
dependencies_hash = hash_for(
|
7
|
+
def self.dot_for(options = {}, &block_to_analyze)
|
8
|
+
dependencies_hash = hash_for(options, &block_to_analyze)
|
9
9
|
|
10
10
|
if dependencies_hash
|
11
11
|
g = GraphViz::new( "G", :use => 'dot', :mode => 'major', :rankdir => 'LR', :concentrate => 'true', :fontname => 'Arial')
|
@@ -27,14 +27,17 @@ module Rubydeps
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def self.hash_for(
|
30
|
+
def self.hash_for(options = {}, &block_to_analyze)
|
31
31
|
analyzer = Rcov::CallSiteAnalyzer.new
|
32
32
|
analyzer.run_hooked do
|
33
33
|
block_to_analyze.call
|
34
34
|
end
|
35
35
|
|
36
|
+
path_filter = options.fetch(:path_filter, /.*/)
|
37
|
+
class_name_filter = options.fetch(:class_name_filter, /.*/)
|
38
|
+
|
36
39
|
dependency_hash = create_dependency_hash(analyzer, path_filter)
|
37
|
-
clean_hash(dependency_hash)
|
40
|
+
clean_hash(dependency_hash, class_name_filter)
|
38
41
|
end
|
39
42
|
|
40
43
|
private
|
@@ -43,6 +46,7 @@ private
|
|
43
46
|
end
|
44
47
|
|
45
48
|
#we build a hash structured in this way: {"called_class_name1" => ["calling_class_name1", "calling_class_name2"], "called_class_name2" => ...}
|
49
|
+
#TODO: want moar love here
|
46
50
|
def self.create_dependency_hash(analyzer, path_filter)
|
47
51
|
dependency_hash = {}
|
48
52
|
analyzer.analyzed_classes.each do |c|
|
@@ -51,7 +55,7 @@ private
|
|
51
55
|
called_class_method = "#{c}##{m}"
|
52
56
|
def_site = analyzer.defsite(called_class_method)
|
53
57
|
if path_filtered_site?(def_site, path_filter)
|
54
|
-
calling_class_names =
|
58
|
+
calling_class_names = Set.new
|
55
59
|
analyzer.callsites(called_class_method).each do |call_site, _|
|
56
60
|
if path_filtered_site?(call_site, path_filter)
|
57
61
|
calling_class = call_site.calling_class
|
@@ -59,7 +63,7 @@ private
|
|
59
63
|
calling_class_names << calling_class_name
|
60
64
|
end
|
61
65
|
end
|
62
|
-
dependency_hash[called_class_name] ||=
|
66
|
+
dependency_hash[called_class_name] ||= Set.new
|
63
67
|
dependency_hash[called_class_name] += calling_class_names
|
64
68
|
end
|
65
69
|
end
|
@@ -68,11 +72,15 @@ private
|
|
68
72
|
dependency_hash
|
69
73
|
end
|
70
74
|
|
71
|
-
def self.clean_hash(dependency_hash)
|
75
|
+
def self.clean_hash(dependency_hash, class_name_filter)
|
72
76
|
cleaned_hash = {}
|
73
77
|
dependency_hash.each do |called_class_name, calling_class_names|
|
74
|
-
if interesting_class_name(called_class_name) && !dependency_hash[called_class_name].empty?
|
75
|
-
cleaned_hash[called_class_name] = calling_class_names.
|
78
|
+
if interesting_class_name(called_class_name) && !dependency_hash[called_class_name].empty? && called_class_name =~ class_name_filter
|
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
|
76
84
|
cleaned_hash.delete(called_class_name) if cleaned_hash[called_class_name].empty?
|
77
85
|
end
|
78
86
|
end
|
data/spec/rubydeps_spec.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
#require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
1
|
require 'file_test_helper'
|
3
2
|
|
4
3
|
class Grandparent
|
@@ -86,4 +85,53 @@ describe "Rubydeps" do
|
|
86
85
|
dependencies["Parent"].should == ["Son"]
|
87
86
|
dependencies["Grandparent"].should =~ ["Son", "Parent"]
|
88
87
|
end
|
88
|
+
|
89
|
+
sample_dir_structure = {'path1/class_a.rb' => <<-CLASSA,
|
90
|
+
require 'path1/class_b'
|
91
|
+
require 'path2/class_c'
|
92
|
+
class A
|
93
|
+
def depend_on_b_and_c
|
94
|
+
B.new.b
|
95
|
+
C.new.c
|
96
|
+
end
|
97
|
+
end
|
98
|
+
CLASSA
|
99
|
+
'path1/class_b.rb' => 'class B; def b; end end',
|
100
|
+
'path2/class_c.rb' => 'class C; def c; end end'}
|
101
|
+
|
102
|
+
it "should not filter classes when no filter is specified" do
|
103
|
+
with_files(sample_dir_structure) do
|
104
|
+
load 'path1/class_a.rb'
|
105
|
+
|
106
|
+
dependencies = Rubydeps.hash_for do
|
107
|
+
A.new.depend_on_b_and_c
|
108
|
+
end
|
109
|
+
|
110
|
+
dependencies.should == {"B"=>["A"], "C"=>["A"]}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should filter classes when a path filter is specified" do
|
115
|
+
with_files(sample_dir_structure) do
|
116
|
+
load 'path1/class_a.rb'
|
117
|
+
|
118
|
+
dependencies = Rubydeps.hash_for(:path_filter => /path1/) do
|
119
|
+
A.new.depend_on_b_and_c
|
120
|
+
end
|
121
|
+
|
122
|
+
dependencies.should == {"B"=>["A"]}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should filter classes when a class name filter is specified" do
|
127
|
+
with_files(sample_dir_structure) do
|
128
|
+
load 'path1/class_a.rb'
|
129
|
+
|
130
|
+
dependencies = Rubydeps.hash_for(:class_name_filter => /C|A/) do
|
131
|
+
A.new.depend_on_b_and_c
|
132
|
+
end
|
133
|
+
|
134
|
+
dependencies.should == {"C"=>["A"]}
|
135
|
+
end
|
136
|
+
end
|
89
137
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubydeps
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Daniel Cadenas
|