rferraz-churn 0.0.16
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/Gemfile +4 -0
- data/Gemfile.lock +54 -0
- data/LICENSE +20 -0
- data/README.rdoc +123 -0
- data/Rakefile +96 -0
- data/VERSION +1 -0
- data/bin/churn +32 -0
- data/churn.gemspec +123 -0
- data/lib/churn/bzr_analyzer.rb +31 -0
- data/lib/churn/churn_calculator.rb +252 -0
- data/lib/churn/churn_history.rb +33 -0
- data/lib/churn/git_analyzer.rb +27 -0
- data/lib/churn/hg_analyzer.rb +31 -0
- data/lib/churn/location_mapping.rb +52 -0
- data/lib/churn/source_control.rb +66 -0
- data/lib/churn/svn_analyzer.rb +23 -0
- data/lib/churn.rb +1 -0
- data/lib/tasks/churn_tasks.rb +11 -0
- data/man/churn.1 +150 -0
- data/man/churn.html +542 -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/bzr_analyzer_test.rb +65 -0
- data/test/unit/churn_calculator_test.rb +90 -0
- data/test/unit/churn_history_test.rb +24 -0
- data/test/unit/git_analyzer_test.rb +17 -0
- data/test/unit/hg_analyzer_test.rb +66 -0
- data/test/unit/location_mapping_test.rb +35 -0
- metadata +389 -0
@@ -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,65 @@
|
|
1
|
+
require File.expand_path('../test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class BzrAnalyzerTest < Test::Unit::TestCase
|
4
|
+
context "BzrAnalyzer#get_logs" do
|
5
|
+
should "return a list of changed files" do
|
6
|
+
bzr_analyzer = Churn::BzrAnalyzer.new
|
7
|
+
bzr_analyzer.expects(:`).with('bzr log -v --short ').returns(" 1947 Adam Walters 2010-01-16\n Second commit with 3 files now.\n M file1.rb\n M file2.rb\n M file3.rb\n\n 1946 Adam Walters 2010-01-16\n First commit\n A file1.rb\n")
|
8
|
+
assert_equal ["file1.rb", "file2.rb", "file3.rb", "file1.rb"], bzr_analyzer.get_logs
|
9
|
+
end
|
10
|
+
|
11
|
+
should "scope the changed files to an optional date range" do
|
12
|
+
bzr_analyzer = Churn::BzrAnalyzer.new("1/16/2010")
|
13
|
+
bzr_analyzer.expects(:`).with('bzr log -v --short -r 2010-01-16..').returns(" 1947 Adam Walters 2010-01-16\n Second commit with 3 files now.\n M file1.rb\n M file2.rb\n M file3.rb\n\n 1946 Adam Walters 2010-01-16\n First commit\n A file1.rb\n")
|
14
|
+
assert_equal ["file1.rb", "file2.rb", "file3.rb", "file1.rb"], bzr_analyzer.get_logs
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "BzrAnalyzer#get_revisions" do
|
19
|
+
should "return a list of changeset ids" do
|
20
|
+
bzr_analyzer = Churn::BzrAnalyzer.new
|
21
|
+
bzr_analyzer.expects(:`).with('bzr log --line ').returns("1947: Adam Walters 2010-01-16 Second commit with 3 files now.\n1946: Adam Walters 2010-01-16 First commit\n")
|
22
|
+
assert_equal ["1947", "1946"], bzr_analyzer.get_revisions
|
23
|
+
end
|
24
|
+
|
25
|
+
should "scope the changesets to an optional date range" do
|
26
|
+
bzr_analyzer = Churn::BzrAnalyzer.new("1/16/2010")
|
27
|
+
bzr_analyzer.expects(:`).with('bzr log --line -r 2010-01-16..').returns("1947: Adam Walters 2010-01-16 Second commit with 3 files now.\n1946: Adam Walters 2010-01-16 First commit\n")
|
28
|
+
assert_equal ["1947", "1946"], bzr_analyzer.get_revisions
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "BzrAnalyzer#get_updated_files_from_log(revision, revisions)" do
|
33
|
+
should "return a list of modified files and the change hunks (chunks)" do
|
34
|
+
bzr_analyzer = Churn::BzrAnalyzer.new
|
35
|
+
bzr_analyzer.expects(:`).with('bzr diff -r 1946..1947').returns("=== modified file 'a/file1.rb'\n--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600\n@@ -1,3 +0,0 @@\n-First\n-Adding sample data\n-Third line\ndiff -r 1947 -r 1946 file2.rb\n=== modified file 'a/file2.rb'\n--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,7 +0,0 @@\n-This is the second file.\n-\n-Little more data\n-\n-def cool_method\n- \"hello\"\n-end\ndiff -r 1947 -r 1946 file3.rb\n--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,5 +0,0 @@\n-Third file here.\n-\n-def another_method\n- \"foo\"\n-end\n")
|
36
|
+
assert_equal ["--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600", "@@ -1,3 +0,0 @@", "--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,7 +0,0 @@", "--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,5 +0,0 @@"], bzr_analyzer.get_updated_files_from_log("1947", ["1947", "1946"])
|
37
|
+
end
|
38
|
+
|
39
|
+
should "return an empty array if it's the final revision" do
|
40
|
+
bzr_analyzer = Churn::BzrAnalyzer.new
|
41
|
+
assert_equal [], bzr_analyzer.get_updated_files_from_log("1946", ["1947", "1946"])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "BzrAnalyzer#get_updated_files_change_info(revision, revisions)" do
|
46
|
+
setup do
|
47
|
+
@bzr_analyzer = Churn::BzrAnalyzer.new
|
48
|
+
end
|
49
|
+
|
50
|
+
should "return all modified files with their line differences" do
|
51
|
+
@bzr_analyzer.expects(:get_updated_files_from_log).with("1947", ["1947", "1946"]).returns(["--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600", "@@ -1,3 +0,0 @@", "--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,7 +0,0 @@", "--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,5 +0,0 @@"])
|
52
|
+
assert_equal({"/dev/null" => [1..8, 0..0, 1..6, 0..0], "file3.rb" => [], "file1.rb" => [], "file2.rb" => [], "file1.rb" => [1..4, 0..0]}, @bzr_analyzer.get_updated_files_change_info("1947", ["1947", "1946"]))
|
53
|
+
end
|
54
|
+
|
55
|
+
should "raise an error if it encounters a line it cannot parse" do
|
56
|
+
@bzr_analyzer.expects(:get_updated_files_from_log).with("1947", ["1947", "1946"]).returns(["foo"])
|
57
|
+
assert_raise RuntimeError do
|
58
|
+
@bzr_analyzer.stubs(:puts) # supress output from raised error
|
59
|
+
@bzr_analyzer.get_updated_files_change_info("1947", ["1947", "1946"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,90 @@
|
|
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
|
+
should "initialize a churn calculator for hg repositories" do
|
83
|
+
Churn::ChurnCalculator.stubs(:git?).returns(false)
|
84
|
+
Churn::ChurnCalculator.stubs(:system).with('hg branch').returns(true)
|
85
|
+
churn = Churn::ChurnCalculator.new({:minimum_churn_count => 3})
|
86
|
+
assert churn.instance_variable_get(:@source_control).is_a?(Churn::HgAnalyzer)
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
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,66 @@
|
|
1
|
+
require File.expand_path('../test_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class HgAnalyzerTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "HgAnalyzer#get_logs" do
|
6
|
+
should "return a list of changed files" do
|
7
|
+
hg_analyzer = Churn::HgAnalyzer.new
|
8
|
+
hg_analyzer.expects(:`).with('hg log -v').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nfiles: file1.rb file2.rb file3.rb\ndescription:\nSecond commit with 3 files now.\nLong commit\n\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nfiles: file1.rb\ndescription:\nFirst commit\n\n\n")
|
9
|
+
assert_equal ["file1.rb", "file2.rb", "file3.rb", "file1.rb"], hg_analyzer.get_logs
|
10
|
+
end
|
11
|
+
|
12
|
+
should "scope the changed files to an optional date range" do
|
13
|
+
hg_analyzer = Churn::HgAnalyzer.new("1/16/2010")
|
14
|
+
hg_analyzer.expects(:`).with('hg log -v -d "> 2010-01-16"').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nfiles: file1.rb file2.rb file3.rb\ndescription:\nSecond commit with 3 files now.\nLong commit\n\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nfiles: file1.rb\ndescription:\nFirst commit\n\n\n")
|
15
|
+
assert_equal ["file1.rb", "file2.rb", "file3.rb", "file1.rb"], hg_analyzer.get_logs
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "HgAnalyzer#get_revisions" do
|
20
|
+
should "return a list of changeset ids" do
|
21
|
+
hg_analyzer = Churn::HgAnalyzer.new
|
22
|
+
hg_analyzer.expects(:`).with('hg log').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nsummary: Second commit with 3 files now.\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nsummary: First commit\n\n")
|
23
|
+
assert_equal ["4760c1d7cd40", "3cb77114f02a"], hg_analyzer.get_revisions
|
24
|
+
end
|
25
|
+
|
26
|
+
should "scope the changesets to an optional date range" do
|
27
|
+
hg_analyzer = Churn::HgAnalyzer.new("1/16/2010")
|
28
|
+
hg_analyzer.expects(:`).with('hg log -d "> 2010-01-16"').returns("changeset: 1:4760c1d7cd40\ntag: tip\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:21:28 2010 -0600\nsummary: Second commit with 3 files now.\n\nchangeset: 0:3cb77114f02a\nuser: Adam Walters <awalters@obtiva.com>\ndate: Sat Jan 16 14:19:32 2010 -0600\nsummary: First commit\n\n")
|
29
|
+
assert_equal ["4760c1d7cd40", "3cb77114f02a"], hg_analyzer.get_revisions
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "HgAnalyzer#get_updated_files_from_log(revision, revisions)" do
|
34
|
+
should "return a list of modified files and the change hunks (chunks)" do
|
35
|
+
hg_analyzer = Churn::HgAnalyzer.new
|
36
|
+
hg_analyzer.expects(:`).with('hg diff -r 4760c1d7cd40:3cb77114f02a -U 0').returns("diff -r 4760c1d7cd40 -r 3cb77114f02a file1.rb\n--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600\n@@ -1,3 +0,0 @@\n-First\n-Adding sample data\n-Third line\ndiff -r 4760c1d7cd40 -r 3cb77114f02a file2.rb\n--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,7 +0,0 @@\n-This is the second file.\n-\n-Little more data\n-\n-def cool_method\n- \"hello\"\n-end\ndiff -r 4760c1d7cd40 -r 3cb77114f02a file3.rb\n--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,5 +0,0 @@\n-Third file here.\n-\n-def another_method\n- \"foo\"\n-end\n")
|
37
|
+
assert_equal ["--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600", "@@ -1,3 +0,0 @@", "--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,7 +0,0 @@", "--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,5 +0,0 @@"], hg_analyzer.get_updated_files_from_log("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"])
|
38
|
+
end
|
39
|
+
|
40
|
+
should "return an empty array if it's the final revision" do
|
41
|
+
hg_analyzer = Churn::HgAnalyzer.new
|
42
|
+
assert_equal [], hg_analyzer.get_updated_files_from_log("3cb77114f02a", ["4760c1d7cd40", "3cb77114f02a"])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "HgAnalyzer#get_updated_files_change_info(revision, revisions)" do
|
47
|
+
setup do
|
48
|
+
@hg_analyzer = Churn::HgAnalyzer.new
|
49
|
+
end
|
50
|
+
|
51
|
+
should "return all modified files with their line differences" do
|
52
|
+
@hg_analyzer.expects(:get_updated_files_from_log).with("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"]).returns(["--- a/file1.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ b/file1.rb\tSat Jan 16 14:19:32 2010 -0600", "@@ -1,3 +0,0 @@", "--- a/file2.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,7 +0,0 @@", "--- a/file3.rb\tSat Jan 16 14:21:28 2010 -0600", "+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000", "@@ -1,5 +0,0 @@"])
|
53
|
+
assert_equal({"/dev/null" => [1..8, 0..0, 1..6, 0..0], "file3.rb" => [], "file1.rb" => [], "file2.rb" => [], "file1.rb" => [1..4, 0..0]}, @hg_analyzer.get_updated_files_change_info("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"]))
|
54
|
+
end
|
55
|
+
|
56
|
+
should "raise an error if it encounters a line it cannot parse" do
|
57
|
+
@hg_analyzer.expects(:get_updated_files_from_log).with("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"]).returns(["foo"])
|
58
|
+
assert_raise RuntimeError do
|
59
|
+
@hg_analyzer.stubs(:puts) # supress output from raised error
|
60
|
+
@hg_analyzer.get_updated_files_change_info("4760c1d7cd40", ["4760c1d7cd40", "3cb77114f02a"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
@@ -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
|
+
|