cane 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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