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