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.
@@ -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
@@ -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
+