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.
@@ -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: []