tipster 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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: []
|