tipster 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ .idea
2
+ *.lock
3
+ *.html
4
+ *.gem
@@ -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,6 @@
1
+ class FilesChanged
2
+ attr_reader :list
3
+ def initialize(commit_id)
4
+ @list = `git show #{commit_id} --numstat`
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class LatestCommit
2
+ attr_reader :id
3
+ def initialize
4
+ @id = `git rev-parse --verify HEAD`.chomp
5
+ end
6
+ 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,10 @@
1
+ require 'tmpdir'
2
+
3
+ class File
4
+ def self.root(file)
5
+ expand_path('../../../', dirname(__FILE__)) << '/' << file
6
+ end
7
+ def self.temp(file)
8
+ expand_path File.expand_path Dir.tmpdir << '/' << file
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ class FileInfo
2
+ def initialize(file_name)
3
+ @file_name = file_name
4
+ end
5
+
6
+ def line_count
7
+ File.readlines(@file_name).length
8
+ end
9
+ 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,9 @@
1
+ class CommitHistory
2
+ attr_accessor :lines_modified, :lines_removed, :file_name
3
+
4
+ def initialize(lines_modified = 0, lines_removed = 0, file_name = "")
5
+ @lines_modified = lines_modified
6
+ @lines_removed = lines_removed
7
+ @file_name = file_name
8
+ end
9
+ 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,10 @@
1
+ class HeuristicStatus
2
+
3
+ attr_accessor :status, :description
4
+
5
+ def initialize(safe, description)
6
+ @status = safe ? 'Safe' : 'Risky'
7
+ @description = description
8
+ end
9
+
10
+ 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
@@ -0,0 +1,8 @@
1
+ require_relative '../tipster'
2
+
3
+ path = File.expand_path '../../'
4
+
5
+ Tipster.new(path).html_report("HEAD",3)
6
+
7
+
8
+
@@ -0,0 +1,3 @@
1
+ module Tipster
2
+ VERSION = "0.3.0"
3
+ end
@@ -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.
@@ -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
+
@@ -0,0 +1,10 @@
1
+ line 1
2
+
3
+ line 3
4
+
5
+
6
+
7
+
8
+ line 8
9
+
10
+ This file has 10 lines in it.....
@@ -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: []