churn 0.0.2
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/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +32 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/lib/churn.rb +1 -0
- data/lib/churn/churn_calculator.rb +209 -0
- data/lib/churn/churn_history.rb +25 -0
- data/lib/churn/git_analyzer.rb +73 -0
- data/lib/churn/location_mapping.rb +49 -0
- data/lib/churn/source_control.rb +9 -0
- data/lib/churn/svn_analyzer.rb +22 -0
- data/lib/tasks/churn_tasks.rb +20 -0
- data/test/data/churn_calculator.rb +217 -0
- data/test/data/test_helper.rb +14 -0
- data/test/test_helper.rb +14 -0
- data/test/unit/churn_calculator_test.rb +82 -0
- data/test/unit/churn_history_test.rb +24 -0
- data/test/unit/git_analyzer_test.rb +17 -0
- data/test/unit/location_mapping_test.rb +35 -0
- metadata +91 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Dan Mayer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
= churn
|
2
|
+
|
3
|
+
A Project to give the churn file, class, and method for a project for a given checkin
|
4
|
+
This will allow us over time to give the number of times a file, class, or method is changing during the life of a project.
|
5
|
+
|
6
|
+
TODO:
|
7
|
+
* SVN only supports file
|
8
|
+
* make storage directory configurable instead of using tmp
|
9
|
+
* allow passing in directories to churn, directories to ignore
|
10
|
+
* add a bin/ with args passed as well as just the rake task
|
11
|
+
* todo add a filter that allows for other files besides. *.rb
|
12
|
+
|
13
|
+
Usage:
|
14
|
+
* 'gem install churn'
|
15
|
+
* on any project you want to use churn, add "require 'churn'" to your rake file
|
16
|
+
* run 'rake churn' to view the current output, file churn history is immediate, class and method churn builds up a history as it is run on each revision
|
17
|
+
* temporary files with class / method churn history are stored in /tmp, to clear churn history delete them
|
18
|
+
|
19
|
+
== Note on Patches/Pull Requests
|
20
|
+
|
21
|
+
* Fork the project.
|
22
|
+
* Make your feature addition or bug fix.
|
23
|
+
* Add tests for it. This is important so I don't break it in a
|
24
|
+
future version unintentionally.
|
25
|
+
* Commit, do not mess with rakefile, version, or history.
|
26
|
+
(if you want to have your own version, that is fine but
|
27
|
+
bump version in a commit by itself I can ignore when I pull)
|
28
|
+
* Send me a pull request. Bonus points for topic branches.
|
29
|
+
|
30
|
+
== Copyright
|
31
|
+
|
32
|
+
Copyright (c) 2009 Dan Mayer. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'lib/tasks/churn_tasks'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.name = "churn"
|
9
|
+
gem.summary = %Q{Providing additional churn metrics over the original metric_fu churn}
|
10
|
+
gem.description = %Q{High method and class churn has been shown to have increased bug and error rates. This gem helps you know what is changing a lot so you can do additional testing, code review, or refactoring to try to tame the volatile code. }
|
11
|
+
gem.email = "Danmayer@gmail.com"
|
12
|
+
gem.homepage = "http://github.com/danmayer/churn"
|
13
|
+
gem.authors = ["Dan Mayer"]
|
14
|
+
gem.add_development_dependency "thoughtbot-shoulda"
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/*_test.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/*_test.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
if File.exist?('VERSION')
|
48
|
+
version = File.read('VERSION')
|
49
|
+
else
|
50
|
+
version = ""
|
51
|
+
end
|
52
|
+
|
53
|
+
rdoc.rdoc_dir = 'rdoc'
|
54
|
+
rdoc.title = "churn #{version}"
|
55
|
+
rdoc.rdoc_files.include('README*')
|
56
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
57
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.2
|
data/lib/churn.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'tasks', 'churn_tasks')
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
require 'sexp_processor'
|
3
|
+
require 'ruby_parser'
|
4
|
+
require 'json'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
require 'source_control'
|
9
|
+
require 'git_analyzer'
|
10
|
+
require 'svn_analyzer'
|
11
|
+
require 'location_mapping'
|
12
|
+
require 'churn_history'
|
13
|
+
|
14
|
+
module Churn
|
15
|
+
|
16
|
+
class ChurnCalculator
|
17
|
+
|
18
|
+
def initialize(options={})
|
19
|
+
start_date = options.fetch(:start_date) { '3 months ago' }
|
20
|
+
@minimum_churn_count = options.fetch(:minimum_churn_count) { 5 }
|
21
|
+
@source_control = set_source_control(start_date)
|
22
|
+
@revision_changes = {}
|
23
|
+
@method_changes = {}
|
24
|
+
@class_changes = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def report(print = true)
|
28
|
+
self.emit
|
29
|
+
self.analyze
|
30
|
+
print ? self.to_s : self.to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
def emit
|
34
|
+
@changes = parse_log_for_changes.reject {|file, change_count| change_count < @minimum_churn_count}
|
35
|
+
@revisions = parse_log_for_revision_changes
|
36
|
+
end
|
37
|
+
|
38
|
+
def analyze
|
39
|
+
@changes = @changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
40
|
+
@changes = @changes.map {|file_path, times_changed| {:file_path => file_path, :times_changed => times_changed }}
|
41
|
+
|
42
|
+
calculate_revision_changes
|
43
|
+
|
44
|
+
@method_changes = @method_changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
45
|
+
@method_changes = @method_changes.map {|method, times_changed| {'method' => method, 'times_changed' => times_changed }}
|
46
|
+
@class_changes = @class_changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
47
|
+
@class_changes = @class_changes.map {|klass, times_changed| {'klass' => klass, 'times_changed' => times_changed }}
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_h
|
51
|
+
hash = {:churn => {:changes => @changes}}
|
52
|
+
hash[:churn][:class_churn] = @class_changes
|
53
|
+
hash[:churn][:method_churn] = @method_changes
|
54
|
+
#detail the most recent changes made this revision
|
55
|
+
if @revision_changes[@revisions.first]
|
56
|
+
changes = @revision_changes[@revisions.first]
|
57
|
+
hash[:churn][:changed_files] = changes[:files]
|
58
|
+
hash[:churn][:changed_classes] = changes[:classes]
|
59
|
+
hash[:churn][:changed_methods] = changes[:methods]
|
60
|
+
end
|
61
|
+
#TODO crappy place to do this but save hash to revision file but while entirely under metric_fu only choice
|
62
|
+
ChurnHistory.store_revision_history(@revisions.first, hash)
|
63
|
+
hash
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
hash = to_h
|
68
|
+
result = seperator
|
69
|
+
result +="* Revision Changes \n"
|
70
|
+
result += seperator
|
71
|
+
result += "files: \n"
|
72
|
+
result += display_array(hash[:churn][:changed_files])
|
73
|
+
result += "\nclasses: \n"
|
74
|
+
result += display_array(hash[:churn][:changed_classes])
|
75
|
+
result += "\nmethods: \n"
|
76
|
+
result += display_array(hash[:churn][:changed_methods])
|
77
|
+
result += seperator
|
78
|
+
result +="* Project Churn \n"
|
79
|
+
result += seperator
|
80
|
+
result += "files: \n"
|
81
|
+
result += display_array(hash[:churn][:changes])
|
82
|
+
result += "\nclasses: \n"
|
83
|
+
result += display_array(hash[:churn][:class_churn])
|
84
|
+
result += "\nmethods: \n"
|
85
|
+
result += display_array(hash[:churn][:method_churn])
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def display_array(array)
|
91
|
+
result = ""
|
92
|
+
array.each { |element| result += " * #{element.inspect}\n" } if array
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def seperator
|
97
|
+
"*"*70+"\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.git?
|
101
|
+
system("git branch")
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_source_control(start_date)
|
105
|
+
if self.class.git?
|
106
|
+
GitAnalyzer.new(start_date)
|
107
|
+
elsif File.exist?(".svn")
|
108
|
+
SvnAnalyzer.new(start_date)
|
109
|
+
else
|
110
|
+
raise "Churning requires a subversion or git repo"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def calculate_revision_changes
|
115
|
+
@revisions.each do |revision|
|
116
|
+
if revision == @revisions.first
|
117
|
+
#can't iterate through all the changes and tally them up
|
118
|
+
#it only has the current files not the files at the time of the revision
|
119
|
+
#parsing requires the files
|
120
|
+
changed_files, changed_classes, changed_methods = calculate_revision_data(revision)
|
121
|
+
else
|
122
|
+
changed_files, changed_classes, changed_methods = ChurnHistory.load_revision_data(revision)
|
123
|
+
end
|
124
|
+
calculate_changes!(changed_methods, @method_changes) if changed_methods
|
125
|
+
calculate_changes!(changed_classes, @class_changes) if changed_classes
|
126
|
+
|
127
|
+
@revision_changes[revision] = { :files => changed_files, :classes => changed_classes, :methods => changed_methods }
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def calculate_revision_data(revision)
|
132
|
+
changed_files = parse_logs_for_updated_files(revision, @revisions)
|
133
|
+
|
134
|
+
changed_classes = []
|
135
|
+
changed_methods = []
|
136
|
+
changed_files.each do |file_changes|
|
137
|
+
if file_changes.first.match(/.*\.rb/)
|
138
|
+
classes, methods = get_changes(file_changes)
|
139
|
+
changed_classes += classes
|
140
|
+
changed_methods += methods
|
141
|
+
end
|
142
|
+
end
|
143
|
+
changed_files = changed_files.map { |file, lines| file }
|
144
|
+
[changed_files, changed_classes, changed_methods]
|
145
|
+
end
|
146
|
+
|
147
|
+
def calculate_changes!(changed_objs, total_changes)
|
148
|
+
if changed_objs
|
149
|
+
changed_objs.each do |change|
|
150
|
+
total_changes.include?(change) ? total_changes[change] = total_changes[change]+1 : total_changes[change] = 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
total_changes
|
154
|
+
end
|
155
|
+
|
156
|
+
def get_changes(change)
|
157
|
+
file = change.first
|
158
|
+
breakdown = LocationMapping.new
|
159
|
+
breakdown.get_info(file)
|
160
|
+
changes = change.last
|
161
|
+
classes = changes_for_type(changes, breakdown.klasses_collection)
|
162
|
+
methods = changes_for_type(changes, breakdown.methods_collection)
|
163
|
+
classes = classes.map{ |klass| {'file' => file, 'klass' => klass} }
|
164
|
+
methods = methods.map{ |method| {'file' => file, 'klass' => get_klass_for(method), 'method' => method} }
|
165
|
+
[classes, methods]
|
166
|
+
rescue => error
|
167
|
+
[[],[]]
|
168
|
+
end
|
169
|
+
|
170
|
+
def get_klass_for(method)
|
171
|
+
method.gsub(/(#|\.).*/,'')
|
172
|
+
end
|
173
|
+
|
174
|
+
def changes_for_type(changes, item_collection)
|
175
|
+
changed_items = []
|
176
|
+
item_collection.each_pair do |item, item_lines|
|
177
|
+
item_lines = item_lines[0].to_a
|
178
|
+
changes.each do |change_range|
|
179
|
+
item_lines.each do |line|
|
180
|
+
changed_items << item if change_range.include?(line) && !changed_items.include?(item)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
changed_items
|
185
|
+
end
|
186
|
+
|
187
|
+
def parse_log_for_changes
|
188
|
+
changes = {}
|
189
|
+
|
190
|
+
logs = @source_control.get_logs
|
191
|
+
logs.each do |line|
|
192
|
+
changes[line] ? changes[line] += 1 : changes[line] = 1
|
193
|
+
end
|
194
|
+
changes
|
195
|
+
end
|
196
|
+
|
197
|
+
def parse_log_for_revision_changes
|
198
|
+
@source_control.get_revisions
|
199
|
+
end
|
200
|
+
|
201
|
+
def parse_logs_for_updated_files(revision, revisions)
|
202
|
+
#SVN doesn't support this
|
203
|
+
return {} unless @source_control.respond_to?(:get_updated_files_change_info)
|
204
|
+
@source_control.get_updated_files_change_info(revision, revisions)
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
class ChurnHistory
|
4
|
+
|
5
|
+
def self.store_revision_history(revision, hash_data)
|
6
|
+
FileUtils.mkdir 'tmp' unless File.directory?('tmp')
|
7
|
+
File.open("tmp/#{revision}.json", 'w') {|f| f.write(hash_data.to_json) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load_revision_data(revision)
|
11
|
+
#load revision data from scratch folder if it exists
|
12
|
+
filename = "tmp/#{revision}.json"
|
13
|
+
if File.exists?(filename)
|
14
|
+
json_data = File.read(filename)
|
15
|
+
data = JSON.parse(json_data)
|
16
|
+
changed_files = data['churn']['changed_files']
|
17
|
+
changed_classes = data['churn']['changed_classes']
|
18
|
+
changed_methods = data['churn']['changed_methods']
|
19
|
+
end
|
20
|
+
[changed_files, changed_classes, changed_methods]
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
class GitAnalyzer < SourceControl
|
4
|
+
def get_logs
|
5
|
+
`git log #{date_range} --name-only --pretty=format:`.split(/\n/).reject{|line| line == ""}
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_revisions
|
9
|
+
`git log #{date_range} --pretty=format:"%H"`.split(/\n/).reject{|line| line == ""}
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_updated_files_from_log(revision, revisions)
|
13
|
+
current_index = revisions.index(revision)
|
14
|
+
previous_index = current_index+1
|
15
|
+
previous_revision = revisions[previous_index] unless revisions.length < previous_index
|
16
|
+
if revision && previous_revision
|
17
|
+
`git diff #{revision} #{previous_revision} --unified=0`.split(/\n/).select{|line| line.match(/^@@/) || line.match(/^---/) || line.match(/^\+\+\+/) }
|
18
|
+
else
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_updated_files_change_info(revision, revisions)
|
24
|
+
updated = {}
|
25
|
+
logs = get_updated_files_from_log(revision, revisions)
|
26
|
+
recent_file = nil
|
27
|
+
logs.each do |line|
|
28
|
+
if line.match(/^---/) || line.match(/^\+\+\+/)
|
29
|
+
recent_file = get_recent_file(line)
|
30
|
+
updated[recent_file] = [] unless updated.include?(recent_file)
|
31
|
+
elsif line.match(/^@@/)
|
32
|
+
removed_range = get_changed_range(line, '-')
|
33
|
+
added_range = get_changed_range(line, '\+')
|
34
|
+
updated[recent_file] << removed_range
|
35
|
+
updated[recent_file] << added_range
|
36
|
+
else
|
37
|
+
puts line.match(/^---/)
|
38
|
+
raise "git diff lines that don't match the two patterns aren't expected: '#{line}'"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
updated
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def get_changed_range(line, matcher)
|
47
|
+
change_start = line.match(/#{matcher}[0-9]+/)
|
48
|
+
change_end = line.match(/#{matcher}[0-9]+,[0-9]+/)
|
49
|
+
change_start = change_start.to_s.gsub(/#{matcher}/,'')
|
50
|
+
change_end = change_end.to_s.gsub(/.*,/,'')
|
51
|
+
|
52
|
+
range = if change_end && change_end!=''
|
53
|
+
(change_start.to_i..(change_start.to_i+change_end.to_i))
|
54
|
+
else
|
55
|
+
(change_start.to_i..change_start.to_i)
|
56
|
+
end
|
57
|
+
range
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_recent_file(line)
|
61
|
+
line = line.gsub(/^--- /,'').gsub(/^\+\+\+ /,'').gsub(/^a\//,'').gsub(/^b\//,'')
|
62
|
+
end
|
63
|
+
|
64
|
+
def date_range
|
65
|
+
if @start_date
|
66
|
+
date = Chronic.parse(@start_date)
|
67
|
+
"--after=#{date.strftime('%Y-%m-%d')}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
class LocationMapping < SexpProcessor
|
4
|
+
|
5
|
+
attr_reader :klasses_collection, :methods_collection
|
6
|
+
|
7
|
+
def initialize()
|
8
|
+
super
|
9
|
+
@klasses_collection = {}
|
10
|
+
@methods_collection = {}
|
11
|
+
@parser = RubyParser.new
|
12
|
+
self.auto_shift_type = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_info(file)
|
16
|
+
ast = @parser.process(File.read(file), file)
|
17
|
+
process ast
|
18
|
+
end
|
19
|
+
|
20
|
+
def process_class(exp)
|
21
|
+
name = exp.shift
|
22
|
+
start_line = exp.line
|
23
|
+
last_line = exp.last.line
|
24
|
+
name = name if name.is_a?(Symbol)
|
25
|
+
name = name.values.value if name.is_a?(Sexp) #deals with cases like class Test::Unit::TestCase
|
26
|
+
@current_class = name
|
27
|
+
@klasses_collection[name.to_s] = [] unless @klasses_collection.include?(name)
|
28
|
+
@klasses_collection[name.to_s] << (start_line..last_line)
|
29
|
+
analyze_list exp
|
30
|
+
s()
|
31
|
+
end
|
32
|
+
|
33
|
+
def analyze_list exp
|
34
|
+
process exp.shift until exp.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_defn(exp)
|
38
|
+
name = exp.shift
|
39
|
+
start_line = exp.line
|
40
|
+
last_line = exp.last.line
|
41
|
+
full_name = "#{@current_class}##{name}"
|
42
|
+
@methods_collection[full_name] = [] unless @methods_collection.include?(full_name)
|
43
|
+
@methods_collection[full_name] << (start_line..last_line)
|
44
|
+
return s(:defn, name, process(exp.shift), process(exp.shift))
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Churn
|
2
|
+
|
3
|
+
class Svn < SourceControl
|
4
|
+
def get_logs
|
5
|
+
`svn log #{date_range} --verbose`.split(/\n/).map { |line| clean_up_svn_line(line) }.compact
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def date_range
|
10
|
+
if @start_date
|
11
|
+
date = Chronic.parse(@start_date)
|
12
|
+
"--revision {#{date.strftime('%Y-%m-%d')}}:{#{Time.now.strftime('%Y-%m-%d')}}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def clean_up_svn_line(line)
|
17
|
+
m = line.match(/\W*[A,M]\W+(\/.*)\b/)
|
18
|
+
m ? m[1] : nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
def report_churn()
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'churn', 'churn_calculator')
|
3
|
+
Churn::ChurnCalculator.new({:minimum_churn_count => 3}).report
|
4
|
+
end
|
5
|
+
|
6
|
+
desc "Report the current churn for the project"
|
7
|
+
task :churn do
|
8
|
+
report = report_churn()
|
9
|
+
puts report
|
10
|
+
# puts "entire report"
|
11
|
+
# puts report.inspect.to_s
|
12
|
+
# puts "_"*50
|
13
|
+
# puts "changed classes: #{report[:churn][:changed_classes].inspect}"
|
14
|
+
# puts "_"*50
|
15
|
+
# puts "cahnged methods: #{report[:churn][:changed_methods].inspect}"
|
16
|
+
# puts "_"*50
|
17
|
+
# put
|
18
|
+
# s "method churn: #{report[:churn][:method_churn].inspect}"
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
require 'sexp_processor'
|
3
|
+
require 'ruby_parser'
|
4
|
+
require 'json'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'lib/churn/source_control'
|
7
|
+
require 'lib/churn/git_analyzer'
|
8
|
+
require 'lib/churn/svn_analyzer'
|
9
|
+
require 'lib/churn/location_mapping'
|
10
|
+
require 'lib/churn/churn_history'
|
11
|
+
|
12
|
+
module Churn
|
13
|
+
|
14
|
+
class ChurnCalculator
|
15
|
+
|
16
|
+
def initialize(options={})
|
17
|
+
start_date = options.fetch(:start_date) { '3 months ago' }
|
18
|
+
@minimum_churn_count = options.fetch(:minimum_churn_count) { 5 }
|
19
|
+
puts start_date
|
20
|
+
if self.class.git?
|
21
|
+
@source_control = GitAnalyzer.new(start_date)
|
22
|
+
elsif File.exist?(".svn")
|
23
|
+
@source_control = SvnAnalyzer.new(start_date)
|
24
|
+
else
|
25
|
+
raise "Churning requires a subversion or git repo"
|
26
|
+
end
|
27
|
+
@revision_changes = {}
|
28
|
+
@method_changes = {}
|
29
|
+
@class_changes = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def report
|
33
|
+
self.emit
|
34
|
+
self.analyze
|
35
|
+
self.to_h
|
36
|
+
end
|
37
|
+
|
38
|
+
def emit
|
39
|
+
@changes = parse_log_for_changes.reject {|file, change_count| change_count < @minimum_churn_count}
|
40
|
+
@revisions = parse_log_for_revision_changes
|
41
|
+
end
|
42
|
+
|
43
|
+
def analyze
|
44
|
+
@changes = @changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
45
|
+
@changes = @changes.map {|file_path, times_changed| {:file_path => file_path, :times_changed => times_changed }}
|
46
|
+
|
47
|
+
calculate_revision_changes
|
48
|
+
|
49
|
+
@method_changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
50
|
+
@method_changes = @method_changes.map {|method, times_changed| {'method' => method, 'times_changed' => times_changed }}
|
51
|
+
@class_changes.to_a.sort {|x,y| y[1] <=> x[1]}
|
52
|
+
@class_changes = @class_changes.map {|klass, times_changed| {'klass' => klass, 'times_changed' => times_changed }}
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
hash = {:churn => {:changes => @changes}}
|
57
|
+
hash[:churn][:method_churn] = @method_changes
|
58
|
+
hash[:churn][:class_churn] = @class_changes
|
59
|
+
#detail the most recent changes made this revision
|
60
|
+
if @revision_changes[@revisions.first]
|
61
|
+
changes = @revision_changes[@revisions.first]
|
62
|
+
hash[:churn][:changed_files] = changes[:files]
|
63
|
+
hash[:churn][:changed_classes] = changes[:classes]
|
64
|
+
hash[:churn][:changed_methods] = changes[:methods]
|
65
|
+
end
|
66
|
+
#TODO crappy place to do this but save hash to revision file but while entirely under metric_fu only choice
|
67
|
+
revision = @revisions.first
|
68
|
+
ChurnHistory.store_revision_history(revision, hash)
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def self.git?
|
75
|
+
system("git branch")
|
76
|
+
end
|
77
|
+
|
78
|
+
def calculate_revision_changes
|
79
|
+
@revisions.each do |revision|
|
80
|
+
if revision == @revisions.first
|
81
|
+
#can't iterate through all the changes and tally them up
|
82
|
+
#it only has the current files not the files at the time of the revision
|
83
|
+
#parsing requires the files
|
84
|
+
changed_files, changed_classes, changed_methods = calculate_revision_data(revision)
|
85
|
+
else
|
86
|
+
changed_files, changed_classes, changed_methods = ChurnHistory.load_revision_data(revision)
|
87
|
+
end
|
88
|
+
calculate_changes!(changed_methods, @method_changes) if changed_methods
|
89
|
+
calculate_changes!(changed_classes, @class_changes) if changed_classes
|
90
|
+
|
91
|
+
@revision_changes[revision] = { :files => changed_files, :classes => changed_classes, :methods => changed_methods }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def calculate_revision_data(revision)
|
96
|
+
changed_files = parse_logs_for_updated_files(revision, @revisions)
|
97
|
+
|
98
|
+
changed_classes = []
|
99
|
+
changed_methods = []
|
100
|
+
changed_files.each do |file|
|
101
|
+
classes, methods = get_changes(file)
|
102
|
+
changed_classes += classes
|
103
|
+
changed_methods += methods
|
104
|
+
end
|
105
|
+
changed_files = changed_files.map { |file, lines| file }
|
106
|
+
[changed_files, changed_classes, changed_methods]
|
107
|
+
end
|
108
|
+
|
109
|
+
def calculate_changes!(changed, total_changes)
|
110
|
+
if changed
|
111
|
+
changed.each do |change|
|
112
|
+
total_changes.include?(change) ? total_changes[change] = total_changes[change]+1 : total_changes[change] = 1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
total_changes
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_changes(change)
|
119
|
+
begin
|
120
|
+
file = change.first
|
121
|
+
breakdown = LocationMapping.new
|
122
|
+
breakdown.get_info(file)
|
123
|
+
changes = change.last
|
124
|
+
classes = changes_for_type(changes, breakdown, :classes)
|
125
|
+
methods = changes_for_type(changes, breakdown, :methods)
|
126
|
+
#todo move to method
|
127
|
+
classes = classes.map{ |klass| {'file' => file, 'klass' => klass} }
|
128
|
+
methods = methods.map{ |method| {'file' => file, 'klass' => get_klass_for(method), 'method' => method} }
|
129
|
+
[classes, methods]
|
130
|
+
rescue => error
|
131
|
+
[[],[]]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_klass_for(method)
|
136
|
+
method.gsub(/(#|\.).*/,'')
|
137
|
+
end
|
138
|
+
|
139
|
+
def changes_for_type(changes, breakdown, type)
|
140
|
+
item_collection = if type == :classes
|
141
|
+
breakdown.klasses_collection
|
142
|
+
elsif type == :methods
|
143
|
+
breakdown.methods_collection
|
144
|
+
end
|
145
|
+
changed_items = []
|
146
|
+
item_collection.each_pair do |item, item_lines|
|
147
|
+
item_lines = item_lines[0].to_a
|
148
|
+
changes.each do |change_range|
|
149
|
+
item_lines.each do |line|
|
150
|
+
changed_items << item if change_range.include?(line) && !changed_items.include?(item)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
changed_items
|
155
|
+
end
|
156
|
+
|
157
|
+
def parse_log_for_changes
|
158
|
+
changes = {}
|
159
|
+
|
160
|
+
logs = @source_control.get_logs
|
161
|
+
logs.each do |line|
|
162
|
+
changes[line] ? changes[line] += 1 : changes[line] = 1
|
163
|
+
end
|
164
|
+
changes
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_log_for_revision_changes
|
168
|
+
@source_control.get_revisions
|
169
|
+
end
|
170
|
+
|
171
|
+
def parse_logs_for_updated_files(revision, revisions)
|
172
|
+
updated = {}
|
173
|
+
recent_file = nil
|
174
|
+
|
175
|
+
#SVN doesn't support this
|
176
|
+
return updated unless @source_control.respond_to?(:get_updated_files_from_log)
|
177
|
+
logs = @source_control.get_updated_files_from_log(revision, revisions)
|
178
|
+
logs.each do |line|
|
179
|
+
if line.match(/^---/) || line.match(/^\+\+\+/)
|
180
|
+
line = line.gsub(/^--- /,'').gsub(/^\+\+\+ /,'').gsub(/^a\//,'').gsub(/^b\//,'')
|
181
|
+
unless updated.include?(line)
|
182
|
+
updated[line] = []
|
183
|
+
end
|
184
|
+
recent_file = line
|
185
|
+
elsif line.match(/^@@/)
|
186
|
+
#TODO cleanup / refactor
|
187
|
+
#puts "#{recent_file}: #{line}"
|
188
|
+
removed = line.match(/-[0-9]+/)
|
189
|
+
removed_length = line.match(/-[0-9]+,[0-9]+/)
|
190
|
+
removed = removed.to_s.gsub(/-/,'')
|
191
|
+
removed_length = removed_length.to_s.gsub(/.*,/,'')
|
192
|
+
added = line.match(/\+[0-9]+/)
|
193
|
+
added_length = line.match(/\+[0-9]+,[0-9]+/)
|
194
|
+
added = added.to_s.gsub(/\+/,'')
|
195
|
+
added_length = added_length.to_s.gsub(/.*,/,'')
|
196
|
+
removed_range = if removed_length && removed_length!=''
|
197
|
+
(removed.to_i..(removed.to_i+removed_length.to_i))
|
198
|
+
else
|
199
|
+
(removed.to_i..removed.to_i)
|
200
|
+
end
|
201
|
+
added_range = if added_length && added_length!=''
|
202
|
+
(added.to_i..(added.to_i+added_length.to_i))
|
203
|
+
else
|
204
|
+
(added.to_i..added.to_i)
|
205
|
+
end
|
206
|
+
updated[recent_file] << removed_range
|
207
|
+
updated[recent_file] << added_range
|
208
|
+
else
|
209
|
+
raise "git diff lines that don't match the two patterns aren't expected"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
updated
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'construct'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
9
|
+
require 'churn/churn_calculator'
|
10
|
+
Mocha::Configuration.prevent(:stubbing_non_existent_method)
|
11
|
+
|
12
|
+
class Test::Unit::TestCase
|
13
|
+
include Construct::Helpers
|
14
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'construct'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
9
|
+
require 'churn/churn_calculator'
|
10
|
+
Mocha::Configuration.prevent(:stubbing_non_existent_method)
|
11
|
+
|
12
|
+
class Test::Unit::TestCase
|
13
|
+
include Construct::Helpers
|
14
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.expand_path('../test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ChurnCalculatorTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
should "use minimum churn count" do
|
6
|
+
within_construct do |container|
|
7
|
+
Churn::ChurnCalculator.stubs(:git?).returns(true)
|
8
|
+
churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
|
9
|
+
|
10
|
+
churn.stubs(:parse_log_for_changes).returns([['file.rb', 4],['less.rb',1]])
|
11
|
+
churn.stubs(:parse_log_for_revision_changes).returns(['revision'])
|
12
|
+
churn.stubs(:analyze)
|
13
|
+
report = churn.report(false)
|
14
|
+
assert_equal 1, report[:churn][:changes].length
|
15
|
+
assert_equal ["file.rb", 4], report[:churn][:changes].first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
should "analize sorts changes" do
|
20
|
+
within_construct do |container|
|
21
|
+
Churn::ChurnCalculator.stubs(:git?).returns(true)
|
22
|
+
churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
|
23
|
+
|
24
|
+
churn.stubs(:parse_log_for_changes).returns([['file.rb', 4],['most.rb', 9],['less.rb',1]])
|
25
|
+
churn.stubs(:parse_log_for_revision_changes).returns(['revision'])
|
26
|
+
report = churn.report(false)
|
27
|
+
assert_equal 2, report[:churn][:changes].length
|
28
|
+
top = {:file_path => "most.rb", :times_changed => 9}
|
29
|
+
assert_equal top, report[:churn][:changes].first
|
30
|
+
bottom = {:file_path => "file.rb", :times_changed => 4}
|
31
|
+
assert_equal bottom, report[:churn][:changes].last
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
should "have correct changed_files data" do
|
36
|
+
within_construct do |container|
|
37
|
+
Churn::ChurnCalculator.stubs(:git?).returns(true)
|
38
|
+
churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
|
39
|
+
|
40
|
+
churn.stubs(:parse_log_for_changes).returns([['less.rb',1]])
|
41
|
+
churn.stubs(:parse_log_for_revision_changes).returns(['first'])
|
42
|
+
churn.stubs(:parse_logs_for_updated_files).returns(['fake_file.rb'])
|
43
|
+
report = churn.report(false)
|
44
|
+
assert_equal ["fake_file.rb"], report[:churn][:changed_files]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
should "have correct changed classes and methods data" do
|
49
|
+
within_construct do |container|
|
50
|
+
Churn::ChurnCalculator.stubs(:git?).returns(true)
|
51
|
+
churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
|
52
|
+
|
53
|
+
churn.stubs(:parse_log_for_changes).returns([['less.rb',1]])
|
54
|
+
churn.stubs(:parse_log_for_revision_changes).returns(['first'])
|
55
|
+
churn.stubs(:parse_logs_for_updated_files).returns(['fake_file.rb'])
|
56
|
+
klasses = [{"klass"=>"LocationMapping", "file"=>"lib/churn/location_mapping.rb"}]
|
57
|
+
methods = [{"klass"=>"LocationMapping", "method"=>"LocationMapping#process_class", "file"=>"lib/churn/location_mapping.rb"}]
|
58
|
+
churn.stubs(:get_changes).returns([klasses,methods])
|
59
|
+
report = churn.report(false)
|
60
|
+
assert_equal [{"klass"=>"LocationMapping", "method"=>"LocationMapping#process_class", "file"=>"lib/churn/location_mapping.rb"}], report[:churn][:changed_methods]
|
61
|
+
assert_equal [{"klass"=>"LocationMapping", "file"=>"lib/churn/location_mapping.rb"}], report[:churn][:changed_classes]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
should "have correct churn method and classes at 1 change" do
|
66
|
+
within_construct do |container|
|
67
|
+
Churn::ChurnCalculator.stubs(:git?).returns(true)
|
68
|
+
churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
|
69
|
+
|
70
|
+
churn.stubs(:parse_log_for_changes).returns([['less.rb',1]])
|
71
|
+
churn.stubs(:parse_log_for_revision_changes).returns(['first'])
|
72
|
+
churn.stubs(:parse_logs_for_updated_files).returns(['fake_file.rb'])
|
73
|
+
klasses = [{"klass"=>"LocationMapping", "file"=>"lib/churn/location_mapping.rb"}]
|
74
|
+
methods = [{"klass"=>"LocationMapping", "method"=>"LocationMapping#process_class", "file"=>"lib/churn/location_mapping.rb"}]
|
75
|
+
churn.stubs(:get_changes).returns([klasses,methods])
|
76
|
+
report = churn.report(false)
|
77
|
+
assert_equal [{"method"=>{"klass"=>"LocationMapping", "method"=>"LocationMapping#process_class", "file"=>"lib/churn/location_mapping.rb"}, "times_changed"=>1}], report[:churn][:method_churn]
|
78
|
+
assert_equal [{"klass"=>{"klass"=>"LocationMapping", "file"=>"lib/churn/location_mapping.rb"}, "times_changed"=>1}], report[:churn][:class_churn]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path('../test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ChurnHistoryTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
should "store results" do
|
6
|
+
within_construct do |container|
|
7
|
+
Churn::ChurnHistory.store_revision_history('aaa','data')
|
8
|
+
assert File.exists?('tmp/aaa.json')
|
9
|
+
data = File.read('tmp/aaa.json')
|
10
|
+
assert data.match(/data/)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
should "restores results" do
|
15
|
+
within_construct do |container|
|
16
|
+
container.file('tmp/aaa.json', '{"churn":{"changes":[{"file_path":".gitignore","times_changed":2},{"file_path":"lib\/churn.rb","times_changed":2},{"file_path":"Rakefile","times_changed":2},{"file_path":"README.rdoc","times_changed":2},{"file_path":"lib\/churn\/source_control.rb","times_changed":1},{"file_path":"lib\/churn\/svn_analyzer.rb","times_changed":1},{"file_path":"lib\/tasks\/churn_tasks.rb","times_changed":1},{"file_path":"LICENSE","times_changed":1},{"file_path":"test\/churn_test.rb","times_changed":1},{"file_path":"lib\/churn\/locationmapping.rb","times_changed":1},{"file_path":"lib\/churn\/git_analyzer.rb","times_changed":1},{"file_path":".document","times_changed":1},{"file_path":"test\/test_helper.rb","times_changed":1},{"file_path":"lib\/churn\/churn_calculator.rb","times_changed":1}],"method_churn":[],"changed_files":[".gitignore","lib\/churn\/source_control.rb","lib\/tasks\/churn_tasks.rb","lib\/churn\/svn_analyzer.rb","Rakefile","README.rdoc","lib\/churn\/locationmapping.rb","lib\/churn\/git_analyzer.rb","\/dev\/null","lib\/churn\/churn_calculator.rb","lib\/churn.rb"],"class_churn":[],"changed_classes":[{"klass":"ChurnTest","file":"test\/churn_test.rb"},{"klass":"ChurnCalculator","file":"lib\/churn\/churn_calculator.rb"}],"changed_methods":[{"klass":"","method":"#report_churn","file":"lib\/tasks\/churn_tasks.rb"}]}}')
|
17
|
+
changed_files, changed_classes, changed_methods = Churn::ChurnHistory.load_revision_data('aaa')
|
18
|
+
assert changed_files.include?("lib/churn/source_control.rb")
|
19
|
+
assert_equal 2, changed_classes.length
|
20
|
+
assert_equal 1, changed_methods.length
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path('../test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class GitAnalyzerTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
should "parses logs correctly" do
|
6
|
+
git_analyzer = Churn::GitAnalyzer.new
|
7
|
+
revision = 'first'
|
8
|
+
revisions = ['first']
|
9
|
+
lines = ["--- a/lib/churn/churn_calculator.rb", "+++ b/lib/churn/churn_calculator.rb", "@@ -18,0 +19 @@ module Churn"]
|
10
|
+
git_analyzer.stubs(:get_updated_files_from_log).returns(lines)
|
11
|
+
updated = git_analyzer.get_updated_files_change_info(revision, revisions)
|
12
|
+
expected_hash = {"lib/churn/churn_calculator.rb"=>[18..18, 19..19]}
|
13
|
+
assert_equal = updated
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path('../test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class LocationMappingTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
#todo unfortunately it looks like ruby parser can't handle construct tmp dirs
|
6
|
+
#<Pathname:/private/var/folders/gl/glhHkYYSGgG5nb6+4OG0yU+++TI/-Tmp-/construct_container-56784-851001101/fake_class.rb>
|
7
|
+
#(rdb:1) p locationmapping.get_info(file.to_s)
|
8
|
+
#RegexpError Exception: invalid regular expression; there's no previous pattern, to which '+' would define cardinality at 2: /^+++/
|
9
|
+
|
10
|
+
should "location_mapping gets correct classes info" do
|
11
|
+
file = 'test/data/churn_calculator.rb'
|
12
|
+
locationmapping = Churn::LocationMapping.new
|
13
|
+
locationmapping.get_info(file.to_s)
|
14
|
+
klass_hash = {"ChurnCalculator"=>[14..215]}
|
15
|
+
assert_equal klass_hash, locationmapping.klasses_collection
|
16
|
+
end
|
17
|
+
|
18
|
+
should "location_mapping gets correct methods info" do
|
19
|
+
file = 'test/data/churn_calculator.rb'
|
20
|
+
locationmapping = Churn::LocationMapping.new
|
21
|
+
locationmapping.get_info(file.to_s)
|
22
|
+
methods_hash = {"ChurnCalculator#report"=>[32..36], "ChurnCalculator#emit"=>[38..41], "ChurnCalculator#changes_for_type"=>[139..155], "ChurnCalculator#get_klass_for"=>[135..137], "ChurnCalculator#calculate_changes!"=>[109..116], "ChurnCalculator#analyze"=>[43..53], "ChurnCalculator#calculate_revision_data"=>[95..107], "ChurnCalculator#calculate_revision_changes"=>[78..93], "ChurnCalculator#parse_logs_for_updated_files"=>[171..213], "ChurnCalculator#to_h"=>[55..70], "ChurnCalculator#parse_log_for_revision_changes"=>[167..169], "ChurnCalculator#get_changes"=>[118..133], "ChurnCalculator#parse_log_for_changes"=>[157..165], "ChurnCalculator#initialize"=>[16..30]}
|
23
|
+
assert_equal methods_hash, locationmapping.methods_collection
|
24
|
+
end
|
25
|
+
|
26
|
+
should "location_mapping gets correct classes info for test helper files" do
|
27
|
+
file = 'test/data/test_helper.rb'
|
28
|
+
locationmapping = Churn::LocationMapping.new
|
29
|
+
locationmapping.get_info(file.to_s)
|
30
|
+
klass_hash = {"TestCase"=>[12..14]}
|
31
|
+
assert_equal klass_hash, locationmapping.klasses_collection
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: churn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Mayer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-08 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: "High method and class churn has been shown to have increased bug and error rates. This gem helps you know what is changing a lot so you can do additional testing, code review, or refactoring to try to tame the volatile code. "
|
26
|
+
email: Danmayer@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
files:
|
35
|
+
- .document
|
36
|
+
- .gitignore
|
37
|
+
- LICENSE
|
38
|
+
- README.rdoc
|
39
|
+
- Rakefile
|
40
|
+
- VERSION
|
41
|
+
- lib/churn.rb
|
42
|
+
- lib/churn/churn_calculator.rb
|
43
|
+
- lib/churn/churn_history.rb
|
44
|
+
- lib/churn/git_analyzer.rb
|
45
|
+
- lib/churn/location_mapping.rb
|
46
|
+
- lib/churn/source_control.rb
|
47
|
+
- lib/churn/svn_analyzer.rb
|
48
|
+
- lib/tasks/churn_tasks.rb
|
49
|
+
- test/data/churn_calculator.rb
|
50
|
+
- test/data/test_helper.rb
|
51
|
+
- test/test_helper.rb
|
52
|
+
- test/unit/churn_calculator_test.rb
|
53
|
+
- test/unit/churn_history_test.rb
|
54
|
+
- test/unit/git_analyzer_test.rb
|
55
|
+
- test/unit/location_mapping_test.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/danmayer/churn
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --charset=UTF-8
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.3.5
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Providing additional churn metrics over the original metric_fu churn
|
84
|
+
test_files:
|
85
|
+
- test/data/churn_calculator.rb
|
86
|
+
- test/data/test_helper.rb
|
87
|
+
- test/test_helper.rb
|
88
|
+
- test/unit/churn_calculator_test.rb
|
89
|
+
- test/unit/churn_history_test.rb
|
90
|
+
- test/unit/git_analyzer_test.rb
|
91
|
+
- test/unit/location_mapping_test.rb
|