tipster 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/lib/tipster.rb +21 -0
- data/lib/tipster/commands/git/files_changed.rb +6 -0
- data/lib/tipster/commands/git/latest_commit.rb +6 -0
- data/lib/tipster/commands/git/repository_context.rb +11 -0
- data/lib/tipster/files/file.rb +10 -0
- data/lib/tipster/files/file_info.rb +9 -0
- data/lib/tipster/heuristics/code_churn_heuristic.rb +36 -0
- data/lib/tipster/heuristics/code_ratio_heuristic.rb +39 -0
- data/lib/tipster/history/commit_history.rb +9 -0
- data/lib/tipster/history/commit_history_context.rb +24 -0
- data/lib/tipster/presenters/code_churn_presenter.rb +39 -0
- data/lib/tipster/presenters/code_ratio_presenter.rb +39 -0
- data/lib/tipster/presenters/heuristic_status.rb +10 -0
- data/lib/tipster/reports/html_report.rb +157 -0
- data/lib/tipster/run_tipster.rb +8 -0
- data/lib/tipster/version.rb +3 -0
- data/license.txt +14 -0
- data/readme.md +31 -0
- data/spec/commands/file_info_spec.rb +14 -0
- data/spec/commands/git/files_changed_spec.rb +31 -0
- data/spec/commands/git/latest_commit_spec.rb +23 -0
- data/spec/commands/git/repository_context_spec.rb +39 -0
- data/spec/files/file_spec.rb +13 -0
- data/spec/heuristics/code_churn_heuristic_spec.rb +47 -0
- data/spec/heuristics/code_ratio_heuristic_spec.rb +70 -0
- data/spec/history/commit_history_context_spec.rb +53 -0
- data/spec/presenters/code_churn_presenter_spec.rb +22 -0
- data/spec/presenters/code_ratio_presenter_spec.rb +23 -0
- data/spec/stub/fake_file.txt +10 -0
- data/tipster.gemspec +21 -0
- metadata +101 -0
data/.gitignore
ADDED
data/lib/tipster.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'tipster/reports/html_report'
|
2
|
+
|
3
|
+
class Tipster
|
4
|
+
def initialize(repository_root_path = '.')
|
5
|
+
Dir.chdir repository_root_path
|
6
|
+
end
|
7
|
+
def html_report(commit_id = 'HEAD', count = 1)
|
8
|
+
for i in 0..count - 1
|
9
|
+
next_commit_id = commit_id << "~#{i}"
|
10
|
+
in_root? ? HtmlReport.new(next_commit_id).display_in_browser : display_error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def in_root?
|
15
|
+
File.directory? '.git'
|
16
|
+
end
|
17
|
+
|
18
|
+
def display_error
|
19
|
+
puts "Error: Please provide the path to the root of the git repository."
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'files_changed'
|
2
|
+
require_relative 'latest_commit'
|
3
|
+
|
4
|
+
class RepositoryContext
|
5
|
+
def self.valid_repository?
|
6
|
+
LatestCommit.new.id.length == 40
|
7
|
+
end
|
8
|
+
def self.valid_commit_id?(commit_id)
|
9
|
+
FilesChanged.new(commit_id).list.length > 0
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative '../history/commit_history'
|
2
|
+
require_relative '../files/file_info'
|
3
|
+
require_relative '../files/file'
|
4
|
+
|
5
|
+
class CodeChurnHeuristic
|
6
|
+
|
7
|
+
attr_reader :files
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@files = Hash.new
|
11
|
+
@passing_ratio = 0.5
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply(changed_files)
|
15
|
+
changed_files.each { |file| process(file) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def process(file)
|
19
|
+
@files[file.file_name] = churn_ratio file
|
20
|
+
end
|
21
|
+
|
22
|
+
def churn_ratio(file)
|
23
|
+
total_lines = FileInfo.new(file.file_name).line_count
|
24
|
+
churned_lines = file.lines_modified
|
25
|
+
churned_lines.to_f / total_lines.to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
def pass?
|
29
|
+
@files.each do |key, value|
|
30
|
+
if value > @passing_ratio
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
true
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../history/commit_history'
|
2
|
+
|
3
|
+
class CodeRatioHeuristic
|
4
|
+
|
5
|
+
attr_reader :test_lines_of_code, :production_lines_of_code
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@passing_ratio = 0.0
|
9
|
+
@test_lines_of_code = 0
|
10
|
+
@production_lines_of_code = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(changed_files)
|
14
|
+
changed_files.each { |file| process(file) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def process(file)
|
18
|
+
|
19
|
+
if file.file_name =~ /(spec|test)/i
|
20
|
+
@test_lines_of_code += file.lines_modified
|
21
|
+
elsif file.file_name =~ /(cs|rb|java)$/
|
22
|
+
@production_lines_of_code += file.lines_modified
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def ratio
|
28
|
+
@test_lines_of_code.to_f / @production_lines_of_code.to_f
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_production_code?
|
32
|
+
@production_lines_of_code > 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def pass?
|
36
|
+
has_production_code? ? ratio > @passing_ratio : true
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'commit_history'
|
2
|
+
|
3
|
+
class CommitHistoryContext
|
4
|
+
|
5
|
+
attr_reader :change_list
|
6
|
+
|
7
|
+
def initialize(raw_output)
|
8
|
+
build_change_list(affected_files(raw_output))
|
9
|
+
end
|
10
|
+
|
11
|
+
def affected_files(output)
|
12
|
+
output.scan(/^\d.*$/)
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_change_list(changed_files)
|
16
|
+
@change_list = []
|
17
|
+
changed_files.each { |file| @change_list << commit_history(file) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def commit_history(line)
|
21
|
+
CommitHistory.new(line.split[0].to_i, line.split[1].to_i, line.split[2])
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../heuristics/code_churn_heuristic'
|
2
|
+
require_relative '../commands/git/files_changed'
|
3
|
+
require_relative '../commands/git/latest_commit'
|
4
|
+
require_relative '../presenters/heuristic_status'
|
5
|
+
require_relative '../history/commit_history_context'
|
6
|
+
|
7
|
+
class CodeChurnPresenter
|
8
|
+
|
9
|
+
def initialize(id = current_commit_id)
|
10
|
+
@commit_id = id
|
11
|
+
end
|
12
|
+
|
13
|
+
def pass?
|
14
|
+
code_ratio_heuristic = CodeChurnHeuristic.new
|
15
|
+
code_ratio_heuristic.apply change_list
|
16
|
+
code_ratio_heuristic.pass?
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_commit_id
|
20
|
+
LatestCommit.new.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def git_output
|
24
|
+
FilesChanged.new(@commit_id).list
|
25
|
+
end
|
26
|
+
|
27
|
+
def change_list
|
28
|
+
CommitHistoryContext.new(git_output).change_list
|
29
|
+
end
|
30
|
+
|
31
|
+
def status
|
32
|
+
safe = pass?
|
33
|
+
result = 'Warning: Your commit is risky due to high churn.'
|
34
|
+
if safe
|
35
|
+
result = 'Your commit does not include any high-churn files.'
|
36
|
+
end
|
37
|
+
HeuristicStatus.new(safe ,result)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../heuristics/code_ratio_heuristic'
|
2
|
+
require_relative '../history/commit_history_context'
|
3
|
+
require_relative '../commands/git/files_changed'
|
4
|
+
require_relative '../commands/git/latest_commit'
|
5
|
+
require_relative '../presenters/heuristic_status'
|
6
|
+
|
7
|
+
class CodeRatioPresenter
|
8
|
+
|
9
|
+
def initialize(id = current_commit_id)
|
10
|
+
@commit_id = id
|
11
|
+
end
|
12
|
+
|
13
|
+
def pass?
|
14
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
15
|
+
code_ratio_heuristic.apply(change_list)
|
16
|
+
code_ratio_heuristic.pass?
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_commit_id
|
20
|
+
LatestCommit.new.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def git_output
|
24
|
+
FilesChanged.new(@commit_id).list
|
25
|
+
end
|
26
|
+
|
27
|
+
def change_list
|
28
|
+
CommitHistoryContext.new(git_output).change_list
|
29
|
+
end
|
30
|
+
|
31
|
+
def status
|
32
|
+
safe = pass?
|
33
|
+
result = 'Warning: Your commit is risky due to a lack of tests. Consider adding more tests and amending your commit.'
|
34
|
+
if safe
|
35
|
+
result = 'Your commit has adequate tests and will not be flagged as risky.'
|
36
|
+
end
|
37
|
+
HeuristicStatus.new(safe ,result)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'launchy'
|
3
|
+
require_relative '../presenters/code_ratio_presenter'
|
4
|
+
require_relative '../presenters/code_churn_presenter'
|
5
|
+
require_relative '../files/file'
|
6
|
+
require_relative '../commands/git/repository_context'
|
7
|
+
|
8
|
+
class HtmlReport
|
9
|
+
|
10
|
+
def initialize(commit_id = nil)
|
11
|
+
@risk_report = File.temp 'risk_report.html'
|
12
|
+
generate html commit_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def html(commit_id)
|
16
|
+
|
17
|
+
is_valid_repository = RepositoryContext.valid_repository?
|
18
|
+
is_valid_commit = RepositoryContext.valid_commit_id? commit_id
|
19
|
+
|
20
|
+
if !is_valid_repository
|
21
|
+
return 'Invalid repository path.'
|
22
|
+
end
|
23
|
+
|
24
|
+
if !is_valid_commit
|
25
|
+
return 'Invalid commit ID'
|
26
|
+
end
|
27
|
+
|
28
|
+
code_ratio_presenter = CodeRatioPresenter.new commit_id
|
29
|
+
code_ratio_status = code_ratio_presenter.status
|
30
|
+
|
31
|
+
code_churn_presenter = CodeChurnPresenter.new commit_id
|
32
|
+
code_churn_status = code_churn_presenter.status
|
33
|
+
|
34
|
+
header << file_details(code_ratio_presenter.change_list) << code_ratio_details(code_ratio_status) << code_churn_details(code_churn_status) << footer
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate(source)
|
38
|
+
File.open(@risk_report, 'w') {|f| f.write(source) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def display_in_browser
|
42
|
+
Launchy.open('file:///' << @risk_report)
|
43
|
+
end
|
44
|
+
|
45
|
+
def file_details(change_list)
|
46
|
+
file_details = "
|
47
|
+
<h2>What files am I checking in?</h2>
|
48
|
+
<div class=\"commitDetails\">
|
49
|
+
<ul>"
|
50
|
+
|
51
|
+
change_list.each { |x| file_details << "<li>" << x.file_name << "</li>" }
|
52
|
+
|
53
|
+
file_details << "
|
54
|
+
</ul>
|
55
|
+
</div>
|
56
|
+
"
|
57
|
+
end
|
58
|
+
|
59
|
+
def code_ratio_details(code_ratio_status)
|
60
|
+
"
|
61
|
+
<h2>Is my code risky?</h2>
|
62
|
+
<div class=\"heuristicStatus #{code_ratio_status.status}\">
|
63
|
+
<ul>
|
64
|
+
<li class=\"result\">#{code_ratio_status.status}</li>
|
65
|
+
<li class=\"heuristicName\">Code Ratio</li>
|
66
|
+
<li class=\"description\">#{code_ratio_status.description}</li>
|
67
|
+
</ul>
|
68
|
+
</div>
|
69
|
+
"
|
70
|
+
end
|
71
|
+
|
72
|
+
def code_churn_details(code_churn_status)
|
73
|
+
"
|
74
|
+
<div class=\"heuristicStatus #{code_churn_status.status}\">
|
75
|
+
<ul>
|
76
|
+
<li class=\"result\">#{code_churn_status.status}</li>
|
77
|
+
<li class=\"heuristicName\">Code Churn</li>
|
78
|
+
<li class=\"description\">#{code_churn_status.description}</li>
|
79
|
+
</ul>
|
80
|
+
</div>
|
81
|
+
"
|
82
|
+
end
|
83
|
+
|
84
|
+
def header
|
85
|
+
"
|
86
|
+
<!DOCTYPE html>
|
87
|
+
<html lang=\"en\">
|
88
|
+
<head>
|
89
|
+
<meta charset=\"utf-8\" />
|
90
|
+
<title>Check-In Risk Report</title>
|
91
|
+
|
92
|
+
<style media=\"screen\" type=\"text/css\">
|
93
|
+
body {
|
94
|
+
font-family: Helvetica, \"Helvetica Neue\", Arial;
|
95
|
+
font-size: 16px;
|
96
|
+
color: #333;
|
97
|
+
}
|
98
|
+
|
99
|
+
h1 {
|
100
|
+
font-size: 20px;
|
101
|
+
}
|
102
|
+
|
103
|
+
h2 {
|
104
|
+
font-size: 16px;
|
105
|
+
}
|
106
|
+
|
107
|
+
.commitDetails {
|
108
|
+
font-size: 12px;
|
109
|
+
}
|
110
|
+
|
111
|
+
.heuristicStatus ul, .heuristicStatus li {
|
112
|
+
display: inline;
|
113
|
+
}
|
114
|
+
|
115
|
+
.heuristicStatus ul li {
|
116
|
+
display: block;
|
117
|
+
padding: 1px 5px 0px 5px;
|
118
|
+
float: left;
|
119
|
+
}
|
120
|
+
|
121
|
+
.Risky {
|
122
|
+
border-left: 5px solid #9e2b20;
|
123
|
+
height: 20px;
|
124
|
+
margin-bottom: 15px;
|
125
|
+
}
|
126
|
+
|
127
|
+
.Safe {
|
128
|
+
border-left: 5px solid #0af510;
|
129
|
+
height: 20px;
|
130
|
+
margin-bottom: 15px;
|
131
|
+
}
|
132
|
+
|
133
|
+
.heuristicName {
|
134
|
+
color: #555;
|
135
|
+
}
|
136
|
+
|
137
|
+
.result {
|
138
|
+
font-weight: bold;
|
139
|
+
}
|
140
|
+
|
141
|
+
.description {
|
142
|
+
color: #999;
|
143
|
+
}
|
144
|
+
</style>
|
145
|
+
|
146
|
+
</head>
|
147
|
+
<body>
|
148
|
+
<h1>Commit Risk Report</h1>
|
149
|
+
"
|
150
|
+
end
|
151
|
+
|
152
|
+
def footer
|
153
|
+
"
|
154
|
+
</body>
|
155
|
+
</html>"
|
156
|
+
end
|
157
|
+
end
|
data/license.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
Copyright (c) 2011 Ratcheting Project
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use,
|
6
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to
|
7
|
+
whom the Software is furnished to do so, subject to the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
10
|
+
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
12
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
13
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
14
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/readme.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
tipster
|
2
|
+
========
|
3
|
+
|
4
|
+
Tipster assesses the risk of a git commit by running various heuristics against what has changed.
|
5
|
+
|
6
|
+
Using tipster
|
7
|
+
-------------------
|
8
|
+
|
9
|
+
Getting started with tipster in 3 easy steps:
|
10
|
+
|
11
|
+
### Install ruby gem
|
12
|
+
|
13
|
+
gem install tipster
|
14
|
+
|
15
|
+
### Create ruby file
|
16
|
+
|
17
|
+
`number_of_commits` will tell tipster to build an HTML report for the current `commit_id` and the previous x commits.
|
18
|
+
|
19
|
+
require 'tipster'
|
20
|
+
Tipster.new(['/path/to/repository' = "."]).html_report(['commit_id' = HEAD], [number_of_commits = 1])
|
21
|
+
|
22
|
+
### Run!
|
23
|
+
|
24
|
+
ruby tipster.rb
|
25
|
+
|
26
|
+
Copyright
|
27
|
+
---------
|
28
|
+
|
29
|
+
Copyright (c) 2012 [Robert Greiner](http://creatingcode.com/quality).
|
30
|
+
|
31
|
+
See LICENSE for details.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/files/file_info'
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib', 'tipster'))
|
4
|
+
|
5
|
+
describe "Line count" do
|
6
|
+
it "should return the number of lines in a file" do
|
7
|
+
file_name = 'stub/fake_file.txt'
|
8
|
+
FileInfo.new(file_name).line_count.should be 10
|
9
|
+
end
|
10
|
+
it "should raise an error if the file does not exist" do
|
11
|
+
file_name = "./stub/does-not-exist.txt"
|
12
|
+
expect { FileInfo.new(file_name).line_count }.to raise_error(Errno::ENOENT)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../../lib/tipster/commands/git/files_changed'
|
3
|
+
|
4
|
+
describe "git show --numstat" do
|
5
|
+
it "should return detailed information about a specified commit" do
|
6
|
+
files_changed = double("FilesChanged")
|
7
|
+
files_changed.stub(:list).and_return fake_change_list
|
8
|
+
files_changed.list.include?("Robert").should be true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "an invalid git SHA" do
|
13
|
+
it "should have an empty change list" do
|
14
|
+
files_changed = double("FilesChanged")
|
15
|
+
files_changed.stub(:list).and_return ''
|
16
|
+
files_changed.list.should == ''
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def fake_change_list
|
21
|
+
list = 'commit d229f066bec91f6fc80448f707e7b070c1791631
|
22
|
+
Author: Robert Greiner <robert@robertgreiner.com>
|
23
|
+
Date: Tue Jan 10 15:09:46 2012 -0600
|
24
|
+
|
25
|
+
Execute git command to get the hash of the latest commit in the repository.
|
26
|
+
|
27
|
+
6 0 lib/tipster/commands/git/latest_commit.rb
|
28
|
+
10 0 spec/commands/git/latest_commit_spec.rb
|
29
|
+
4 0 spec/helper.rb
|
30
|
+
'
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative '../../../lib/tipster/commands/git/latest_commit'
|
2
|
+
|
3
|
+
describe "The latest git commit" do
|
4
|
+
it "should return the most recent SHA" do
|
5
|
+
latest_commit = double("LatestCommit")
|
6
|
+
latest_commit.stub(:id).and_return fake_sha
|
7
|
+
sha = latest_commit.id
|
8
|
+
sha.length.should be 40
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "An invalid git repository" do
|
13
|
+
it "should have an empty id" do
|
14
|
+
latest_commit = double("LatestCommit")
|
15
|
+
latest_commit.stub(:id).and_return ''
|
16
|
+
sha = latest_commit.id
|
17
|
+
sha.length.should be 0
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def fake_sha
|
22
|
+
'c5b5861cb13fc2f2fcbe0d3578758def652c08ab'
|
23
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../../lib/tipster/commands/git/repository_context'
|
3
|
+
|
4
|
+
describe "a git repository" do
|
5
|
+
|
6
|
+
before do
|
7
|
+
LatestCommit.stub(:new).and_return double("LatestCommit")
|
8
|
+
@latest_commit = LatestCommit.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should not be in a valid repository" do
|
12
|
+
@latest_commit.stub(:id).and_return ''
|
13
|
+
RepositoryContext.valid_repository?.should be false
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be in a valid repository" do
|
17
|
+
@latest_commit.stub(:id).and_return 'be6da5838f94e3003ea92150fae92fb5b07c04bb'
|
18
|
+
RepositoryContext.valid_repository?.should be true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "a git commit" do
|
23
|
+
|
24
|
+
before do
|
25
|
+
FilesChanged.stub(:new).and_return double("FilesChanged")
|
26
|
+
@files_changed = FilesChanged.new(stub(:commit_id => ''))
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not be a valid git commit" do
|
30
|
+
@files_changed.stub(:list).and_return ''
|
31
|
+
RepositoryContext.valid_commit_id?('be6da5838f94e3003ea92150fae92fb5b07c04bb').should be false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not be a valid git commit" do
|
35
|
+
@files_changed.stub(:list).and_return 'some file information'
|
36
|
+
RepositoryContext.valid_commit_id?('some-invalid-id').should be true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/files/file'
|
3
|
+
|
4
|
+
describe "File" do
|
5
|
+
it "should get the path of a file from the root directory" do
|
6
|
+
path = File.root "test.rb"
|
7
|
+
path.include?('/tipster/test.rb').should be true
|
8
|
+
end
|
9
|
+
it "should not contain relative path" do
|
10
|
+
path = File.root "test.rb"
|
11
|
+
path.include?('..').should be false
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/heuristics/code_churn_heuristic'
|
3
|
+
require_relative '../../lib/tipster/history/commit_history'
|
4
|
+
|
5
|
+
describe "Code Churn Heuristic" do
|
6
|
+
it "should calculate file churn ratio" do
|
7
|
+
ratio = CodeChurnHeuristic.new.churn_ratio fake_file
|
8
|
+
ratio.should == 0.5
|
9
|
+
end
|
10
|
+
it "should hold the ratio for each file in the commit" do
|
11
|
+
code_churn_heuristic = CodeChurnHeuristic.new
|
12
|
+
code_churn_heuristic.process fake_file
|
13
|
+
code_churn_heuristic.files["stub/fake_file.txt"].should == 0.5
|
14
|
+
end
|
15
|
+
it "should fail on high churn" do
|
16
|
+
code_churn_heuristic = CodeChurnHeuristic.new
|
17
|
+
code_churn_heuristic.apply build_high_churn_commit_history
|
18
|
+
code_churn_heuristic.pass?.should be false
|
19
|
+
end
|
20
|
+
it "should pass on low churn" do
|
21
|
+
code_churn_heuristic = CodeChurnHeuristic.new
|
22
|
+
code_churn_heuristic.apply build_low_churn_commit_history
|
23
|
+
code_churn_heuristic.pass?.should be true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_high_churn_commit_history
|
28
|
+
commit_history = []
|
29
|
+
commit_history << CommitHistory.new(6, 0, "stub/fake_file.txt")
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_low_churn_commit_history
|
33
|
+
commit_history = []
|
34
|
+
commit_history << CommitHistory.new(1, 0, "stub/fake_file.txt")
|
35
|
+
end
|
36
|
+
|
37
|
+
def fake_file
|
38
|
+
CommitHistory.new(5, 0, "stub/fake_file.txt")
|
39
|
+
end
|
40
|
+
|
41
|
+
def fake_high_churn_file
|
42
|
+
CommitHistory.new(5, 0, "stub/fake_file.txt")
|
43
|
+
end
|
44
|
+
|
45
|
+
def fake_low_churn_file
|
46
|
+
CommitHistory.new(5, 0, "stub/fake_file.txt")
|
47
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/heuristics/code_ratio_heuristic'
|
3
|
+
require_relative '../../lib/tipster/history/commit_history'
|
4
|
+
|
5
|
+
describe "Code Ratio Heuristic" do
|
6
|
+
it "should calculate ratio based on test lines of code over production lines of code" do
|
7
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
8
|
+
code_ratio_heuristic.apply build_commit_history_with_tests
|
9
|
+
code_ratio_heuristic.ratio.should == 0.1
|
10
|
+
end
|
11
|
+
it "should return the number of production lines of code" do
|
12
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
13
|
+
code_ratio_heuristic.apply build_commit_history_with_tests
|
14
|
+
code_ratio_heuristic.production_lines_of_code.should be 100
|
15
|
+
end
|
16
|
+
it "should return the number of test lines of code" do
|
17
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
18
|
+
code_ratio_heuristic.apply build_commit_history_with_tests
|
19
|
+
code_ratio_heuristic.test_lines_of_code.should be 10
|
20
|
+
end
|
21
|
+
it "should pass when test code is checked in" do
|
22
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
23
|
+
code_ratio_heuristic.apply build_commit_history_with_tests
|
24
|
+
code_ratio_heuristic.pass?.should be true
|
25
|
+
end
|
26
|
+
it "should pass regardless of type case" do
|
27
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
28
|
+
code_ratio_heuristic.apply build_commit_history_with_capital_letters_in_the_test_names
|
29
|
+
code_ratio_heuristic.pass?.should be true
|
30
|
+
end
|
31
|
+
it "should not pass when no code is checked in" do
|
32
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
33
|
+
code_ratio_heuristic.apply build_commit_history_without_tests
|
34
|
+
code_ratio_heuristic.pass?.should be false
|
35
|
+
end
|
36
|
+
it "should not count web changes as production code" do
|
37
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
38
|
+
code_ratio_heuristic.apply build_commit_with_web_changes_only
|
39
|
+
code_ratio_heuristic.has_production_code?.should be false
|
40
|
+
end
|
41
|
+
it "should pass if only web changes are made" do
|
42
|
+
code_ratio_heuristic = CodeRatioHeuristic.new
|
43
|
+
code_ratio_heuristic.apply build_commit_with_web_changes_only
|
44
|
+
code_ratio_heuristic.pass?.should be true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_commit_history_with_tests
|
49
|
+
commit_history = []
|
50
|
+
commit_history << CommitHistory.new(75, 10, "production_code.java")
|
51
|
+
commit_history << CommitHistory.new(25, 20, "more_production_code.rb")
|
52
|
+
commit_history << CommitHistory.new(10, 40, "some_test_code_spec.rb")
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_commit_history_without_tests
|
56
|
+
commit_history = [CommitHistory.new(75, 10, "production_code.rb")]
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_commit_with_web_changes_only
|
60
|
+
commit_history = []
|
61
|
+
commit_history << CommitHistory.new(75, 10, "style.css")
|
62
|
+
commit_history << CommitHistory.new(25, 20, "index.html")
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_commit_history_with_capital_letters_in_the_test_names
|
66
|
+
commit_history = []
|
67
|
+
commit_history << CommitHistory.new(75, 10, "SomeClass.cs")
|
68
|
+
commit_history << CommitHistory.new(25, 20, "AnotherClass.cs")
|
69
|
+
commit_history << CommitHistory.new(10, 40, "ComeClassTests.cs")
|
70
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/history/commit_history_context'
|
3
|
+
|
4
|
+
describe "Commit History Context" do
|
5
|
+
it "should store the list of modified files in memory" do
|
6
|
+
commit_history_context = CommitHistoryContext.new raw_output
|
7
|
+
change_list = commit_history_context.change_list
|
8
|
+
change_list.size.should be 3
|
9
|
+
end
|
10
|
+
it "should allow retrieval of file information" do
|
11
|
+
commit_history_context = CommitHistoryContext.new raw_output
|
12
|
+
change_list = commit_history_context.change_list
|
13
|
+
change_list[1].file_name.should == "more_production_code.rb"
|
14
|
+
end
|
15
|
+
it "should filter results from raw output" do
|
16
|
+
commit_history_context = CommitHistoryContext.new raw_output
|
17
|
+
files = commit_history_context.affected_files raw_output
|
18
|
+
files[0] == "75 10 production_code.rb"
|
19
|
+
end
|
20
|
+
it "should get the number of modified lines for a single file" do
|
21
|
+
commit_history_context = CommitHistoryContext.new raw_output
|
22
|
+
result = commit_history_context.commit_history "75 10 production_code.rb"
|
23
|
+
result.lines_modified.should be 75
|
24
|
+
end
|
25
|
+
it "should get the number of removed lines for a single file" do
|
26
|
+
commit_history_context = CommitHistoryContext.new raw_output
|
27
|
+
result = commit_history_context.commit_history "75 10 production_code.rb"
|
28
|
+
result.lines_removed.should be 10
|
29
|
+
end
|
30
|
+
it "should get the file name from raw output" do
|
31
|
+
commit_history_context = CommitHistoryContext.new raw_output
|
32
|
+
result = commit_history_context.commit_history "75 10 production_code.rb"
|
33
|
+
result.file_name.should == "production_code.rb"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def raw_output
|
38
|
+
numstat = "commit 38776ceb87e27d4de736e1c2f416e0cb50e19c66\n"
|
39
|
+
numstat << "Author: Fake Person <contact@ratcheting.org>\n"
|
40
|
+
numstat << "Date: Sun Sep 18 12:09:13 2011 -0500\n"
|
41
|
+
numstat << "\n"
|
42
|
+
numstat << " This is a fake commit message.\n"
|
43
|
+
numstat << "\n"
|
44
|
+
numstat << "75 10 production_code.rb\n"
|
45
|
+
numstat << "25 20 more_production_code.rb\n"
|
46
|
+
numstat << "40 30 production_code_spec.rb"
|
47
|
+
end
|
48
|
+
|
49
|
+
def filtered_output
|
50
|
+
result = "75 10 production_code.rb\n"
|
51
|
+
result << "25 20 more_production_code.rb\n"
|
52
|
+
result << "40 30 production_code_spec.rb"
|
53
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/presenters/code_churn_presenter'
|
3
|
+
|
4
|
+
describe "Code Churn Presenter" do
|
5
|
+
|
6
|
+
before do
|
7
|
+
CodeChurnHeuristic.stub(:new).and_return double("CodeChurnHeuristic")
|
8
|
+
@code_churn_heuristic = CodeChurnHeuristic.new
|
9
|
+
@code_churn_heuristic.stub(:apply).and_return nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should not have a risky commit if churn is low" do
|
13
|
+
@code_churn_heuristic.stub(:pass?).and_return true
|
14
|
+
commit = CodeChurnPresenter.new 'a77b5e63d1da2436fc4aa5931e3bf54469ab36c5'
|
15
|
+
commit.pass?.should be true
|
16
|
+
end
|
17
|
+
it "should have a risky commit if churn is high" do
|
18
|
+
@code_churn_heuristic.stub(:pass?).and_return false
|
19
|
+
commit = CodeChurnPresenter.new '2246e9ec9dbc4eb2ffd1fa775d2c64deb11ee8be'
|
20
|
+
commit.pass?.should be false
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../../lib/tipster/presenters/code_ratio_presenter'
|
3
|
+
|
4
|
+
describe "Code Ratio Presenter" do
|
5
|
+
|
6
|
+
before do
|
7
|
+
CodeRatioHeuristic.stub(:new).and_return double("CodeRatioHeuristic")
|
8
|
+
@code_ratio_heuristic = CodeRatioHeuristic.new
|
9
|
+
@code_ratio_heuristic.stub(:apply).and_return nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should not have a risky commit if tests pass" do
|
13
|
+
@code_ratio_heuristic.stub(:pass?).and_return true
|
14
|
+
commit = CodeRatioPresenter.new '451b60a38eecfee6ebee5ec8ceb34ea9c9c77145'
|
15
|
+
commit.pass?.should be true
|
16
|
+
end
|
17
|
+
it "should have a risky commit if tests fail" do
|
18
|
+
@code_ratio_heuristic.stub(:pass?).and_return false
|
19
|
+
commit = CodeRatioPresenter.new '2246e9ec9dbc4eb2ffd1fa775d2c64deb11ee8be'
|
20
|
+
commit.pass?.should be false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
data/tipster.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "tipster/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "tipster"
|
7
|
+
s.version = Tipster::VERSION
|
8
|
+
s.authors = ["Robert Greiner", "Neelima Sriramula"]
|
9
|
+
s.email = ["robert@robertgreiner.com"]
|
10
|
+
s.homepage = "http://creatingcode.com/quality"
|
11
|
+
s.summary = %q{Code risk identification and mitigation tool}
|
12
|
+
s.description = %q{Tipster attempts to assess the risk of your most recent Git commit by applying various code heuristics that have indicated a high probability of introducing defects.}
|
13
|
+
s.rubyforge_project = "tipster"
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
s.add_runtime_dependency "launchy"
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tipster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Greiner
|
9
|
+
- Neelima Sriramula
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-01-20 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: &25452252 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *25452252
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: launchy
|
28
|
+
requirement: &25452000 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *25452000
|
37
|
+
description: Tipster attempts to assess the risk of your most recent Git commit by
|
38
|
+
applying various code heuristics that have indicated a high probability of introducing
|
39
|
+
defects.
|
40
|
+
email:
|
41
|
+
- robert@robertgreiner.com
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- lib/tipster.rb
|
48
|
+
- lib/tipster/commands/git/files_changed.rb
|
49
|
+
- lib/tipster/commands/git/latest_commit.rb
|
50
|
+
- lib/tipster/commands/git/repository_context.rb
|
51
|
+
- lib/tipster/files/file.rb
|
52
|
+
- lib/tipster/files/file_info.rb
|
53
|
+
- lib/tipster/heuristics/code_churn_heuristic.rb
|
54
|
+
- lib/tipster/heuristics/code_ratio_heuristic.rb
|
55
|
+
- lib/tipster/history/commit_history.rb
|
56
|
+
- lib/tipster/history/commit_history_context.rb
|
57
|
+
- lib/tipster/presenters/code_churn_presenter.rb
|
58
|
+
- lib/tipster/presenters/code_ratio_presenter.rb
|
59
|
+
- lib/tipster/presenters/heuristic_status.rb
|
60
|
+
- lib/tipster/reports/html_report.rb
|
61
|
+
- lib/tipster/run_tipster.rb
|
62
|
+
- lib/tipster/version.rb
|
63
|
+
- license.txt
|
64
|
+
- readme.md
|
65
|
+
- spec/commands/file_info_spec.rb
|
66
|
+
- spec/commands/git/files_changed_spec.rb
|
67
|
+
- spec/commands/git/latest_commit_spec.rb
|
68
|
+
- spec/commands/git/repository_context_spec.rb
|
69
|
+
- spec/files/file_spec.rb
|
70
|
+
- spec/heuristics/code_churn_heuristic_spec.rb
|
71
|
+
- spec/heuristics/code_ratio_heuristic_spec.rb
|
72
|
+
- spec/history/commit_history_context_spec.rb
|
73
|
+
- spec/presenters/code_churn_presenter_spec.rb
|
74
|
+
- spec/presenters/code_ratio_presenter_spec.rb
|
75
|
+
- spec/stub/fake_file.txt
|
76
|
+
- tipster.gemspec
|
77
|
+
homepage: http://creatingcode.com/quality
|
78
|
+
licenses: []
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project: tipster
|
97
|
+
rubygems_version: 1.8.15
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: Code risk identification and mitigation tool
|
101
|
+
test_files: []
|