sugarcane 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+
3
+ require 'sugarcane/abc_check'
4
+
5
+ describe SugarCane::AbcCheck do
6
+ def check(file_name, opts = {})
7
+ described_class.new(opts.merge(abc_glob: file_name))
8
+ end
9
+
10
+ it 'does not create violations when no_abc flag is set' do
11
+ file_name = make_file(<<-RUBY)
12
+ class Harness
13
+ def complex_method(a)
14
+ b = a
15
+ return b if b > 3
16
+ end
17
+ end
18
+ RUBY
19
+
20
+ violations = check(file_name, abc_max: 1, no_abc: true).violations
21
+ violations.should be_empty
22
+ end
23
+
24
+ it 'creates an AbcMaxViolation for each method above the threshold' do
25
+ file_name = make_file(<<-RUBY)
26
+ class Harness
27
+ def not_complex
28
+ true
29
+ end
30
+
31
+ def complex_method(a)
32
+ b = a
33
+ return b if b > 3
34
+ end
35
+ end
36
+ RUBY
37
+
38
+ violations = check(file_name, abc_max: 1, no_abc: false).violations
39
+ violations.length.should == 1
40
+ violations[0].values_at(:file, :label, :value).should ==
41
+ [file_name, "Harness#complex_method", 2]
42
+ end
43
+
44
+ it 'sorts violations by complexity' do
45
+ file_name = make_file(<<-RUBY)
46
+ class Harness
47
+ def not_complex
48
+ true
49
+ end
50
+
51
+ def complex_method(a)
52
+ b = a
53
+ return b if b > 3
54
+ end
55
+ end
56
+ RUBY
57
+
58
+ violations = check(file_name, abc_max: 0).violations
59
+ violations.length.should == 2
60
+ complexities = violations.map {|x| x[:value] }
61
+ complexities.should == complexities.sort.reverse
62
+ end
63
+
64
+ it 'creates a violation when code cannot be parsed' do
65
+ file_name = make_file(<<-RUBY)
66
+ class Harness
67
+ RUBY
68
+
69
+ violations = check(file_name).violations
70
+ violations.length.should == 1
71
+ violations[0][:file].should == file_name
72
+ violations[0][:description].should be_instance_of(String)
73
+ end
74
+
75
+ it 'skips declared exclusions' do
76
+ file_name = make_file(<<-RUBY)
77
+ class Harness
78
+ def instance_meth
79
+ true
80
+ end
81
+
82
+ def self.class_meth
83
+ true
84
+ end
85
+
86
+ module Nested
87
+ def i_meth
88
+ true
89
+ end
90
+
91
+ def self.c_meth
92
+ true
93
+ end
94
+
95
+ def other_meth
96
+ true
97
+ end
98
+ end
99
+ end
100
+ RUBY
101
+
102
+ exclusions = %w[ Harness#instance_meth Harness.class_meth
103
+ Harness::Nested#i_meth Harness::Nested.c_meth ]
104
+ violations = check(file_name,
105
+ abc_max: 0,
106
+ abc_exclude: exclusions
107
+ ).violations
108
+ violations.length.should == 1
109
+ violations[0].values_at(:file, :label, :value).should ==
110
+ [file_name, "Harness::Nested#other_meth", 1]
111
+ end
112
+
113
+ it "creates an AbcMaxViolation for method in assigned anonymous class" do
114
+ file_name = make_file(<<-RUBY)
115
+ MyClass = Struct.new(:foo) do
116
+ def test_method(a)
117
+ b = a
118
+ return b if b > 3
119
+ end
120
+ end
121
+ RUBY
122
+
123
+ violations = check(file_name, abc_max: 1).violations
124
+ violations[0][:label] == "MyClass#test_method"
125
+ end
126
+
127
+ it "creates an AbcMaxViolation for method in anonymous class" do
128
+ file_name = make_file(<<-RUBY)
129
+ Class.new do
130
+ def test_method(a)
131
+ b = a
132
+ return b if b > 3
133
+ end
134
+ end
135
+ RUBY
136
+
137
+ violations = check(file_name, abc_max: 1).violations
138
+ violations[0][:label].should == "(anon)#test_method"
139
+ end
140
+
141
+ def self.it_should_extract_method_name(name, label=name, sep='#')
142
+ it "creates an AbcMaxViolation for #{name}" do
143
+ file_name = make_file(<<-RUBY)
144
+ class Harness
145
+ def #{name}(a)
146
+ b = a
147
+ return b if b > 3
148
+ end
149
+ end
150
+ RUBY
151
+
152
+ violations = check(file_name, abc_max: 1).violations
153
+ violations[0][:label].should == "Harness#{sep}#{label}"
154
+ end
155
+ end
156
+
157
+ # These method names all create different ASTs. Which is weird.
158
+ it_should_extract_method_name 'a'
159
+ it_should_extract_method_name 'self.a', 'a', '.'
160
+ it_should_extract_method_name 'next'
161
+ it_should_extract_method_name 'GET'
162
+ it_should_extract_method_name '`'
163
+ it_should_extract_method_name '>='
164
+ end
data/spec/cane_spec.rb ADDED
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+ require "stringio"
3
+ require 'sugarcane/cli'
4
+ require 'sugarcane/menu'
5
+
6
+ require 'sugarcane/rake_task'
7
+ require 'sugarcane/task_runner'
8
+
9
+ # Acceptance tests
10
+ describe 'The sugarcane application' do
11
+ let(:class_name) { "C#{rand(10 ** 10)}" }
12
+
13
+ let(:fn) do
14
+ make_file(<<-RUBY + " ")
15
+ class Harness
16
+ def complex_method(a)
17
+ if a < 2
18
+ return "low"
19
+ else
20
+ return "high"
21
+ end
22
+ end
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ let(:check_file) do
28
+ make_file <<-RUBY
29
+ class #{class_name} < Struct.new(:opts)
30
+ def self.options
31
+ {
32
+ unhappy_file: ["File to check", default: [nil]]
33
+ }
34
+ end
35
+
36
+ def violations
37
+ [
38
+ description: "Files are unhappy",
39
+ file: opts.fetch(:unhappy_file),
40
+ label: ":("
41
+ ]
42
+ end
43
+ end
44
+ RUBY
45
+ end
46
+
47
+ it 'returns a non-zero exit code and a details of checks that failed' do
48
+ output, exitstatus = run %(
49
+ --report
50
+ --style-glob #{fn}
51
+ --doc-glob #{fn}
52
+ --abc-glob #{fn}
53
+ --abc-max 1
54
+ -r #{check_file}
55
+ --check #{class_name}
56
+ --unhappy-file #{fn}
57
+ )
58
+ output.should include("Lines violated style requirements")
59
+ output.should include("Methods exceeded maximum allowed ABC complexity")
60
+ output.should include(
61
+ "Class and Module definitions require explanatory comments"
62
+ )
63
+ exitstatus.should == 1
64
+ end
65
+
66
+ it 'should not show methods within the expected ABC complexity' do
67
+ output, exitstatus = run %(
68
+ --report
69
+ --style-glob #{fn}
70
+ --doc-glob #{fn}
71
+ --abc-glob #{fn}
72
+ --abc-max 10
73
+ -r #{check_file}
74
+ --check #{class_name}
75
+ --unhappy-file #{fn}
76
+ )
77
+ output.should include("Lines violated style requirements")
78
+ output.should_not include("Methods exceeded maximum allowed ABC complexity")
79
+ output.should include(
80
+ "Class and Module definitions require explanatory comments"
81
+ )
82
+ exitstatus.should == 1
83
+ end
84
+
85
+ it 'creates a menu' do
86
+ pending "Make a unit test or find a way to create artificial key presses"
87
+ output, exitstatus = run %(
88
+ --style-glob #{fn}
89
+ --doc-glob #{fn}
90
+ --abc-glob #{fn}
91
+ --abc-max 1
92
+ -r #{check_file}
93
+ --check #{class_name}
94
+ --unhappy-file #{fn}
95
+ )
96
+
97
+ exitstatus.should == 1
98
+ end
99
+
100
+ it 'handles invalid unicode input' do
101
+ fn = make_file("\xc3\x28")
102
+
103
+ _, exitstatus = run("--style-glob #{fn} --abc-glob #{fn} --doc-glob #{fn}")
104
+
105
+ exitstatus.should == 0
106
+ end
107
+
108
+ it 'can run tasks in parallel' do
109
+ # This spec isn't great, but there is no good way to actually observe that
110
+ # tasks run in parallel and we want to verify the conditional is correct.
111
+ SugarCane.task_runner(parallel: true).should == Parallel
112
+ end
113
+
114
+ it 'colorizes output' do
115
+ output, exitstatus = run("--report --color --abc-max 0")
116
+
117
+ output.should include("\e[31m")
118
+ end
119
+
120
+ after do
121
+ if Object.const_defined?(class_name)
122
+ Object.send(:remove_const, class_name)
123
+ end
124
+ end
125
+
126
+ def run(cli_args)
127
+ result = nil
128
+ output = capture_stdout do
129
+ result = SugarCane::CLI.run(
130
+ %w(--no-abc --no-style --no-doc) + cli_args.split(/\s+/m)
131
+ )
132
+ end
133
+
134
+ [output, result ? 0 : 1]
135
+ end
136
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/cli'
3
+
4
+ describe SugarCane::CLI do
5
+ describe '.run' do
6
+
7
+ let!(:parser) { class_double("SugarCane::CLI::Parser").as_stubbed_const }
8
+ let!(:cane) { class_double("SugarCane").as_stubbed_const }
9
+
10
+ it 'runs SugarCane with the given arguments' do
11
+ parser.should_receive(:parse).with("--args").and_return(args: true)
12
+ cane.should_receive(:run).with(args: true).and_return("tracer")
13
+
14
+ described_class.run("--args").should == "tracer"
15
+ end
16
+
17
+ it 'does not run SugarCane if parser was able to handle input' do
18
+ parser.should_receive(:parse).with("--args").and_return("tracer")
19
+
20
+ described_class.run("--args").should == "tracer"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/doc_check'
3
+
4
+ describe SugarCane::DocCheck do
5
+ def check(file_name, opts = {})
6
+ described_class.new(opts.merge(doc_glob: file_name))
7
+ end
8
+
9
+ it 'creates a DocViolation for each undocumented class with a method' do
10
+ file_name = make_file <<-RUBY
11
+ class Doc; end
12
+ class Empty; end # No doc is fine
13
+ class NoDoc; def with_method; end; end
14
+ classIgnore = nil
15
+ [:class]
16
+ # class Ignore
17
+ class Meta
18
+ class << self; end
19
+ end
20
+ module DontNeedDoc; end
21
+ # This module is documented
22
+ module HasDoc
23
+ def mixin; end
24
+ end
25
+ module AlsoNeedsDoc; def mixin; end; end
26
+ module NoDocIsFine
27
+ module ButThisNeedsDoc
28
+ def self.global
29
+ end
30
+ end
31
+ module AlsoNoDocIsFine; end
32
+ # We've got docs
33
+ module CauseWeNeedThem
34
+ def mixin
35
+ end
36
+ end
37
+ end
38
+ RUBY
39
+
40
+ violations = check(file_name).violations
41
+ violations.length.should == 3
42
+
43
+ violations[0].values_at(:file, :line, :label).should == [
44
+ file_name, 3, "NoDoc"
45
+ ]
46
+
47
+ violations[1].values_at(:file, :line, :label).should == [
48
+ file_name, 15, "AlsoNeedsDoc"
49
+ ]
50
+
51
+ violations[2].values_at(:file, :line, :label).should == [
52
+ file_name, 17, "ButThisNeedsDoc"
53
+ ]
54
+ end
55
+
56
+ it 'does not create violations for single line classes without methods' do
57
+ file_name = make_file <<-RUBY
58
+ class NeedsDoc
59
+ class AlsoNeedsDoc < StandardError; def foo; end; end
60
+ class NoDocIsOk < StandardError; end
61
+ class NoDocIsAlsoOk < StandardError; end # No doc is fine on this too
62
+
63
+ def my_method
64
+ end
65
+ end
66
+ RUBY
67
+
68
+ violations = check(file_name).violations
69
+ violations.length.should == 2
70
+
71
+ violations[0].values_at(:file, :line, :label).should == [
72
+ file_name, 1, "NeedsDoc"
73
+ ]
74
+
75
+ violations[1].values_at(:file, :line, :label).should == [
76
+ file_name, 2, "AlsoNeedsDoc"
77
+ ]
78
+ end
79
+
80
+ it 'ignores magic encoding comments' do
81
+ file_name = make_file <<-RUBY
82
+ # coding = utf-8
83
+ class NoDoc; def do_stuff; end; end
84
+ # -*- encoding : utf-8 -*-
85
+ class AlsoNoDoc; def do_more_stuff; end; end
86
+ # Parse a Transfer-Encoding: Chunked response
87
+ class Doc; end
88
+ RUBY
89
+
90
+ violations = check(file_name).violations
91
+ violations.length.should == 2
92
+
93
+ violations[0].values_at(:file, :line, :label).should == [
94
+ file_name, 2, "NoDoc"
95
+ ]
96
+ violations[1].values_at(:file, :line, :label).should == [
97
+ file_name, 4, "AlsoNoDoc"
98
+ ]
99
+ end
100
+
101
+ it 'creates a violation for missing README' do
102
+ file = class_double("SugarCane::File").as_stubbed_const
103
+ stub_const("SugarCane::File", file)
104
+ file.should_receive(:case_insensitive_glob).with("README*").and_return([])
105
+
106
+ violations = check("").violations
107
+ violations.length.should == 1
108
+
109
+ violations[0].values_at(:description, :label).should == [
110
+ "Missing documentation", "No README found"
111
+ ]
112
+ end
113
+
114
+ it 'does not create a violation when readme exists' do
115
+ file = class_double("SugarCane::File").as_stubbed_const
116
+ stub_const("SugarCane::File", file)
117
+ file
118
+ .should_receive(:case_insensitive_glob)
119
+ .with("README*")
120
+ .and_return(%w(readme.md))
121
+
122
+ violations = check("").violations
123
+ violations.length.should == 0
124
+ end
125
+
126
+ it 'skips declared exclusions' do
127
+ file_name = make_file <<-FILE.gsub /^\s{6}/, ''
128
+ class NeedsDocumentation
129
+ end
130
+ FILE
131
+
132
+ violations = check(file_name,
133
+ doc_exclude: [file_name]
134
+ ).violations
135
+
136
+ violations.length.should == 0
137
+ end
138
+
139
+ it 'skips declared glob-based exclusions' do
140
+ file_name = make_file <<-FILE.gsub /^\s{6}/, ''
141
+ class NeedsDocumentation
142
+ end
143
+ FILE
144
+
145
+ violations = check(file_name,
146
+ doc_exclude: ["#{File.dirname(file_name)}/*"]
147
+ ).violations
148
+
149
+ violations.length.should == 0
150
+ end
151
+
152
+ it 'skips class inside an array' do
153
+ file_name = make_file <<-RUBY
154
+ %w(
155
+ class
156
+ method
157
+ )
158
+ RUBY
159
+
160
+ violations = check(file_name).violations
161
+ violations.length.should == 0
162
+ end
163
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+ require 'sugarcane/encoding_aware_iterator'
4
+
5
+ # Example bad input from:
6
+ # http://stackoverflow.com/questions/1301402/example-invalid-utf8-string
7
+ describe SugarCane::EncodingAwareIterator do
8
+ it 'handles non-UTF8 input' do
9
+ lines = ["\xc3\x28"]
10
+ result = described_class.new(lines).map.with_index do |line, number|
11
+ line.should be_kind_of(String)
12
+ [line =~ /\s/, number]
13
+ end
14
+ result.should == [[nil, 0]]
15
+ end
16
+
17
+ it 'does not enter an infinite loop on persistently bad input' do
18
+ ->{
19
+ described_class.new([""]).map.with_index do |line, number|
20
+ "\xc3\x28" =~ /\s/
21
+ end
22
+ }.should raise_error(ArgumentError)
23
+ end
24
+
25
+ it 'allows each with no block' do
26
+ called_with_line = nil
27
+ described_class.new([""]).each.with_index do |line, number|
28
+ called_with_line = line
29
+ end
30
+ called_with_line.should == ""
31
+ end
32
+ end
data/spec/file_spec.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'tmpdir'
3
+
4
+ require 'sugarcane/file'
5
+
6
+ describe SugarCane::File do
7
+ describe '.case_insensitive_glob' do
8
+ it 'matches all kinds of readmes' do
9
+ expected = %w(
10
+ README
11
+ readme.md
12
+ ReaDME.TEXTILE
13
+ )
14
+
15
+ Dir.mktmpdir do |dir|
16
+ Dir.chdir(dir) do
17
+ expected.each do |x|
18
+ FileUtils.touch(x)
19
+ end
20
+ SugarCane::File.case_insensitive_glob("README*").should =~ expected
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'sugarcane/json_formatter'
3
+
4
+ describe SugarCane::JsonFormatter do
5
+ it 'outputs violations as JSON' do
6
+ violations = [{description: 'Fail', line: 3}]
7
+ JSON.parse(described_class.new(violations).to_s).should ==
8
+ [{'description' => 'Fail', 'line' => 3}]
9
+ end
10
+ end