sugarcane 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+ require "stringio"
3
+ require 'sugarcane/cli/parser'
4
+
5
+ describe SugarCane::CLI::Parser do
6
+ def run(cli_args)
7
+ result = nil
8
+ output = StringIO.new("")
9
+ result = SugarCane::CLI::Parser.new(output).parse(cli_args.split(/\s+/m))
10
+
11
+ [output.string, result]
12
+ end
13
+
14
+ it 'allows style options to be configured' do
15
+ output, result = run("--style-glob myfile --style-measure 3")
16
+ result[:style_glob].should == 'myfile'
17
+ result[:style_measure].should == 3
18
+ end
19
+
20
+ it 'allows checking gte of a value in a file' do
21
+ output, result = run("--gte myfile,90")
22
+ result[:gte].should == [['myfile', '90']]
23
+ end
24
+
25
+ it 'allows checking eq of a value in a file' do
26
+ output, result = run("--eq myfile,90")
27
+ result[:eq].should == [['myfile', '90']]
28
+ end
29
+
30
+ it 'allows checking lte of a value in a file' do
31
+ output, result = run("--lte myfile,90")
32
+ result[:lte].should == [['myfile', '90']]
33
+ end
34
+
35
+ it 'allows checking lt of a value in a file' do
36
+ output, result = run("--lt myfile,90")
37
+ result[:lt].should == [['myfile', '90']]
38
+ end
39
+
40
+ it 'allows checking gt of a value in a file' do
41
+ output, resugt = run("--gt myfile,90")
42
+ resugt[:gt].should == [['myfile', '90']]
43
+ end
44
+
45
+ it 'allows upper bound of failed checks' do
46
+ output, result = run("--max-violations 1")
47
+ result[:max_violations].should == 1
48
+ end
49
+
50
+ it 'uses positional arguments as shortcut for individual files' do
51
+ output, result = run("--all mysinglefile")
52
+ result[:abc_glob].should == 'mysinglefile'
53
+ result[:style_glob].should == 'mysinglefile'
54
+ result[:doc_glob].should == 'mysinglefile'
55
+
56
+ output, result = run("--all mysinglefile --abc-glob myotherfile")
57
+ result[:abc_glob].should == 'myotherfile'
58
+ result[:style_glob].should == 'mysinglefile'
59
+ result[:doc_glob].should == 'mysinglefile'
60
+ end
61
+
62
+ it 'displays a help message' do
63
+ output, result = run("--help")
64
+
65
+ result.should be
66
+ output.should include("Usage:")
67
+ end
68
+
69
+ it 'handles invalid options by showing help' do
70
+ output, result = run("--bogus")
71
+
72
+ output.should include("Usage:")
73
+ result.should_not be
74
+ end
75
+
76
+ it 'displays version' do
77
+ output, result = run("--version")
78
+
79
+ result.should be
80
+ output.should include(SugarCane::VERSION)
81
+ end
82
+
83
+ it 'supports exclusions' do
84
+ options = [
85
+ "--abc-exclude", "Harness#complex_method",
86
+ "--doc-exclude", 'myfile',
87
+ "--style-exclude", 'myfile'
88
+ ].join(' ')
89
+
90
+ _, result = run(options)
91
+ result[:abc_exclude].should == [['Harness#complex_method']]
92
+ result[:doc_exclude].should == [['myfile']]
93
+ result[:style_exclude].should == [['myfile']]
94
+ end
95
+
96
+ describe 'argument ordering' do
97
+ it 'gives precedence to the last argument #1' do
98
+ _, result = run("--doc-glob myfile --no-doc")
99
+ result[:no_doc].should be
100
+
101
+ _, result = run("--no-doc --doc-glob myfile")
102
+ result[:no_doc].should_not be
103
+ end
104
+ end
105
+
106
+ it 'loads default options from .sugarcane' do
107
+ defaults = <<-EOS
108
+ --no-doc
109
+ --abc-glob myfile
110
+ --style-glob myfile
111
+ EOS
112
+
113
+ SugarCane::File.stub(:exists?).with("./.sugarcane").and_return(true)
114
+ SugarCane::File.stub(:contents).with("./.sugarcane").and_return(defaults)
115
+
116
+ _, result = run("--style-glob myotherfile")
117
+
118
+ result[:no_doc].should be
119
+ result[:abc_glob].should == 'myfile'
120
+ result[:style_glob].should == 'myotherfile'
121
+ end
122
+
123
+ it 'loads default options from .cane if .sugarcane is not present' do
124
+ defaults = <<-EOS
125
+ --no-doc
126
+ --abc-glob myfile
127
+ --style-glob myfile
128
+ EOS
129
+
130
+ SugarCane::File.stub(:exists?).with("./.sugarcane").and_return(false)
131
+ SugarCane::File.stub(:exists?).with("./.cane").and_return(true)
132
+ SugarCane::File.stub(:contents).with("./.cane").and_return(defaults)
133
+
134
+ _, result = run("--style-glob myotherfile")
135
+
136
+ result[:no_doc].should be
137
+ result[:abc_glob].should == 'myfile'
138
+ result[:style_glob].should == 'myotherfile'
139
+ end
140
+
141
+ it 'allows parallel option' do
142
+ _, result = run("--parallel")
143
+ result[:parallel].should be
144
+ end
145
+
146
+ it 'handles ambiguous options' do
147
+ output, result = run("-abc-max")
148
+ output.should include("Usage:")
149
+ result.should_not be
150
+ end
151
+
152
+ it 'handles no_readme option' do
153
+ _, result = run("--no-readme")
154
+ result[:no_readme].should be
155
+ end
156
+
157
+ it 'handles json option' do
158
+ _, result = run("--json")
159
+ result[:json].should be
160
+ end
161
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/rake_task'
3
+
4
+ describe SugarCane::RakeTask do
5
+ it 'enables cane to be configured an run via rake' do
6
+ fn = make_file("90")
7
+ my_check = Class.new(Struct.new(:opts)) do
8
+ def violations
9
+ [description: 'test', label: opts.fetch(:some_opt)]
10
+ end
11
+ end
12
+
13
+ task = SugarCane::RakeTask.new(:quality) do |cane|
14
+ cane.no_abc = true
15
+ cane.no_doc = true
16
+ cane.no_style = true
17
+ cane.add_threshold fn, :>=, 99
18
+ cane.use my_check, some_opt: "theopt"
19
+ cane.max_violations = 0
20
+ cane.parallel = false
21
+ end
22
+
23
+ task.no_abc.should == true
24
+
25
+ task.should_receive(:abort)
26
+ out = capture_stdout do
27
+ Rake::Task['quality'].invoke
28
+ end
29
+
30
+ out.should include("Quality threshold crossed")
31
+ out.should include("theopt")
32
+ end
33
+
34
+ it 'can be configured using a .cane file' do
35
+ conf = "--gte 90,99"
36
+
37
+ task = SugarCane::RakeTask.new(:canefile_quality) do |cane|
38
+ cane.canefile = make_file(conf)
39
+ end
40
+
41
+ task.should_receive(:abort)
42
+ out = capture_stdout do
43
+ Rake::Task['canefile_quality'].invoke
44
+ end
45
+
46
+ out.should include("Quality threshold crossed")
47
+ end
48
+
49
+ it 'defaults to using a canefile without a block' do
50
+ in_tmp_dir do
51
+ conf = "--gte 90,99"
52
+ File.open('.cane', 'w') {|f| f.write conf }
53
+
54
+ task = SugarCane::RakeTask.new(:canefile_quality)
55
+
56
+ task.should_receive(:abort)
57
+ out = capture_stdout do
58
+ Rake::Task['canefile_quality'].invoke
59
+ end
60
+
61
+ out.should include("Quality threshold crossed")
62
+ end
63
+ end
64
+
65
+ after do
66
+ Rake::Task.clear
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/runner'
3
+
4
+ describe SugarCane::Runner do
5
+ describe '#run' do
6
+ it 'returns true iff fewer violations than max allowed' do
7
+ described_class.new(checks: [], max_violations: 0).run.should be
8
+ described_class.new(checks: [], max_violations: -1).run.should_not be
9
+ end
10
+
11
+ it 'returns JSON output' do
12
+ formatter = class_double("SugarCane::JsonFormatter").as_stubbed_const
13
+ formatter.should_receive(:new).and_return("JSON")
14
+ buffer = StringIO.new("")
15
+
16
+ described_class.new(
17
+ out: buffer, checks: [], max_violations: 0, json: true
18
+ ).run
19
+
20
+ buffer.string.should == "JSON"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+ Coveralls.wear!
4
+
5
+ class SimpleCov::Formatter::QualityFormatter
6
+ def format(result)
7
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
8
+ File.open("tmp/coverage/covered_percent", "w") do |f|
9
+ f.puts result.source_files.covered_percent.to_i
10
+ end
11
+ end
12
+ end
13
+
14
+ SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
15
+ SimpleCov.start do
16
+ coverage_dir('tmp/coverage')
17
+ add_filter "vendor/bundle/"
18
+ add_filter "spec/"
19
+ end
20
+
21
+ require 'rspec/fire'
22
+ require 'tempfile'
23
+ require 'stringio'
24
+ require 'rake'
25
+ require 'rake/tasklib'
26
+
27
+ RSpec.configure do |config|
28
+ config.include(RSpec::Fire)
29
+
30
+ def capture_stdout &block
31
+ real_stdout, $stdout = $stdout, StringIO.new
32
+ yield
33
+ $stdout.string
34
+ ensure
35
+ $stdout = real_stdout
36
+ end
37
+ end
38
+
39
+ # Keep a reference to all tempfiles so they are not garbage collected until the
40
+ # process exits.
41
+ $tempfiles = []
42
+
43
+ def make_file(content)
44
+ tempfile = Tempfile.new('cane')
45
+ $tempfiles << tempfile
46
+ tempfile.print(content)
47
+ tempfile.flush
48
+ tempfile.path
49
+ end
50
+
51
+ def in_tmp_dir(&block)
52
+ Dir.mktmpdir do |dir|
53
+ Dir.chdir(dir, &block)
54
+ end
55
+ end
56
+
57
+ RSpec::Matchers.define :have_violation do |label|
58
+ match do |check|
59
+ violations = check.violations
60
+ violations.length.should == 1
61
+ violations[0][:label].should == label
62
+ end
63
+ end
64
+
65
+ RSpec::Matchers.define :have_no_violations do |label|
66
+ match do |check|
67
+ violations = check.violations
68
+ violations.length.should == 0
69
+ end
70
+ end
71
+
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/style_check'
3
+
4
+ describe SugarCane::StyleCheck do
5
+ def check(file_name, opts = {})
6
+ described_class.new(opts.merge(style_glob: file_name))
7
+ end
8
+
9
+ let(:ruby_with_style_issue) do
10
+ [
11
+ "def test ",
12
+ "\t1",
13
+ "end"
14
+ ].join("\n")
15
+ end
16
+
17
+ it 'creates a StyleViolation for each method above the threshold' do
18
+ file_name = make_file(ruby_with_style_issue)
19
+
20
+ violations = check(file_name, style_measure: 8).violations
21
+ violations.length.should == 3
22
+ end
23
+
24
+ it 'skips declared exclusions' do
25
+ file_name = make_file(ruby_with_style_issue)
26
+
27
+ violations = check(file_name,
28
+ style_measure: 80,
29
+ style_exclude: [file_name]
30
+ ).violations
31
+
32
+ violations.length.should == 0
33
+ end
34
+
35
+ it 'skips declared glob-based exclusions' do
36
+ file_name = make_file(ruby_with_style_issue)
37
+
38
+ violations = check(file_name,
39
+ style_measure: 80,
40
+ style_exclude: ["#{File.dirname(file_name)}/*"]
41
+ ).violations
42
+
43
+ violations.length.should == 0
44
+ end
45
+
46
+ it 'does not include trailing new lines in the character count' do
47
+ file_name = make_file('#' * 80 + "\n" + '#' * 80)
48
+
49
+ violations = check(file_name,
50
+ style_measure: 80,
51
+ style_exclude: [file_name]
52
+ ).violations
53
+
54
+ violations.length.should == 0
55
+ end
56
+
57
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/threshold_check'
3
+
4
+ describe SugarCane::ThresholdCheck do
5
+
6
+ let(:simplecov_last_run) do
7
+ <<-ENDL
8
+ {
9
+ "result": {
10
+ "covered_percent": 93.88
11
+ }
12
+ }
13
+ ENDL
14
+ end
15
+
16
+ context "checking violations" do
17
+
18
+ def run(threshold, value)
19
+ described_class.new(threshold => [['x', value]])
20
+ end
21
+
22
+ context "when the current coverage cannot be read" do
23
+ it do
24
+ run(:gte, 20).should \
25
+ have_violation('x is unavailable, should be >= 20.0')
26
+ end
27
+ end
28
+
29
+ context "when the coverage threshold is incorrectly specified" do
30
+ it do
31
+ described_class.new(gte: [['20', 'bogus_file']]).should \
32
+ have_violation('bogus_file is not a number or a file')
33
+ end
34
+ end
35
+
36
+ context 'when coverage threshold is valid' do
37
+ before do
38
+ file = class_double("SugarCane::File").as_stubbed_const
39
+ stub_const("SugarCane::File", file)
40
+ file.should_receive(:contents).with('x').and_return("8\n")
41
+ end
42
+
43
+ context '>' do
44
+ it { run(:gt, 7).should have_no_violations }
45
+ it { run(:gt, 8).should have_violation('x is 8.0, should be > 8.0') }
46
+ it { run(:gt, 9).should have_violation('x is 8.0, should be > 9.0') }
47
+ end
48
+
49
+ context '>=' do
50
+ it { run(:gte, 7).should have_no_violations }
51
+ it { run(:gte, 8).should have_no_violations }
52
+ it { run(:gte, 9).should have_violation('x is 8.0, should be >= 9.0') }
53
+ end
54
+
55
+ context '==' do
56
+ it { run(:eq, 7).should have_violation('x is 8.0, should be == 7.0') }
57
+ it { run(:eq, 8).should have_no_violations }
58
+ it { run(:eq, 9).should have_violation('x is 8.0, should be == 9.0') }
59
+ end
60
+
61
+ context '<=' do
62
+ it { run(:lte, 7).should have_violation('x is 8.0, should be <= 7.0') }
63
+ it { run(:lte, 8).should have_no_violations }
64
+ it { run(:lte, 9).should have_no_violations }
65
+ end
66
+
67
+ context '<' do
68
+ it { run(:lt, 7).should have_violation('x is 8.0, should be < 7.0') }
69
+ it { run(:lt, 8).should have_violation('x is 8.0, should be < 8.0') }
70
+ it { run(:lt, 9).should have_no_violations }
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ context "normalizing a user supplied value to a threshold" do
77
+ it "normalizes an integer to itself" do
78
+ subject.normalized_limit(99).should == 99
79
+ end
80
+
81
+ it "normalizes a float to itself" do
82
+ subject.normalized_limit(99.6).should == 99.6
83
+ end
84
+
85
+ it "normalizes a valid file to its contents" do
86
+ subject.normalized_limit(make_file('99.5')).should == 99.5
87
+ end
88
+
89
+ it "normalizes an invalid file to an unavailable value" do
90
+ limit = subject.normalized_limit("/File.does.not.exist")
91
+ limit.should be_a SugarCane::ThresholdCheck::UnavailableValue
92
+ end
93
+
94
+
95
+ it 'normalizes a json file to a float' do
96
+ subject.normalized_limit(make_file(simplecov_last_run)).should == 93.88
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/violation_formatter'
3
+
4
+ describe SugarCane::ViolationFormatter do
5
+ def violation(description)
6
+ {
7
+ description: description
8
+ }
9
+ end
10
+
11
+ it 'includes number of violations in the group header' do
12
+ described_class.new([violation("FAIL")]).to_s.should include("(1)")
13
+ end
14
+
15
+ it 'includes total number of violations' do
16
+ violations = [violation("FAIL1"), violation("FAIL2")]
17
+ result = described_class.new(violations).to_s
18
+ result.should include("Total Violations: 2")
19
+ end
20
+
21
+ it 'does not colorize output by default' do
22
+ result = described_class.new([violation("FAIL")]).to_s
23
+ result.should_not include("\e[31m")
24
+ end
25
+
26
+ it 'colorizes output when passed color: true' do
27
+ result = described_class.new([violation("FAIL")], color: true).to_s
28
+ result.should include("\e[31m")
29
+ result.should include("\e[0m")
30
+ end
31
+
32
+ it 'does not colorize output if max_violations is not crossed' do
33
+ options = { color: true, max_violations: 1 }
34
+ result = described_class.new([violation("FAIL")], options).to_s
35
+
36
+ result.should_not include("\e[31m")
37
+ end
38
+ end
data/sugarcane.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ require 'sugarcane/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ["Robert Qualls"]
7
+ gem.email = ["robert@robertqualls.com"]
8
+ gem.description = "Cane with a menu that opens a text editor for each issue"
9
+ gem.summary = %q{
10
+ Fails your build if code quality thresholds are not met. Provides
11
+ complexity and style checkers built-in, and allows integration with with
12
+ custom quality metrics.
13
+ }
14
+ gem.homepage = "http://github.com/rlqualls/sugarcane"
15
+
16
+ gem.executables = []
17
+ gem.required_ruby_version = '>= 1.9.0'
18
+ gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
19
+ README.md
20
+ HISTORY.md
21
+ LICENSE
22
+ sugarcane.gemspec
23
+ )
24
+ gem.test_files = Dir.glob("spec/**/*.rb")
25
+ gem.name = "sugarcane"
26
+ gem.require_paths = ["lib"]
27
+ gem.bindir = "bin"
28
+ gem.executables << "sugarcane"
29
+ gem.license = "Apache 2.0"
30
+ gem.version = SugarCane::VERSION
31
+ gem.has_rdoc = false
32
+
33
+ gem.add_dependency 'parallel'
34
+ gem.add_dependency 'ncursesw'
35
+
36
+ gem.add_development_dependency 'rspec', '~> 2.0'
37
+ gem.add_development_dependency 'rake'
38
+ gem.add_development_dependency 'simplecov'
39
+ gem.add_development_dependency 'coveralls'
40
+ gem.add_development_dependency 'rspec-fire', '~> 1.2.0'
41
+ end