cane 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,17 +1,29 @@
1
1
  # Cane History
2
2
 
3
+ ## 2.1.0 - 26 August 2012 (2962d8fb)
4
+
5
+ * Support for user-defined checks (#30)
6
+
3
7
  ## 2.0.0 - 19 August 2012 (35cae086)
4
8
 
5
9
  * ABC check labels `MyClass = Struct.new {}` and `Class.new` correctly (#20)
6
10
  * Magic comments (`# encoding: utf-8`) are not recognized as appropriate class documentation (#21)
7
11
  * Invalid UTF-8 is handled correctly (#22)
8
12
  * Gracefully handle unknown options
9
- * ABC check output uses a standard format (Foo::Bar#method rather than Foo > Bar > method)
13
+ * ABC check output uses a standard format (`Foo::Bar#method` rather than `Foo > Bar > method`)
10
14
  * **BREAKING** Add `--abc-exclude`, `--style-exclude` CLI flags, remove YAML support
11
15
  * **BREAKING-INTERNAL** Use hashes rather than explicit violation classes
12
16
  * **BREAKING-INTERNAL** Remove translator class, pass CLI args direct to checks
13
17
  * **INTERNAL** Wiring in a new check only requires changing one file (#15)
14
18
 
19
+ This snippet will convert your YAML exclusions file to the new CLI syntax:
20
+
21
+ y = YAML.load(File.read('exclusions.yml'))
22
+ puts (
23
+ y.fetch('abc', []).map {|x| %|--abc-exclude "#{x}"| } +
24
+ y.fetch('style', []).map {|x| %|--style-exclude "#{x}"| }
25
+ ).join("\n")
26
+
15
27
  ## 1.4.0 - 2 July 2012 (1afc999d)
16
28
 
17
29
  * Allow files and methods to be whitelisted (#16)
data/README.md CHANGED
@@ -32,7 +32,10 @@ Customize behaviour with a wealth of options:
32
32
  > cane --help
33
33
  Usage: cane [options]
34
34
 
35
- You can also put these options in a .cane file.
35
+ Default options are loaded from a .cane file in the current directory.
36
+
37
+ -r, --require FILE Load a Ruby file containing user-defined checks
38
+ -c, --check CLASS Use the given user-defined check
36
39
 
37
40
  --abc-glob GLOB Glob to run ABC metrics over (default: {app,lib}/**/*.rb)
38
41
  --abc-max VALUE Ignore methods under this complexity (default: 15)
@@ -54,16 +57,15 @@ Customize behaviour with a wealth of options:
54
57
  -v, --version Show version
55
58
  -h, --help Show this message
56
59
 
57
- Set default options into a `.cane` file:
60
+ Set default options using a `.cane` file:
58
61
 
59
62
  > cat .cane
60
63
  --no-doc
61
64
  --abc-glob **/*.rb
62
65
  > cane
63
66
 
64
- It works just like this:
65
-
66
- > cane --no-doc --abc-glob '**/*.rb'
67
+ It works exactly the same as specifying the options on the command-line.
68
+ Command-line arguments will override arguments specified in the `.cane` file.
67
69
 
68
70
  ## Integrating with Rake
69
71
 
@@ -94,7 +96,9 @@ on to an existing application that may already have many violations. By setting
94
96
  the maximum to the current number, no immediate changes will be required to
95
97
  your existing code base, but you will be protected from things getting worse.
96
98
 
97
- You can also consider defining exclusions for each violation (see below).
99
+ You may also consider beginning with high thresholds and ratcheting them down
100
+ over time, or defining exclusions for specific troublesome violations (not
101
+ recommended).
98
102
 
99
103
  ## Integrating with SimpleCov
100
104
 
@@ -120,12 +124,54 @@ You can use a `SimpleCov` formatter to create the required file:
120
124
 
121
125
  SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
122
126
 
127
+ ## Implementing your own checks
128
+
129
+ Checks must implement:
130
+
131
+ * A class level `options` method that returns a hash of available options. This
132
+ will be included in help output if the check is added before `--help`. If
133
+ your check does not require any configuration, return an empty hash.
134
+ * A one argument constructor, into which will be passed the options specified
135
+ for your check.
136
+ * A `violations` method that returns an array of violations.
137
+
138
+ See existing checks for guidance. Create your check in a new file:
139
+
140
+ # unhappy.rb
141
+ class UnhappyCheck < Struct.new(:opts)
142
+ def self.options
143
+ {
144
+ unhappy_file: ["File to check", default: [nil]]
145
+ }
146
+ end
147
+
148
+ def violations
149
+ [
150
+ description: "Files are unhappy",
151
+ file: opts.fetch(:unhappy_file),
152
+ label: ":("
153
+ ]
154
+ end
155
+ end
156
+
157
+ Include your check either using command-line options:
158
+
159
+ cane -r unhappy.rb --check UnhappyCheck --unhappy-file myfile
160
+
161
+ Or in your rake task:
162
+
163
+ require 'unhappy'
164
+
165
+ Cane::RakeTask.new(:quality) do |c|
166
+ c.use UnhappyCheck, unhappy_file: 'myfile'
167
+ end
168
+
123
169
  ## Compatibility
124
170
 
125
171
  Requires MRI 1.9, since it depends on the `ripper` library to calculate
126
172
  complexity metrics. This only applies to the Ruby used to run Cane, not the
127
173
  project it is being run against. In other words, you can run Cane against your
128
- 1.8 project.
174
+ 1.8 or JRuby project.
129
175
 
130
176
  ## Support
131
177
 
@@ -30,4 +30,5 @@ Gem::Specification.new do |gem|
30
30
  gem.add_development_dependency 'rspec', '~> 2.0'
31
31
  gem.add_development_dependency 'rake'
32
32
  gem.add_development_dependency 'simplecov'
33
+ gem.add_development_dependency 'rspec-fire'
33
34
  end
@@ -10,9 +10,9 @@ module Cane
10
10
  # hands the result to a formatter for display. This is the core of the
11
11
  # application, but for the actual entry point see `Cane::CLI`.
12
12
  class Runner
13
- def initialize(opts, checks)
14
- @opts = opts
15
- @checks = checks
13
+ def initialize(spec)
14
+ @opts = spec
15
+ @checks = spec[:checks]
16
16
  end
17
17
 
18
18
  def run
@@ -1,16 +1,16 @@
1
1
  require 'cane'
2
2
  require 'cane/version'
3
3
 
4
- require 'cane/cli/spec'
4
+ require 'cane/cli/parser'
5
5
 
6
6
  module Cane
7
7
  module CLI
8
8
  def run(args)
9
- opts = Spec.new.parse(args)
10
- if opts.is_a?(Hash)
11
- Cane.run(opts, Spec::CHECKS)
9
+ spec = Parser.parse(args)
10
+ if spec.is_a?(Hash)
11
+ Cane.run(spec)
12
12
  else
13
- opts
13
+ spec
14
14
  end
15
15
  end
16
16
  module_function :run
@@ -0,0 +1,28 @@
1
+ require 'cane/default_checks'
2
+
3
+ module Cane
4
+ module CLI
5
+ def defaults(check)
6
+ check.options.each_with_object({}) {|(k, v), h|
7
+ option_opts = v[1] || {}
8
+ if option_opts[:type] == Array
9
+ h[k] = []
10
+ else
11
+ h[k] = option_opts[:default]
12
+ end
13
+ }
14
+ end
15
+ module_function :defaults
16
+
17
+ def default_options
18
+ {
19
+ max_violations: 0,
20
+ exclusions_file: nil,
21
+ checks: Cane.default_checks
22
+ }.merge(Cane.default_checks.inject({}) {|a, check|
23
+ a.merge(defaults(check))
24
+ })
25
+ end
26
+ module_function :default_options
27
+ end
28
+ end
@@ -1,38 +1,31 @@
1
1
  require 'optparse'
2
2
 
3
- require 'cane/abc_check'
4
- require 'cane/style_check'
5
- require 'cane/doc_check'
6
- require 'cane/threshold_check'
3
+ require 'cane/default_checks'
4
+ require 'cane/cli/options'
5
+ require 'cane/version'
7
6
 
8
7
  module Cane
9
8
  module CLI
10
9
 
11
10
  # Provides a specification for the command line interface that drives
12
11
  # documentation, parsing, and default values.
13
- class Spec
14
- CHECKS = [AbcCheck, StyleCheck, DocCheck, ThresholdCheck]
15
-
16
- def self.defaults(check)
17
- x = check.options.each_with_object({}) {|(k, v), h|
18
- h[k] = (v[1] || {})[:default]
19
- }
20
- x
21
- end
22
-
23
- OPTIONS = {
24
- max_violations: 0,
25
- exclusions_file: nil,
26
- }.merge(CHECKS.inject({}) {|a, check| a.merge(defaults(check)) })
12
+ class Parser
27
13
 
28
14
  # Exception to indicate that no further processing is required and the
29
15
  # program can exit. This is used to handle --help and --version flags.
30
16
  class OptionsHandled < RuntimeError; end
31
17
 
32
- def initialize
18
+ def self.parse(*args)
19
+ new.parse(*args)
20
+ end
21
+
22
+ def initialize(stdout = $stdout)
23
+ @stdout = stdout
24
+
33
25
  add_banner
26
+ add_user_defined_checks
34
27
 
35
- CHECKS.each do |check|
28
+ Cane.default_checks.each do |check|
36
29
  add_check_options(check)
37
30
  end
38
31
 
@@ -45,7 +38,7 @@ module Cane
45
38
  def parse(args, ret = true)
46
39
  parser.parse!(get_default_options + args)
47
40
 
48
- OPTIONS.merge(options)
41
+ Cane::CLI.default_options.merge(options)
49
42
  rescue OptionParser::InvalidOption
50
43
  args = %w(--help)
51
44
  ret = false
@@ -55,8 +48,8 @@ module Cane
55
48
  end
56
49
 
57
50
  def get_default_options
58
- if ::File.exists?('./.cane')
59
- ::File.read('./.cane').gsub("\n", ' ').split(' ')
51
+ if Cane::File.exists?('./.cane')
52
+ Cane::File.contents('./.cane').split(/\s+/m)
60
53
  else
61
54
  []
62
55
  end
@@ -66,11 +59,26 @@ module Cane
66
59
  parser.banner = <<-BANNER
67
60
  Usage: cane [options]
68
61
 
69
- You can also put these options in a .cane file.
62
+ Default options are loaded from a .cane file in the current directory.
70
63
 
71
64
  BANNER
72
65
  end
73
66
 
67
+ def add_user_defined_checks
68
+ description = "Load a Ruby file containing user-defined checks"
69
+ parser.on("-r", "--require FILE", description) do |f|
70
+ load(f)
71
+ end
72
+
73
+ description = "Use the given user-defined check"
74
+ parser.on("-c", "--check CLASS", description) do |c|
75
+ check = Kernel.const_get(c)
76
+ options[:checks] << check
77
+ add_check_options(check)
78
+ end
79
+ parser.separator ""
80
+ end
81
+
74
82
  def add_check_options(check)
75
83
  check.options.each do |key, data|
76
84
  cli_key = key.to_s.tr('_', '-')
@@ -103,14 +111,14 @@ BANNER
103
111
 
104
112
  def add_version
105
113
  parser.on_tail("-v", "--version", "Show version") do
106
- puts Cane::VERSION
114
+ stdout.puts Cane::VERSION
107
115
  raise OptionsHandled
108
116
  end
109
117
  end
110
118
 
111
119
  def add_help
112
120
  parser.on_tail("-h", "--help", "Show this message") do
113
- puts parser
121
+ stdout.puts parser
114
122
  raise OptionsHandled
115
123
  end
116
124
  end
@@ -131,12 +139,16 @@ BANNER
131
139
  end
132
140
 
133
141
  def options
134
- @options ||= {}
142
+ @options ||= {
143
+ checks: Cane.default_checks
144
+ }
135
145
  end
136
146
 
137
147
  def parser
138
148
  @parser ||= OptionParser.new
139
149
  end
150
+
151
+ attr_reader :stdout
140
152
  end
141
153
 
142
154
  end
@@ -0,0 +1,16 @@
1
+ require 'cane/abc_check'
2
+ require 'cane/style_check'
3
+ require 'cane/doc_check'
4
+ require 'cane/threshold_check'
5
+
6
+ module Cane
7
+ def default_checks
8
+ [
9
+ AbcCheck,
10
+ StyleCheck,
11
+ DocCheck,
12
+ ThresholdCheck
13
+ ]
14
+ end
15
+ module_function :default_checks
16
+ end
@@ -31,7 +31,7 @@ module Cane
31
31
 
32
32
  def find_violations(file_name)
33
33
  last_line = ""
34
- Cane::File.iterator(file_name).map_with_index do |line, number|
34
+ Cane::File.iterator(file_name).map.with_index do |line, number|
35
35
  result = if class_definition?(line) && !comment?(last_line)
36
36
  {
37
37
  file: file_name,
@@ -1,35 +1,30 @@
1
1
  module Cane
2
2
 
3
3
  # Provides iteration over lines (from a file), correctly handling encoding.
4
- class EncodingAwareIterator < Struct.new(:lines)
5
- def map_with_index(&block)
6
- lines.map.with_index do |line, index|
7
- with_encoding_retry(line) do
8
- block.call(line, index)
9
- end
10
- end
4
+ class EncodingAwareIterator
5
+ include Enumerable
6
+
7
+ def initialize(lines)
8
+ @lines = lines
11
9
  end
12
10
 
13
- protected
11
+ def each(&block)
12
+ return Enumerator.new(self, :each) unless block
14
13
 
15
- # This is to avoid re-encoding every line, since most are valid. I should
16
- # performance test this but haven't (maybe can just re-encode always but my
17
- # hunch says no).
18
- def with_encoding_retry(line, &block)
19
- retried = false
20
- begin
21
- block.call(line)
22
- rescue ArgumentError
23
- if retried
24
- # I haven't seen input that causes this to occur. Please report it!
25
- raise
26
- else
14
+ lines.each do |line|
15
+ begin
16
+ line =~ /\s/
17
+ rescue ArgumentError
27
18
  line.encode!('UTF-8', 'UTF-8', invalid: :replace)
28
- retried = true
29
- retry
30
19
  end
20
+
21
+ block.call(line)
31
22
  end
32
23
  end
24
+
25
+ protected
26
+
27
+ attr_reader :lines
33
28
  end
34
29
 
35
30
  end
@@ -17,6 +17,10 @@ module Cane
17
17
  def open(path)
18
18
  ::File.open(path, 'r:utf-8')
19
19
  end
20
+
21
+ def exists?(path)
22
+ ::File.exists?(path)
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -1,7 +1,7 @@
1
1
  require 'rake'
2
2
  require 'rake/tasklib'
3
3
 
4
- require 'cane/cli/spec'
4
+ require 'cane/cli/options'
5
5
 
6
6
  module Cane
7
7
  # Creates a rake task to run cane with given configuration.
@@ -17,22 +17,36 @@ module Cane
17
17
  # end
18
18
  class RakeTask < ::Rake::TaskLib
19
19
  attr_accessor :name
20
- OPTIONS = Cane::CLI::Spec::OPTIONS
21
- OPTIONS.each do |name, value|
22
- attr_accessor name
20
+ attr_reader :options
21
+
22
+ Cane::CLI.default_options.keys.each do |name|
23
+ define_method(name) do
24
+ options.fetch(name)
25
+ end
26
+
27
+ define_method("#{name}=") do |v|
28
+ options[name] = v
29
+ end
23
30
  end
24
31
 
25
32
  # Add a threshold check. If the file exists and it contains a number,
26
33
  # compare that number with the given value using the operator.
27
34
  def add_threshold(file, operator, value)
28
35
  if operator == :>=
29
- @gte << [file, value]
36
+ @options[:gte] << [file, value]
30
37
  end
31
38
  end
32
39
 
40
+ def use(check, options = {})
41
+ @options.merge!(options)
42
+ @options[:checks] = @options[:checks] + [check]
43
+ end
44
+
33
45
  def initialize(task_name = nil)
34
46
  self.name = task_name || :cane
35
47
  @gte = []
48
+ @options = Cane::CLI.default_options
49
+
36
50
  yield self if block_given?
37
51
 
38
52
  unless ::Rake.application.last_comment
@@ -41,15 +55,7 @@ module Cane
41
55
 
42
56
  task name do
43
57
  require 'cane/cli'
44
- abort unless Cane.run(OPTIONS.merge(options), Cane::CLI::Spec::CHECKS)
45
- end
46
- end
47
-
48
- def options
49
- OPTIONS.keys.inject({}) do |opts, setting|
50
- value = self.send(setting)
51
- opts[setting] = value unless value.nil?
52
- opts
58
+ abort unless Cane.run(options)
53
59
  end
54
60
  end
55
61
  end
@@ -67,7 +67,7 @@ module Cane
67
67
  end
68
68
 
69
69
  def map_lines(file_path, &block)
70
- Cane::File.iterator(file_path).map_with_index(&block)
70
+ Cane::File.iterator(file_path).map.with_index(&block)
71
71
  end
72
72
 
73
73
  def exclusions
@@ -1,3 +1,3 @@
1
1
  module Cane
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1.0'
3
3
  end
@@ -2,26 +2,14 @@ require 'spec_helper'
2
2
  require "stringio"
3
3
  require 'cane/cli'
4
4
 
5
- describe 'Cane' do
6
- def capture_stdout &block
7
- real_stdout, $stdout = $stdout, StringIO.new
8
- yield
9
- $stdout.string
10
- ensure
11
- $stdout = real_stdout
12
- end
5
+ require 'cane/rake_task'
13
6
 
14
- def run(cli_args)
15
- result = nil
16
- output = capture_stdout do
17
- result = Cane::CLI.run(['--no-abc'] + cli_args.split(' '))
18
- end
7
+ # Acceptance tests
8
+ describe 'The cane application' do
9
+ let(:class_name) { "C#{rand(10 ** 10)}" }
19
10
 
20
- [output, result ? 0 : 1]
21
- end
22
-
23
- it 'fails if ABC metric does not meet requirements' do
24
- file_name = make_file(<<-RUBY)
11
+ it 'returns a non-zero exit code and a details of checks that failed' do
12
+ fn = make_file(<<-RUBY + " ")
25
13
  class Harness
26
14
  def complex_method(a)
27
15
  if a < 2
@@ -33,126 +21,37 @@ describe 'Cane' do
33
21
  end
34
22
  RUBY
35
23
 
36
- _, exitstatus = run("--abc-glob #{file_name} --abc-max 1")
37
-
38
- exitstatus.should == 1
39
- end
40
-
41
- it 'fails if style metrics do not meet requirements' do
42
- file_name = make_file("whitespace ")
43
-
44
- output, exitstatus = run("--style-glob #{file_name}")
45
- output.should include("Lines violated style requirements")
46
- exitstatus.should == 1
47
- end
48
-
49
- it 'allows measure to be configured' do
50
- file_name = make_file("toolong")
51
-
52
- output, exitstatus = run("--style-glob #{file_name} --style-measure 3")
53
- exitstatus.should == 1
54
- output.should include("Lines violated style requirements")
55
- end
56
-
57
- it 'does not include trailing new lines in the character count' do
58
- file_name = make_file('#' * 80 + "\n" + '#' * 80)
59
-
60
- output, exitstatus = run("--style-glob #{file_name} --style-measure 80")
61
- exitstatus.should == 0
62
- output.should == ""
63
- end
64
-
65
- it 'allows upper bound of failed checks' do
66
- file_name = make_file("whitespace ")
67
-
68
- output, exitstatus = run("--style-glob #{file_name} --max-violations 1")
69
- exitstatus.should == 0
70
- output.should include("Lines violated style requirements")
71
- end
72
-
73
- it 'allows checking of a value in a file' do
74
- file_name = make_file("89")
75
-
76
- output, exitstatus = run("--gte #{file_name},90")
77
- output.should include("Quality threshold crossed")
78
- exitstatus.should == 1
79
- end
80
-
81
- it 'allows checking of class documentation' do
82
- file_name = make_file("class NoDoc")
83
-
84
- output, exitstatus = run("--doc-glob #{file_name}")
85
- exitstatus.should == 1
86
- output.should include("Classes are not documented")
87
- end
88
-
89
- context 'with a .cane file' do
90
- before(:each) do
91
- file_name = make_file("class NoDoc")
92
- make_dot_cane("--doc-glob #{file_name}")
93
- end
94
-
95
- after(:each) do
96
- unmake_dot_cane
97
- end
98
-
99
- it 'loads options from a .cane file' do
100
- output, exitstatus = run('')
101
-
102
- exitstatus.should == 1
103
- output.should include("Classes are not documented")
104
- end
105
- end
106
-
107
- it 'displays a help message' do
108
- output, exitstatus = run("--help")
109
-
110
- exitstatus.should == 0
111
- output.should include("Usage:")
112
- end
113
-
114
- it 'displays version' do
115
- output, exitstatus = run("--version")
116
-
117
- exitstatus.should == 0
118
- output.should include(Cane::VERSION)
119
- end
120
-
121
- it 'uses the last of conflicting arguments' do
122
- file_name = make_file("class NoDoc")
123
-
124
- run("--doc-glob #{file_name} --no-doc").should ==
125
- run("--no-doc")
126
-
127
- run("--no-doc --doc-glob #{file_name}").should ==
128
- run("--doc-glob #{file_name}")
129
- end
24
+ check_file = make_file <<-RUBY
25
+ class #{class_name} < Struct.new(:opts)
26
+ def self.options
27
+ {
28
+ unhappy_file: ["File to check", default: [nil]]
29
+ }
30
+ end
130
31
 
131
- it 'supports exclusions' do
132
- line_with_whitespace = "whitespace "
133
- file_name = make_file(<<-RUBY)
134
- # #{line_with_whitespace}
135
- class Harness
136
- def complex_method(a)
137
- if a < 2
138
- return "low"
139
- else
140
- return "high"
141
- end
32
+ def violations
33
+ [
34
+ description: "Files are unhappy",
35
+ file: opts.fetch(:unhappy_file),
36
+ label: ":("
37
+ ]
142
38
  end
143
39
  end
144
40
  RUBY
145
41
 
146
- options = [
147
- "--abc-glob", file_name,
148
- "--abc-exclude", "Harness#complex_method",
149
- "--abc-max", 1,
150
- "--style-glob", file_name,
151
- "--style-exclude", file_name
152
- ].join(' ')
153
-
154
- _, exitstatus = run(options)
155
- exitstatus.should == 0
42
+ output, exitstatus = run %(
43
+ --style-glob #{fn}
44
+ --doc-glob #{fn}
45
+ --abc-glob #{fn}
46
+ --abc-max 1
47
+ -r #{check_file}
48
+ --check #{class_name}
49
+ --unhappy-file #{fn}
50
+ )
51
+ output.should include("Lines violated style requirements")
52
+ output.should include("Classes are not documented")
53
+ output.should include("Methods exceeded maximum allowed ABC complexity")
54
+ exitstatus.should == 1
156
55
  end
157
56
 
158
57
  it 'handles invalid unicode input' do
@@ -163,10 +62,20 @@ describe 'Cane' do
163
62
  exitstatus.should == 0
164
63
  end
165
64
 
166
- it 'handles invalid options by showing help' do
167
- out, exitstatus = run("--bogus")
65
+ after do
66
+ if Object.const_defined?(class_name)
67
+ Object.send(:remove_const, class_name)
68
+ end
69
+ end
168
70
 
169
- out.should include("Usage:")
170
- exitstatus.should == 1
71
+ def run(cli_args)
72
+ result = nil
73
+ output = capture_stdout do
74
+ result = Cane::CLI.run(
75
+ %w(--no-abc --no-style --no-doc) + cli_args.split(/\s+/m)
76
+ )
77
+ end
78
+
79
+ [output, result ? 0 : 1]
171
80
  end
172
81
  end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane/cli'
4
+
5
+ describe Cane::CLI do
6
+ describe '.run' do
7
+
8
+ let!(:parser) { fire_replaced_class_double("Cane::CLI::Parser") }
9
+ let!(:cane) { fire_replaced_class_double("Cane") }
10
+
11
+ it 'runs Cane with the given arguments' do
12
+ parser.should_receive(:parse).with("--args").and_return(args: true)
13
+ cane.should_receive(:run).with(args: true).and_return("tracer")
14
+
15
+ described_class.run("--args").should == "tracer"
16
+ end
17
+
18
+ it 'does not run Cane if parser was able to handle input' do
19
+ parser.should_receive(:parse).with("--args").and_return("tracer")
20
+
21
+ described_class.run("--args").should == "tracer"
22
+ end
23
+ end
24
+ end
@@ -8,7 +8,8 @@ require 'cane/encoding_aware_iterator'
8
8
  describe Cane::EncodingAwareIterator do
9
9
  it 'handles non-UTF8 input' do
10
10
  lines = ["\xc3\x28"]
11
- result = described_class.new(lines).map_with_index do |line, number|
11
+ result = described_class.new(lines).map.with_index do |line, number|
12
+ line.should be_kind_of(String)
12
13
  [line =~ /\s/, number]
13
14
  end
14
15
  result.should == [[nil, 0]]
@@ -16,9 +17,17 @@ describe Cane::EncodingAwareIterator do
16
17
 
17
18
  it 'does not enter an infinite loop on persistently bad input' do
18
19
  ->{
19
- described_class.new([""]).map_with_index do |line, number|
20
+ described_class.new([""]).map.with_index do |line, number|
20
21
  "\xc3\x28" =~ /\s/
21
22
  end
22
23
  }.should raise_error(ArgumentError)
23
24
  end
25
+
26
+ it 'allows each with no block' do
27
+ called_with_line = nil
28
+ described_class.new([""]).each.with_index do |line, number|
29
+ called_with_line = line
30
+ end
31
+ called_with_line.should == ""
32
+ end
24
33
  end
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+ require "stringio"
3
+ require 'cane/cli/parser'
4
+
5
+ describe Cane::CLI::Parser do
6
+ def run(cli_args)
7
+ result = nil
8
+ output = StringIO.new("")
9
+ result = Cane::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 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 upper bound of failed checks' do
26
+ output, result = run("--max-violations 1")
27
+ result[:max_violations].should == 1
28
+ end
29
+
30
+ it 'displays a help message' do
31
+ output, result = run("--help")
32
+
33
+ result.should be
34
+ output.should include("Usage:")
35
+ end
36
+
37
+ it 'handles invalid options by showing help' do
38
+ output, result = run("--bogus")
39
+
40
+ output.should include("Usage:")
41
+ result.should_not be
42
+ end
43
+
44
+ it 'displays version' do
45
+ output, result = run("--version")
46
+
47
+ result.should be
48
+ output.should include(Cane::VERSION)
49
+ end
50
+
51
+ it 'supports exclusions' do
52
+ options = [
53
+ "--abc-exclude", "Harness#complex_method",
54
+ "--style-exclude", 'myfile'
55
+ ].join(' ')
56
+
57
+ _, result = run(options)
58
+ result[:abc_exclude].should == [['Harness#complex_method']]
59
+ result[:style_exclude].should == [['myfile']]
60
+ end
61
+
62
+ describe 'argument ordering' do
63
+ it 'gives precedence to the last argument #1' do
64
+ _, result = run("--doc-glob myfile --no-doc")
65
+ result[:no_doc].should be
66
+
67
+ _, result = run("--no-doc --doc-glob myfile")
68
+ result[:no_doc].should_not be
69
+ end
70
+ end
71
+
72
+ it 'loads default options from .cane file' do
73
+ defaults = <<-EOS
74
+ --no-doc
75
+ --abc-glob myfile
76
+ --style-glob myfile
77
+ EOS
78
+ file = fire_replaced_class_double("Cane::File")
79
+ stub_const("Cane::File", file)
80
+ file.should_receive(:exists?).with('./.cane').and_return(true)
81
+ file.should_receive(:contents).with('./.cane').and_return(defaults)
82
+
83
+ _, result = run("--style-glob myotherfile")
84
+
85
+ result[:no_doc].should be
86
+ result[:abc_glob].should == 'myfile'
87
+ result[:style_glob].should == 'myotherfile'
88
+ end
89
+ end
@@ -3,11 +3,34 @@ require 'spec_helper'
3
3
  require 'cane/rake_task'
4
4
 
5
5
  describe Cane::RakeTask do
6
- it 'adds a new threshold' do
7
- task = described_class.new do |t|
8
- t.add_threshold 'coverage', :>=, 99
6
+ it 'enables cane to be configured an run via rake' do
7
+ fn = make_file("90")
8
+ my_check = Class.new(Struct.new(:opts)) do
9
+ def violations
10
+ [description: 'test', label: opts.fetch(:some_opt)]
11
+ end
9
12
  end
10
13
 
11
- task.options[:gte].should == [["coverage", 99]]
14
+ task = Cane::RakeTask.new(:quality) do |cane|
15
+ cane.no_abc = true
16
+ cane.no_doc = true
17
+ cane.no_style = true
18
+ cane.add_threshold fn, :>=, 99
19
+ cane.use my_check, some_opt: "theopt"
20
+ end
21
+
22
+ task.no_abc.should == true
23
+
24
+ task.should_receive(:abort)
25
+ out = capture_stdout do
26
+ Rake::Task['quality'].invoke
27
+ end
28
+
29
+ out.should include("Quality threshold crossed")
30
+ out.should include("theopt")
31
+ end
32
+
33
+ after do
34
+ Rake::Task.clear
12
35
  end
13
36
  end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane'
4
+
5
+ describe Cane::Runner do
6
+ describe '#run' do
7
+ it 'returns true iff fewer violations than max allowed' do
8
+ described_class.new(checks: [], max_violations: 0).run.should be
9
+ described_class.new(checks: [], max_violations: -1).run.should_not be
10
+ end
11
+ end
12
+ end
@@ -1,4 +1,20 @@
1
+ require 'rspec/fire'
1
2
  require 'tempfile'
3
+ require 'stringio'
4
+ require 'rake'
5
+ require 'rake/tasklib'
6
+
7
+ RSpec.configure do |config|
8
+ config.include(RSpec::Fire)
9
+
10
+ def capture_stdout &block
11
+ real_stdout, $stdout = $stdout, StringIO.new
12
+ yield
13
+ $stdout.string
14
+ ensure
15
+ $stdout = real_stdout
16
+ end
17
+ end
2
18
 
3
19
  # Keep a reference to all tempfiles so they are not garbage collected until the
4
20
  # process exits.
@@ -12,16 +28,6 @@ def make_file(content)
12
28
  tempfile.path
13
29
  end
14
30
 
15
- def make_dot_cane(content)
16
- File.open('./.cane', 'w') do |f|
17
- f.puts content
18
- end
19
- end
20
-
21
- def unmake_dot_cane
22
- FileUtils.rm('./.cane')
23
- end
24
-
25
31
  require 'simplecov'
26
32
 
27
33
  class SimpleCov::Formatter::QualityFormatter
@@ -18,8 +18,8 @@ describe Cane::StyleCheck do
18
18
  it 'creates a StyleViolation for each method above the threshold' do
19
19
  file_name = make_file(ruby_with_style_issue)
20
20
 
21
- violations = check(file_name, style_measure: 80).violations
22
- violations.length.should == 2
21
+ violations = check(file_name, style_measure: 8).violations
22
+ violations.length.should == 3
23
23
  end
24
24
 
25
25
  it 'skips declared exclusions' do
@@ -32,4 +32,16 @@ describe Cane::StyleCheck do
32
32
 
33
33
  violations.length.should == 0
34
34
  end
35
+
36
+ it 'does not include trailing new lines in the character count' do
37
+ file_name = make_file('#' * 80 + "\n" + '#' * 80)
38
+
39
+ violations = check(file_name,
40
+ style_measure: 80,
41
+ style_exclude: [file_name]
42
+ ).violations
43
+
44
+ violations.length.should == 0
45
+ end
46
+
35
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cane
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-20 00:00:00.000000000 Z
12
+ date: 2012-08-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: !ruby/object:Gem::Requirement
16
+ requirement: &2152567380 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,15 +21,10 @@ dependencies:
21
21
  version: '2.0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ~>
28
- - !ruby/object:Gem::Version
29
- version: '2.0'
24
+ version_requirements: *2152567380
30
25
  - !ruby/object:Gem::Dependency
31
26
  name: rake
32
- requirement: !ruby/object:Gem::Requirement
27
+ requirement: &2152566600 !ruby/object:Gem::Requirement
33
28
  none: false
34
29
  requirements:
35
30
  - - ! '>='
@@ -37,15 +32,10 @@ dependencies:
37
32
  version: '0'
38
33
  type: :development
39
34
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: '0'
35
+ version_requirements: *2152566600
46
36
  - !ruby/object:Gem::Dependency
47
37
  name: simplecov
48
- requirement: !ruby/object:Gem::Requirement
38
+ requirement: &2152565760 !ruby/object:Gem::Requirement
49
39
  none: false
50
40
  requirements:
51
41
  - - ! '>='
@@ -53,12 +43,18 @@ dependencies:
53
43
  version: '0'
54
44
  type: :development
55
45
  prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
46
+ version_requirements: *2152565760
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-fire
49
+ requirement: &2152564960 !ruby/object:Gem::Requirement
57
50
  none: false
58
51
  requirements:
59
52
  - - ! '>='
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2152564960
62
58
  description: Fails your build if code quality thresholds are not met
63
59
  email:
64
60
  - xavier@squareup.com
@@ -69,16 +65,21 @@ extra_rdoc_files: []
69
65
  files:
70
66
  - spec/abc_check_spec.rb
71
67
  - spec/cane_spec.rb
68
+ - spec/cli_spec.rb
72
69
  - spec/doc_check_spec.rb
73
70
  - spec/encoding_aware_iterator_spec.rb
71
+ - spec/parser_spec.rb
74
72
  - spec/rake_task_spec.rb
73
+ - spec/runner_spec.rb
75
74
  - spec/spec_helper.rb
76
75
  - spec/style_check_spec.rb
77
76
  - spec/threshold_check_spec.rb
78
77
  - spec/violation_formatter_spec.rb
79
78
  - lib/cane/abc_check.rb
80
- - lib/cane/cli/spec.rb
79
+ - lib/cane/cli/options.rb
80
+ - lib/cane/cli/parser.rb
81
81
  - lib/cane/cli.rb
82
+ - lib/cane/default_checks.rb
82
83
  - lib/cane/doc_check.rb
83
84
  - lib/cane/encoding_aware_iterator.rb
84
85
  - lib/cane/file.rb
@@ -113,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
114
  version: '0'
114
115
  requirements: []
115
116
  rubyforge_project:
116
- rubygems_version: 1.8.24
117
+ rubygems_version: 1.8.6
117
118
  signing_key:
118
119
  specification_version: 3
119
120
  summary: Fails your build if code quality thresholds are not met. Provides complexity
@@ -121,9 +122,12 @@ summary: Fails your build if code quality thresholds are not met. Provides compl
121
122
  test_files:
122
123
  - spec/abc_check_spec.rb
123
124
  - spec/cane_spec.rb
125
+ - spec/cli_spec.rb
124
126
  - spec/doc_check_spec.rb
125
127
  - spec/encoding_aware_iterator_spec.rb
128
+ - spec/parser_spec.rb
126
129
  - spec/rake_task_spec.rb
130
+ - spec/runner_spec.rb
127
131
  - spec/spec_helper.rb
128
132
  - spec/style_check_spec.rb
129
133
  - spec/threshold_check_spec.rb