sugarcane 0.0.1

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,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