cane 1.0.0

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,5 @@
1
+ # Cane History
2
+
3
+ ## 1.0.0 - 14 January 2012 (4e400534)
4
+
5
+ Initial release.
@@ -0,0 +1,89 @@
1
+ # Cane
2
+
3
+ Fails your build if code quality thresholds are not met.
4
+
5
+ > Discipline will set you free.
6
+
7
+ ## Usage
8
+
9
+ gem install cane
10
+ cane --abc-glob '{lib,spec}/**/*.rb' --abc-max 15
11
+
12
+ Your main build task should run this, probably via `bundle exec`. It will have
13
+ a non-zero exit code if any quality checks fail. Also, a report:
14
+
15
+ > cane
16
+
17
+ Methods exceeded maximum allowed ABC complexity (2):
18
+
19
+ lib/cane.rb Cane > sample 23
20
+ lib/cane.rb Cane > sample_2 17
21
+
22
+ Lines violated style requirements (2):
23
+
24
+ lib/cane.rb:20 Line length >80
25
+ lib/cane.rb:42 Trailing whitespace
26
+
27
+ Classes are not documented (1):
28
+ lib/cane:3 SomeClass
29
+
30
+ Customize behaviour with a wealth of options:
31
+
32
+ > cane --help
33
+ Usage: cane [options]
34
+ --abc-glob GLOB Glob to run ABC metrics over (default: lib/**/*.rb)
35
+ --abc-max VALUE Report any methods with complexity greater than VALUE (default: 15)
36
+ --no-abc Disable ABC checking
37
+
38
+ --style-glob GLOB Glob to run style metrics over (default: {lib,spec}/**/*.rb)
39
+ --style-measure VALUE Max line length (default: 80)
40
+ --no-style Disable style checking
41
+
42
+ --doc-glob GLOB Glob to run documentation metrics over (default: lib/**/*.rb)
43
+ --no-doc Disable documentation checking
44
+
45
+ --gte FILE,THRESHOLD If FILE contains a single number, verify it is >= to THRESHOLD.
46
+
47
+ --max-violations VALUE Max allowed violations (default: 0)
48
+
49
+ --version Show version
50
+ -h, --help Show this message
51
+
52
+ ## Adding to a legacy project
53
+
54
+ Cane can be configured to still pass in the presence of a set number of
55
+ violations using the `--max-violations` option. This is ideal for retrofitting
56
+ on to an existing application that may already have many violations. By setting
57
+ the maximum to the current number, no immediate changes will be required to
58
+ your existing code base, but you will be protected from things getting worse.
59
+
60
+ ## Integrating with SimpleCov
61
+
62
+ Any value in a file can be used as a threshold:
63
+
64
+ > echo "89" > coverage/covered_percent
65
+ > cane --gte 'coverage/covered_percent,90'
66
+
67
+ Quality threshold crossed
68
+
69
+ coverage/covered_percent is 89, should be >= 90
70
+
71
+ You can use a `SimpleCov` formatter to create the required file:
72
+
73
+ class SimpleCov::Formatter::QualityFormatter
74
+ def format(result)
75
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
76
+ File.open("coverage/covered_percent", "w") do |f|
77
+ f.puts result.source_files.covered_percent.to_f
78
+ end
79
+ end
80
+ end
81
+
82
+ SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
83
+
84
+ ## Compatibility
85
+
86
+ Requires MRI 1.9, since it depends on the `ripper` library to calculate
87
+ complexity metrics. This only applies to the Ruby used to run Cane, not the
88
+ project it is being run against. In other words, you can run Cane against your
89
+ 1.8 project.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+
5
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
6
+
7
+ require 'cane/cli'
8
+
9
+ result = Cane::CLI.run(ARGV)
10
+
11
+ exit(1) unless result
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/cane/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Xavier Shay"]
6
+ gem.email = ["xavier@squareup.com"]
7
+ gem.description =
8
+ %q{Fails your build if code quality thresholds are not met}
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/square/cane"
15
+
16
+ gem.executables = []
17
+ gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
18
+ README.md
19
+ HISTORY.md
20
+ cane.gemspec
21
+ )
22
+ gem.test_files = Dir.glob("spec/**/*.rb")
23
+ gem.name = "cane"
24
+ gem.require_paths = ["lib"]
25
+ gem.bindir = "bin"
26
+ gem.executables << "cane"
27
+ gem.version = Cane::VERSION
28
+ gem.has_rdoc = false
29
+ gem.add_dependency 'tailor'
30
+ gem.add_development_dependency 'rspec', '~> 2.0'
31
+ gem.add_development_dependency 'rake'
32
+ gem.add_development_dependency 'simplecov'
33
+ end
@@ -0,0 +1,49 @@
1
+ require 'cane/abc_check'
2
+ require 'cane/style_check'
3
+ require 'cane/doc_check'
4
+ require 'cane/threshold_check'
5
+ require 'cane/violation_formatter'
6
+
7
+ module Cane
8
+ def run(opts)
9
+ Runner.new(opts).run
10
+ end
11
+ module_function :run
12
+
13
+ # Orchestrates the running of checks per the provided configuration, and
14
+ # hands the result to a formatter for display. This is the core of the
15
+ # application, but for the actual entry point see `Cane::CLI`.
16
+ class Runner
17
+ CHECKERS = {
18
+ abc: AbcCheck,
19
+ style: StyleCheck,
20
+ doc: DocCheck,
21
+ threshold: ThresholdCheck
22
+ }
23
+
24
+ def initialize(opts)
25
+ @opts = opts
26
+ end
27
+
28
+ def run
29
+ outputter.print ViolationFormatter.new(violations)
30
+
31
+ violations.length <= opts.fetch(:max_violations)
32
+ end
33
+
34
+ protected
35
+
36
+ attr_reader :opts
37
+
38
+ def violations
39
+ @violations ||= CHECKERS.
40
+ select { |key, _| opts.has_key?(key) }.
41
+ map { |key, check| check.new(opts.fetch(key)).violations }.
42
+ flatten
43
+ end
44
+
45
+ def outputter
46
+ opts.fetch(:out, $stdout)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,96 @@
1
+ require 'ripper'
2
+
3
+ require 'cane/abc_max_violation'
4
+
5
+ module Cane
6
+
7
+ # Creates violations for methods that are too complicated using a simple
8
+ # algorithm run against the parse tree of a file to count assignments,
9
+ # branches, and conditionals. Borrows heavily from metric_abc.
10
+ class AbcCheck < Struct.new(:opts)
11
+ def violations
12
+ order file_names.map { |file_name|
13
+ find_violations(file_name)
14
+ }.flatten
15
+ end
16
+
17
+ protected
18
+
19
+ def find_violations(file_name)
20
+ ast = sexps_from_file(file_name)
21
+
22
+ process_ast(ast).
23
+ select { |nesting, complexity| complexity > max_allowed_complexity }.
24
+ map { |x| AbcMaxViolation.new(file_name, x.first, x.last) }
25
+ end
26
+
27
+ # Recursive function to process an AST. The `complexity` variable mutates,
28
+ # which is a bit confusing. `nesting` does not.
29
+ def process_ast(node, complexity = {}, nesting = [])
30
+ if method_nodes.include?(node[0])
31
+ nesting = nesting + [node[1][1]]
32
+ complexity[nesting.join(" > ")] = calculate_abc(node)
33
+ elsif container_nodes.include?(node[0])
34
+ parent = if node[1][1][1].is_a?(Symbol)
35
+ node[1][1][1]
36
+ else
37
+ node[1][-1][1]
38
+ end
39
+ nesting = nesting + [parent]
40
+ end
41
+
42
+ if node.is_a? Array
43
+ node[1..-1].each { |n| process_ast(n, complexity, nesting) if n }
44
+ end
45
+ complexity
46
+ end
47
+
48
+ def sexps_from_file(file_name)
49
+ Ripper::SexpBuilder.new(File.open(file_name, 'r:utf-8').read).parse
50
+ end
51
+
52
+ def max_allowed_complexity
53
+ opts.fetch(:max)
54
+ end
55
+
56
+ def calculate_abc(method_node)
57
+ a = count_nodes(method_node, assignment_nodes)
58
+ b = count_nodes(method_node, branch_nodes) + 1
59
+ c = count_nodes(method_node, condition_nodes)
60
+ abc = Math.sqrt(a**2 + b**2 + c**2).round
61
+ abc
62
+ end
63
+
64
+ def count_nodes(node, types)
65
+ node.flatten.select { |n| types.include?(n) }.length
66
+ end
67
+
68
+ def file_names
69
+ Dir[opts.fetch(:files)]
70
+ end
71
+
72
+ def order(result)
73
+ result.sort_by(&:complexity).reverse
74
+ end
75
+
76
+ def assignment_nodes
77
+ [:assign, :opassign]
78
+ end
79
+
80
+ def method_nodes
81
+ [:def]
82
+ end
83
+
84
+ def container_nodes
85
+ [:class, :module]
86
+ end
87
+
88
+ def branch_nodes
89
+ [:call, :fcall, :brace_block, :do_block]
90
+ end
91
+
92
+ def condition_nodes
93
+ [:==, :===, :"<>", :"<=", :">=", :"=~", :>, :<, :else, :"<=>"]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,13 @@
1
+ module Cane
2
+
3
+ # Value object used by AbcCheck for a method that is too complicated.
4
+ class AbcMaxViolation < Struct.new(:file_name, :detail, :complexity)
5
+ def columns
6
+ [file_name, detail, complexity]
7
+ end
8
+
9
+ def description
10
+ "Methods exceeded maximum allowed ABC complexity"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ require 'cane'
2
+ require 'cane/version'
3
+
4
+ require 'cane/cli/spec'
5
+ require 'cane/cli/translator'
6
+
7
+ module Cane
8
+ module CLI
9
+
10
+ def run(args)
11
+ opts = Spec.new.parse(args)
12
+ if opts
13
+ Cane.run(opts)
14
+ else
15
+ true
16
+ end
17
+ end
18
+ module_function :run
19
+
20
+ end
21
+ end
@@ -0,0 +1,114 @@
1
+ require 'cane/cli/translator'
2
+
3
+ module Cane
4
+ module CLI
5
+
6
+ # Provides a specification for the command line interface that drives
7
+ # documentation, parsing, and default values.
8
+ class Spec
9
+ DEFAULTS = {
10
+ abc_glob: 'lib/**/*.rb',
11
+ abc_max: '15',
12
+ style_glob: '{lib,spec}/**/*.rb',
13
+ style_measure: '80',
14
+ doc_glob: 'lib/**/*.rb',
15
+ max_violations: '0',
16
+ }
17
+
18
+ # Exception to indicate that no further processing is required and the
19
+ # program can exit. This is used to handle --help and --version flags.
20
+ class OptionsHandled < RuntimeError; end
21
+
22
+ def initialize
23
+ add_abc_options
24
+ add_style_options
25
+ add_doc_options
26
+ add_threshold_options
27
+ add_cane_options
28
+
29
+ add_version
30
+ add_help
31
+ end
32
+
33
+ def parse(args)
34
+ parser.parse!(args)
35
+ Translator.new(options, DEFAULTS).to_hash
36
+ rescue OptionsHandled
37
+ nil
38
+ end
39
+
40
+ def add_abc_options
41
+ add_option %w(--abc-glob GLOB), "Glob to run ABC metrics over"
42
+ add_option %w(--abc-max VALUE), "Ignore methods under this complexity"
43
+ add_option %w(--no-abc), "Disable ABC checking"
44
+
45
+ parser.separator ""
46
+ end
47
+
48
+ def add_style_options
49
+ add_option %w(--style-glob GLOB), "Glob to run style metrics over"
50
+ add_option %w(--style-measure VALUE), "Max line length"
51
+ add_option %w(--no-style), "Disable style checking"
52
+
53
+ parser.separator ""
54
+ end
55
+
56
+ def add_doc_options
57
+ add_option %w(--doc-glob GLOB), "Glob to run documentation checks over"
58
+ add_option %w(--no-doc), "Disable documentation checking"
59
+
60
+ parser.separator ""
61
+ end
62
+
63
+ def add_threshold_options
64
+ desc = "If FILE contains a number, verify it is >= to THRESHOLD."
65
+ parser.on("--gte FILE,THRESHOLD", Array, desc) do |opts|
66
+ (options[:threshold] ||= []) << opts.unshift(:>=)
67
+ end
68
+
69
+ parser.separator ""
70
+ end
71
+
72
+ def add_cane_options
73
+ add_option %w(--max-violations VALUE), "Max allowed violations"
74
+
75
+ parser.separator ""
76
+ end
77
+
78
+ def add_version
79
+ parser.on_tail("--version", "Show version") do
80
+ puts Cane::VERSION
81
+ raise OptionsHandled
82
+ end
83
+ end
84
+
85
+ def add_help
86
+ parser.on_tail("-h", "--help", "Show this message") do
87
+ puts parser
88
+ raise OptionsHandled
89
+ end
90
+ end
91
+
92
+ def add_option(option, description)
93
+ option_key = option[0].gsub('--', '').tr('-', '_').to_sym
94
+
95
+ if DEFAULTS.has_key?(option_key)
96
+ description += " (default: %s)" % DEFAULTS[option_key]
97
+ end
98
+
99
+ parser.on(option.join(' '), description) do |v|
100
+ options[option_key] = v
101
+ end
102
+ end
103
+
104
+ def options
105
+ @options ||= {}
106
+ end
107
+
108
+ def parser
109
+ @parser ||= OptionParser.new
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,49 @@
1
+ module Cane
2
+ module CLI
3
+
4
+ # Translates CLI options with given defaults to a hash suitable to be
5
+ # passed to `Cane.run`.
6
+ class Translator < Struct.new(:options, :defaults)
7
+ def to_hash
8
+ result = {}
9
+ translate_abc_options(result)
10
+ translate_doc_options(result)
11
+ translate_style_options(result)
12
+
13
+ result[:threshold] = options.fetch(:threshold, [])
14
+ result[:max_violations] = option_with_default(:max_violations).to_i
15
+
16
+ result
17
+ end
18
+
19
+ def translate_abc_options(result)
20
+ result[:abc] = {
21
+ files: option_with_default(:abc_glob),
22
+ max: option_with_default(:abc_max).to_i
23
+ } unless check_disabled(:no_abc, [:abc_glob, :abc_max])
24
+ end
25
+
26
+ def translate_style_options(result)
27
+ result[:style] = {
28
+ files: option_with_default(:style_glob),
29
+ measure: option_with_default(:style_measure).to_i,
30
+ } unless check_disabled(:no_style, [:style_glob])
31
+ end
32
+
33
+ def translate_doc_options(result)
34
+ result[:doc] = {
35
+ files: option_with_default(:doc_glob),
36
+ } unless check_disabled(:no_doc, [:doc_glob])
37
+ end
38
+
39
+ def check_disabled(check, params)
40
+ ((params + [check]) & options.keys) == [check]
41
+ end
42
+
43
+ def option_with_default(key)
44
+ options.fetch(key, defaults.fetch(key))
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ module Cane
2
+
3
+ # Creates violations for class definitions that do not have an explantory
4
+ # comment immediately preceeding.
5
+ class DocCheck < Struct.new(:opts)
6
+ def violations
7
+ file_names.map { |file_name|
8
+ find_violations(file_name)
9
+ }.flatten
10
+ end
11
+
12
+ def find_violations(file_name)
13
+ last_line = ""
14
+ File.open(file_name, 'r:utf-8').lines.map.with_index do |line, number|
15
+ result = if class_definition?(line) && !comment?(last_line)
16
+ UndocumentedClassViolation.new(file_name, number + 1, line)
17
+ end
18
+ last_line = line
19
+ result
20
+ end.compact
21
+ end
22
+
23
+ def file_names
24
+ Dir[opts.fetch(:files)]
25
+ end
26
+
27
+ def class_definition?(line)
28
+ line =~ /^\s*class\s/
29
+ end
30
+
31
+ def comment?(line)
32
+ line =~ /^\s*#/
33
+ end
34
+ end
35
+
36
+ # Value object used by DocCheck.
37
+ class UndocumentedClassViolation < Struct.new(:file_name, :number, :line)
38
+ def description
39
+ "Classes are not documented"
40
+ end
41
+
42
+ def columns
43
+ ["%s:%i" % [file_name, number], extract_class_name(line)]
44
+ end
45
+
46
+ def extract_class_name(line)
47
+ line.match(/class (\S+)/)[1]
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,98 @@
1
+ require 'tailor'
2
+
3
+ require 'cane/style_violation'
4
+
5
+ module Cane
6
+
7
+ # Creates violations for files that do not meet style conventions. This uses
8
+ # the `tailor` gem, configured to exclude some of its less reliable checks
9
+ # (mostly spacing around punctuation).
10
+ class StyleCheck < Struct.new(:opts)
11
+ def violations
12
+ Dir[opts.fetch(:files)].map do |file_name|
13
+ find_violations_in_file(file_name)
14
+ end.flatten
15
+ end
16
+
17
+ protected
18
+
19
+ def find_violations_in_file(file_name)
20
+ source = File.open(file_name, 'r:utf-8')
21
+ file_path = Pathname.new(file_name)
22
+
23
+ source.each_line.map.with_index do |source_line, line_number|
24
+ violations_for_line(file_path, source_line, line_number)
25
+ end
26
+ end
27
+
28
+ def violations_for_line(file_path, source_line, line_number)
29
+ FileLine.new(source_line, opts).problems.map do |message|
30
+ StyleViolation.new(file_path, line_number + 1, message)
31
+ end
32
+ end
33
+ end
34
+
35
+ # The `tailor` gem was not designed to be used as a library, so interfacing
36
+ # with it is a bit of a mess. This wrapper is attempt to confine that mess to
37
+ # a single point in the code.
38
+ class FileLine < Tailor::FileLine
39
+ attr_accessor :opts
40
+
41
+ def initialize(source_line, opts)
42
+ self.opts = opts
43
+
44
+ super(source_line, nil, nil)
45
+ end
46
+
47
+ def find_problems
48
+ # This is weird. These methods actually have side-effects! We capture
49
+ # the effects my monkey-patching #print_problem below.
50
+ spacing_problems
51
+ method_line? && camel_case_method?
52
+ class_line? && snake_case_class?
53
+ too_long?
54
+ end
55
+
56
+ def print_problem(message)
57
+ @problems << message.gsub(/\[.+\]\s+/, '')
58
+ end
59
+
60
+ def problems
61
+ @problems = []
62
+ find_problems
63
+ @problems
64
+ end
65
+
66
+ # A copy of the parent method that only uses a small subset of the spacing
67
+ # checks we actually want (the others are too buggy or controversial).
68
+ def spacing_problems
69
+ spacing_conditions.each_pair do |condition, values|
70
+ unless self.scan(values.first).empty?
71
+ print_problem values[1]
72
+ end
73
+ end
74
+ end
75
+
76
+ def spacing_conditions
77
+ SPACING_CONDITIONS.select {|k, _|
78
+ [:hard_tabbed, :trailing_whitespace].include?(k)
79
+ }
80
+ end
81
+
82
+ # Copy of parent method using a configurable line length.
83
+ def too_long?
84
+ length = self.length
85
+ if length > line_length_max
86
+ print_problem "Line is >#{line_length_max} characters (#{length})"
87
+ return true
88
+ end
89
+
90
+ false
91
+ end
92
+
93
+ def line_length_max
94
+ opts.fetch(:measure)
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,10 @@
1
+ # Value object used by StyleCheck.
2
+ class StyleViolation < Struct.new(:file_name, :line, :message)
3
+ def description
4
+ "Lines violated style requirements"
5
+ end
6
+
7
+ def columns
8
+ ["%s:%i" % [file_name, line], message]
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ require 'cane/threshold_violation'
2
+
3
+ # Configurable check that allows the contents of a file to be compared against
4
+ # a given value.
5
+ class ThresholdCheck < Struct.new(:checks)
6
+ def violations
7
+ checks.map do |operator, file, limit|
8
+ value = value_from_file(file)
9
+
10
+ unless value.send(operator, limit.to_f)
11
+ ThresholdViolation.new(file, operator, value, limit)
12
+ end
13
+ end.compact
14
+ end
15
+
16
+ def value_from_file(file)
17
+ begin
18
+ contents = File.read(file).chomp.to_f
19
+ rescue Errno::ENOENT
20
+ UnavailableValue.new
21
+ end
22
+ end
23
+
24
+ # Null object for all cases when the value to be compared against cannot be
25
+ # read.
26
+ class UnavailableValue
27
+ def >=(_); false end
28
+ def to_s; 'unavailable' end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # Value object used by ThresholdCheck.
2
+ class ThresholdViolation < Struct.new(:name, :operator, :value, :limit)
3
+ def description
4
+ "Quality threshold crossed"
5
+ end
6
+
7
+ def columns
8
+ ["%s is %s, should be %s %s" % [
9
+ name,
10
+ value,
11
+ operator,
12
+ limit
13
+ ]]
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Cane
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,49 @@
1
+ require 'stringio'
2
+
3
+ module Cane
4
+
5
+ # Computes a string to be displayed as output from an array of violations
6
+ # computed by the checks.
7
+ class ViolationFormatter < Struct.new(:violations)
8
+ def to_s
9
+ return '' if violations.empty?
10
+
11
+ grouped_violations.map do |description, violations|
12
+ format_group_header(description, violations) +
13
+ format_violations(violations)
14
+ end.flatten.join("\n") + "\n\n"
15
+ end
16
+
17
+ protected
18
+
19
+ def format_group_header(description, violations)
20
+ ["", "%s (%i):" % [description, violations.length], ""]
21
+ end
22
+
23
+ def format_violations(violations)
24
+ column_widths = calculate_columm_widths(violations)
25
+
26
+ violations.map do |violation|
27
+ format_violation(violation, column_widths)
28
+ end
29
+ end
30
+
31
+ def format_violation(violation, column_widths)
32
+ [
33
+ ' ' + violation.columns.map.with_index { |column, index|
34
+ "%-#{column_widths[index]}s" % column
35
+ }.join(' ')
36
+ ]
37
+ end
38
+
39
+ def calculate_columm_widths(violations)
40
+ violations.map { |violation|
41
+ violation.columns.map { |x| x.to_s.length }
42
+ }.transpose.map(&:max)
43
+ end
44
+
45
+ def grouped_violations
46
+ violations.group_by(&:description)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane/abc_check'
4
+
5
+ describe Cane::AbcCheck do
6
+ it 'creates an AbcMaxViolation for each method above the threshold' do
7
+ file_name = make_file(<<-RUBY)
8
+ class Harness
9
+ def not_complex
10
+ true
11
+ end
12
+
13
+ def complex_method(a)
14
+ b = a
15
+ return b if b > 3
16
+ end
17
+ end
18
+ RUBY
19
+
20
+ violations = described_class.new(files: file_name, max: 1).violations
21
+ violations.length.should == 1
22
+ violations[0].should be_instance_of(Cane::AbcMaxViolation)
23
+ violations[0].to_s.should include("Harness")
24
+ violations[0].to_s.should include("complex_method")
25
+ end
26
+
27
+ it 'sorts violations by complexity' do
28
+ file_name = make_file(<<-RUBY)
29
+ class Harness
30
+ def not_complex
31
+ true
32
+ end
33
+
34
+ def complex_method(a)
35
+ b = a
36
+ return b if b > 3
37
+ end
38
+ end
39
+ RUBY
40
+
41
+ violations = described_class.new(files: file_name, max: 0).violations
42
+ violations.length.should == 2
43
+ complexities = violations.map(&:complexity)
44
+ complexities.should == complexities.sort.reverse
45
+ end
46
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require "stringio"
3
+ require 'cane/cli'
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
13
+
14
+ def run(cli_args)
15
+ result = nil
16
+ output = capture_stdout do
17
+ default_cli_opts = %w(--no-style --no-abc --no-doc)
18
+ result = Cane::CLI.run(default_cli_opts + cli_args.split(' '))
19
+ end
20
+
21
+ [output, result ? 0 : 1]
22
+ end
23
+
24
+ it 'fails if ABC metric does not meet requirements' do
25
+ file_name = make_file(<<-RUBY)
26
+ class Harness
27
+ def complex_method(a)
28
+ if a < 2
29
+ return "low"
30
+ else
31
+ return "high"
32
+ end
33
+ end
34
+ end
35
+ RUBY
36
+
37
+ _, exitstatus = run("--abc-glob #{file_name} --abc-max 1")
38
+
39
+ exitstatus.should == 1
40
+ end
41
+
42
+ it 'fails if style metrics do not meet requirements' do
43
+ file_name = make_file("whitespace ")
44
+
45
+ output, exitstatus = run("--style-glob #{file_name}")
46
+ exitstatus.should == 1
47
+ output.should include("Lines violated style requirements")
48
+ end
49
+
50
+ it 'allows measure to be configured' do
51
+ file_name = make_file("toolong")
52
+
53
+ output, exitstatus = run("--style-glob #{file_name} --style-measure 3")
54
+ exitstatus.should == 1
55
+ output.should include("Lines violated style requirements")
56
+ end
57
+
58
+ it 'allows upper bound of failed checks' do
59
+ file_name = make_file("whitespace ")
60
+
61
+ output, exitstatus = run("--style-glob #{file_name} --max-violations 1")
62
+ exitstatus.should == 0
63
+ output.should include("Lines violated style requirements")
64
+ end
65
+
66
+ it 'allows checking of a value in a file' do
67
+ file_name = make_file("89")
68
+
69
+ output, exitstatus = run("--gte #{file_name},90")
70
+ exitstatus.should == 1
71
+ output.should include("Quality threshold crossed")
72
+ end
73
+
74
+ it 'allows checking of class documentation' do
75
+ file_name = make_file("class NoDoc")
76
+
77
+ output, exitstatus = run("--doc-glob #{file_name}")
78
+ exitstatus.should == 1
79
+ output.should include("Classes are not documented")
80
+ end
81
+
82
+ it 'displays a help message' do
83
+ output, exitstatus = run("--help")
84
+
85
+ exitstatus.should == 0
86
+ output.should include("Usage:")
87
+ end
88
+
89
+ it 'displays version' do
90
+ output, exitstatus = run("--version")
91
+
92
+ exitstatus.should == 0
93
+ output.should include(Cane::VERSION)
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane/doc_check'
4
+
5
+ describe Cane::DocCheck do
6
+ it 'creates a DocViolation for each undocumented class' do
7
+ file_name = make_file <<-RUBY
8
+ # This class is documented
9
+ class Doc; end
10
+ class NoDoc; end # No doc
11
+ class AlsoNoDoc; end
12
+ [:class]
13
+ # class Ignore
14
+ RUBY
15
+
16
+ violations = described_class.new(files: file_name).violations
17
+ violations.length.should == 2
18
+
19
+ violations[0].should be_instance_of(Cane::UndocumentedClassViolation)
20
+ violations[0].file_name.should == file_name
21
+ violations[0].number.should == 3
22
+
23
+ violations[1].should be_instance_of(Cane::UndocumentedClassViolation)
24
+ violations[1].file_name.should == file_name
25
+ violations[1].number.should == 4
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'tempfile'
2
+
3
+ # Keep a reference to all tempfiles so they are not garbage collected until the
4
+ # process exits.
5
+ $tempfiles = []
6
+
7
+ def make_file(content)
8
+ tempfile = Tempfile.new('cane')
9
+ $tempfiles << tempfile
10
+ tempfile.print(content)
11
+ tempfile.flush
12
+ tempfile.path
13
+ end
14
+
15
+ require 'simplecov'
16
+
17
+ class SimpleCov::Formatter::QualityFormatter
18
+ def format(result)
19
+ SimpleCov::Formatter::HTMLFormatter.new.format(result)
20
+ File.open("coverage/covered_percent", "w") do |f|
21
+ f.puts result.source_files.covered_percent.to_i
22
+ end
23
+ end
24
+ end
25
+
26
+ SimpleCov.formatter = SimpleCov::Formatter::QualityFormatter
27
+ SimpleCov.start
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane/style_check'
4
+
5
+ describe Cane::StyleCheck do
6
+ it 'creates a StyleViolation for each method above the threshold' do
7
+ ruby = [
8
+ "def test ",
9
+ "\t1",
10
+ "end"
11
+ ].join("\n")
12
+ file_name = make_file(ruby)
13
+
14
+ violations = Cane::StyleCheck.new(files: file_name, measure: 80).violations
15
+ violations.length.should == 2
16
+ violations[0].should be_instance_of(StyleViolation)
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane/threshold_check'
4
+
5
+ describe ThresholdCheck do
6
+ it 'returns a value of unavailable when file cannot be read' do
7
+ check = ThresholdCheck.new([[:>=, 'bogus_file', 20]])
8
+ violations = check.violations
9
+ violations.length.should == 1
10
+ violations[0].to_s.should include("unavailable")
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ require 'cane/violation_formatter'
4
+
5
+ describe Cane::ViolationFormatter do
6
+ def violation(description)
7
+ stub("violation",
8
+ description: description,
9
+ columns: []
10
+ )
11
+ end
12
+
13
+ it 'includes number of violations in the group header' do
14
+ described_class.new([violation("FAIL")]).to_s.should include("(1)")
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cane
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Xavier Shay
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: tailor
16
+ requirement: &2156048880 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2156048880
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &2156048380 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2156048380
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &2156047960 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2156047960
47
+ - !ruby/object:Gem::Dependency
48
+ name: simplecov
49
+ requirement: &2156047500 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2156047500
58
+ description: Fails your build if code quality thresholds are not met
59
+ email:
60
+ - xavier@squareup.com
61
+ executables:
62
+ - cane
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - spec/abc_check_spec.rb
67
+ - spec/cane_spec.rb
68
+ - spec/doc_check_spec.rb
69
+ - spec/spec_helper.rb
70
+ - spec/style_check_spec.rb
71
+ - spec/threshold_check_spec.rb
72
+ - spec/violation_formatter_spec.rb
73
+ - lib/cane/abc_check.rb
74
+ - lib/cane/abc_max_violation.rb
75
+ - lib/cane/cli/spec.rb
76
+ - lib/cane/cli/translator.rb
77
+ - lib/cane/cli.rb
78
+ - lib/cane/doc_check.rb
79
+ - lib/cane/style_check.rb
80
+ - lib/cane/style_violation.rb
81
+ - lib/cane/threshold_check.rb
82
+ - lib/cane/threshold_violation.rb
83
+ - lib/cane/version.rb
84
+ - lib/cane/violation_formatter.rb
85
+ - lib/cane.rb
86
+ - README.md
87
+ - HISTORY.md
88
+ - cane.gemspec
89
+ - bin/cane
90
+ homepage: http://github.com/square/cane
91
+ licenses: []
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 1.8.10
111
+ signing_key:
112
+ specification_version: 3
113
+ summary: Fails your build if code quality thresholds are not met. Provides complexity
114
+ and style checkers built-in, and allows integration with with custom quality metrics.
115
+ test_files:
116
+ - spec/abc_check_spec.rb
117
+ - spec/cane_spec.rb
118
+ - spec/doc_check_spec.rb
119
+ - spec/spec_helper.rb
120
+ - spec/style_check_spec.rb
121
+ - spec/threshold_check_spec.rb
122
+ - spec/violation_formatter_spec.rb