diff --git a/.gitignore b/.gitignore
index a4e8faa76..547b773e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,5 @@ spec/reports
test/tmp
test/version_tmp
tmp
-
+*.swp
+tags
diff --git a/bin/mf-cane b/bin/mf-cane
new file mode 100755
index 000000000..0fa44a43e
--- /dev/null
+++ b/bin/mf-cane
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby_noexec_wrapper
+require 'rubygems'
+
+require 'metric_fu_requires'
+
+gem 'parallel', MetricFu::MetricVersion.parallel
+version = MetricFu::MetricVersion.cane
+gem 'cane', version
+load Gem.bin_path('cane', 'cane', version)
diff --git a/lib/metric_fu/metrics/cane/cane.rb b/lib/metric_fu/metrics/cane/cane.rb
new file mode 100644
index 000000000..83ae2ddb0
--- /dev/null
+++ b/lib/metric_fu/metrics/cane/cane.rb
@@ -0,0 +1,72 @@
+require 'cane'
+
+module MetricFu
+ class Cane < Generator
+ attr_reader :violations, :total_violations
+
+ def emit
+ command = %Q{mf-cane#{abc_max_param}#{style_measure_param}#{no_doc_param}}
+ mf_debug = "** #{command}"
+ @output = `#{command}`
+ end
+
+ def analyze
+ @violations = violations_by_category
+ extract_total_violations
+ end
+
+ def to_h
+ {:cane => {:total_violations => @total_violations, :violations => @violations}}
+ end
+ private
+
+ def abc_max_param
+ MetricFu.cane[:abc_max] ? " --abc-max #{MetricFu.cane[:abc_max]}" : ""
+ end
+
+ def style_measure_param
+ MetricFu.cane[:line_length] ? " --style-measure #{MetricFu.cane[:line_length]}" : ""
+ end
+
+ def no_doc_param
+ MetricFu.cane[:no_doc] == 'y' ? " --no-doc" : ""
+ end
+
+ def violations_by_category
+ violations_output = @output.scan(/(.*?)\n\n(.*?)\n\n/m)
+ violations_output.each_with_object({}) do |(category_desc, violation_list), violations|
+ category = category_from(category_desc)
+ violations[category] = violations_for(category, violation_list)
+ end
+ end
+
+ def category_from(description)
+ category_descriptions = {
+ :abc_complexity => /ABC complexity/,
+ :line_style => /style requirements/,
+ :comment => /comment/
+ }
+ category_descriptions.find {|k,v| description =~ v}[0]
+ end
+
+ def violations_for(category, violation_list)
+ violation_type_for(category).parse(violation_list)
+ end
+
+ def violation_type_for(category)
+ case category
+ when :abc_complexity
+ CaneViolations::AbcComplexity
+ when :line_style
+ CaneViolations::LineStyle
+ when :comment
+ CaneViolations::Comment
+ end
+ end
+
+ def extract_total_violations
+ total = @output.match(/Total Violations: (\d+)/)[1]
+ @total_violations = total.to_i if total
+ end
+ end
+end
diff --git a/lib/metric_fu/metrics/cane/cane_grapher.rb b/lib/metric_fu/metrics/cane/cane_grapher.rb
new file mode 100644
index 000000000..cd2c1d648
--- /dev/null
+++ b/lib/metric_fu/metrics/cane/cane_grapher.rb
@@ -0,0 +1,19 @@
+module MetricFu
+ class CaneGrapher < Grapher
+ attr_accessor :cane_violations, :labels
+
+ def initialize
+ super
+ @cane_violations = []
+ @labels = {}
+ end
+
+ def get_metrics(metrics, date)
+ if metrics && metrics[:cane]
+ @cane_violations.push(metrics[:cane][:total_violations].to_i)
+ @labels.update( { @labels.size => date })
+ end
+ end
+ end
+end
+
diff --git a/lib/metric_fu/metrics/cane/init.rb b/lib/metric_fu/metrics/cane/init.rb
new file mode 100644
index 000000000..e5c106db3
--- /dev/null
+++ b/lib/metric_fu/metrics/cane/init.rb
@@ -0,0 +1,12 @@
+MetricFu::Configuration.run do |config|
+ config.add_metric(:cane)
+ config.add_graph(:cane)
+ config.configure_metric(:cane, {
+ :dirs_to_cane => MetricFu.code_dirs,
+ :abc_max => 15,
+ :line_length => 80,
+ :no_doc => 'n',
+ :filetypes => ['rb']
+ })
+end
+
diff --git a/lib/metric_fu/metrics/cane/template_awesome/cane.html.erb b/lib/metric_fu/metrics/cane/template_awesome/cane.html.erb
new file mode 100644
index 000000000..31165a633
--- /dev/null
+++ b/lib/metric_fu/metrics/cane/template_awesome/cane.html.erb
@@ -0,0 +1,78 @@
+
+
+ Cane Results
+
+
+
+
+ Cane Results
+ back to menu
+ Total Violations: <%= @cane[:total_violations] %>
+ Cane reports code quality threshold violations.
+
+ <% graph_name = 'cane' %>
+ <% if MetricFu.configuration.graph_engine == :gchart %>
+
+ <% else %>
+
+
+ <% end %>
+
+ <% if @cane[:violations][:abc_complexity] && @cane[:violations][:abc_complexity].size > 0 %>
+ Methods exceeding allowed Abc complexity (<%= @cane[:violations][:abc_complexity].size %>)
+
+
+ File |
+ Method |
+ Complexity |
+
+ <% count = 0 %>
+ <% @cane[:violations][:abc_complexity].each do |violation| %>
+
+ <%=violation[:file]%> |
+ <%=violation[:method]%> |
+ <%=violation[:complexity]%> |
+
+ <% count += 1 %>
+ <% end %>
+
+ <% end %>
+ <% if @cane[:violations][:line_style] && @cane[:violations][:line_style].size > 0 %>
+ Lines violating style requirements (<%= @cane[:violations][:line_style].size %>)
+
+
+ File |
+ Description |
+
+ <% count = 0 %>
+ <% @cane[:violations][:line_style].each do |violation| %>
+
+ <%=violation[:line]%> |
+ <%=violation[:description]%> |
+
+ <% count += 1 %>
+ <% end %>
+
+ <% end %>
+ <% if @cane[:violations][:comment] && @cane[:violations][:comment].size > 0 %>
+ Class definitions requiring comments (<%= @cane[:violations][:comment].size %>)
+
+
+ File |
+ Class |
+
+ <% count = 0 %>
+ <% @cane[:violations][:comment].each do |violation| %>
+
+ <%=violation[:line]%> |
+ <%=violation[:class_name]%> |
+
+ <% count += 1 %>
+ <% end %>
+
+ <% end %>
+ Generated on <%= Time.now.localtime %>
+
+
diff --git a/lib/metric_fu/metrics/cane/template_standard/cane.html.erb b/lib/metric_fu/metrics/cane/template_standard/cane.html.erb
new file mode 100644
index 000000000..1e1ac3b66
--- /dev/null
+++ b/lib/metric_fu/metrics/cane/template_standard/cane.html.erb
@@ -0,0 +1,69 @@
+
+
+ Cane Results
+
+
+
+
+ Cane Results
+ back to menu
+ Total Violations: <%= @cane[:total_violations] %>
+ Cane reports code quality threshold violations.
+ <% if @cane[:violations][:abc_complexity] && @cane[:violations][:abc_complexity].size > 0 %>
+ Methods exceeding allowed Abc complexity (<%= @cane[:violations][:abc_complexity].size %>)
+
+
+ File |
+ Method |
+ Complexity |
+
+ <% count = 0 %>
+ <% @cane[:violations][:abc_complexity].each do |violation| %>
+
+ <%=violation[:file]%> |
+ <%=violation[:method]%> |
+ <%=violation[:complexity]%> |
+
+ <% count += 1 %>
+ <% end %>
+
+ <% end %>
+ <% if @cane[:violations][:line_style] && @cane[:violations][:line_style].size > 0 %>
+ Lines violating style requirements (<%= @cane[:violations][:line_style].size %>)
+
+
+ File |
+ Description |
+
+ <% count = 0 %>
+ <% @cane[:violations][:line_style].each do |violation| %>
+
+ <%=violation[:line]%> |
+ <%=violation[:description]%> |
+
+ <% count += 1 %>
+ <% end %>
+
+ <% end %>
+ <% if @cane[:violations][:comment] && @cane[:violations][:comment].size > 0 %>
+ Class definitions requiring comments (<%= @cane[:violations][:comment].size %>)
+
+
+ File |
+ Description |
+
+ <% count = 0 %>
+ <% @cane[:violations][:comment].each do |violation| %>
+
+ <%=violation[:line]%> |
+ <%=violation[:class_name]%> |
+
+ <% count += 1 %>
+ <% end %>
+
+ <% end %>
+ Generated on <%= Time.now.localtime %>
+
+
diff --git a/lib/metric_fu/metrics/cane/violations.rb b/lib/metric_fu/metrics/cane/violations.rb
new file mode 100644
index 000000000..fee218af7
--- /dev/null
+++ b/lib/metric_fu/metrics/cane/violations.rb
@@ -0,0 +1,30 @@
+module MetricFu
+ module CaneViolations
+ class AbcComplexity
+ def self.parse(violation_list)
+ violation_list.split(/\n/).map do |violation|
+ file, method, complexity = violation.split
+ {:file => file, :method => method, :complexity => complexity}
+ end
+ end
+ end
+
+ class LineStyle
+ def self.parse(violation_list)
+ violation_list.split(/\n/).map do |violation|
+ line, description = violation.split(/\s{2,}/).reject{|x|x.strip==''}
+ {:line => line, :description => description}
+ end
+ end
+ end
+
+ class Comment
+ def self.parse(violation_list)
+ violation_list.split(/\n/).map do |violation|
+ line, class_name = violation.split
+ {:line => line, :class_name => class_name}
+ end
+ end
+ end
+ end
+end
diff --git a/lib/metric_fu/reporting/graphs/engines/bluff.rb b/lib/metric_fu/reporting/graphs/engines/bluff.rb
index aa233e600..2e24a5e65 100644
--- a/lib/metric_fu/reporting/graphs/engines/bluff.rb
+++ b/lib/metric_fu/reporting/graphs/engines/bluff.rb
@@ -110,4 +110,18 @@ def graph!
File.open(File.join(MetricFu.output_directory, 'rails_best_practices.js'), 'w') {|f| f << content }
end
end
+
+ class CaneBluffGrapher < CaneGrapher
+ def graph!
+ content = <<-EOS
+ #{BLUFF_DEFAULT_OPTIONS}
+ g.title = 'Cane: code quality threshold violations';
+ g.data('cane', [#{@cane_violations.join(',')}]);
+ g.labels = #{@labels.to_json};
+ g.draw();
+ EOS
+ File.open(File.join(MetricFu.output_directory, 'cane.js'), 'w') {|f| f << content }
+ end
+ end
+
end
diff --git a/lib/metric_fu/reporting/templates/awesome/index.html.erb b/lib/metric_fu/reporting/templates/awesome/index.html.erb
index ef046d27e..950b4f06c 100644
--- a/lib/metric_fu/reporting/templates/awesome/index.html.erb
+++ b/lib/metric_fu/reporting/templates/awesome/index.html.erb
@@ -21,6 +21,9 @@
<% if @saikuro %>
Saikuro
<% end %>
+<% if @cane %>
+ Cane
+<% end %>
<% if @stats %>
Stats
<% end %>
diff --git a/lib/metric_fu/reporting/templates/standard/index.html.erb b/lib/metric_fu/reporting/templates/standard/index.html.erb
index 79b722cee..1fbeedce0 100644
--- a/lib/metric_fu/reporting/templates/standard/index.html.erb
+++ b/lib/metric_fu/reporting/templates/standard/index.html.erb
@@ -29,6 +29,9 @@
<% if @saikuro %>
Saikuro report
<% end %>
+ <% if @cane %>
+ Cane report
+ <% end %>
<% if @stats %>
Stats report
<% end %>
diff --git a/lib/metric_fu_requires.rb b/lib/metric_fu_requires.rb
index fe9a1f5ea..c544ae392 100644
--- a/lib/metric_fu_requires.rb
+++ b/lib/metric_fu_requires.rb
@@ -29,6 +29,9 @@ def ruby_parser
def sexp_processor
'~> 4.0'
end
+ def parallel
+ '0.6.2'
+ end
def rcov
'~> 0.8'
@@ -36,5 +39,8 @@ def rcov
def saikuro
'>= 1.1.1.0'
end
+ def cane
+ '2.5.2'
+ end
end
end
diff --git a/metric_fu.gemspec b/metric_fu.gemspec
index 86f7e02fb..c55d6f38d 100644
--- a/metric_fu.gemspec
+++ b/metric_fu.gemspec
@@ -29,10 +29,12 @@ Gem::Specification.new do |s|
"flog" => ["= #{MetricFu::MetricVersion.flog}"],
"reek" => ["= #{MetricFu::MetricVersion.reek}"],
"churn" => ["= #{MetricFu::MetricVersion.churn}"],
- # specifying dependencies for flay, reek, churn, and flog
+ "cane" => ["= #{MetricFu::MetricVersion.cane}"],
+ # specifying dependencies for flay, reek, churn, flog, and cane
"ruby_parser" => ["~> 3.0", ">= #{MetricFu::MetricVersion.ruby_parser}"],
"sexp_processor" => ["#{MetricFu::MetricVersion.sexp_processor}"],
"ruby2ruby" => ["= #{MetricFu::MetricVersion.ruby2ruby}"],
+ "parallel" => ["= #{MetricFu::MetricVersion.parallel}"],
"activesupport" => [">= 2.0.0"], # ok
"coderay" => [],
"fattr" => ["= 2.2.1"],
diff --git a/spec/cli/helper_spec.rb b/spec/cli/helper_spec.rb
index fa4eee7bf..a039669cf 100644
--- a/spec/cli/helper_spec.rb
+++ b/spec/cli/helper_spec.rb
@@ -39,6 +39,10 @@
defaults[:saikuro].should be_true
end
+ it "enables Cane" do
+ defaults[:cane].should be_true
+ end
+
it "enables RCov" do
defaults[:rcov].should be_true
end
@@ -142,6 +146,13 @@
helper.process_options(["--roodi"])[:roodi].should be_true
end
+ it "turns cane off" do
+ helper.process_options(["--no-cane"])[:cane].should be_false
+ end
+
+ it "turns cane on" do
+ helper.process_options(["--cane"])[:cane].should be_true
+ end
end
end
diff --git a/spec/metric_fu/configuration_spec.rb b/spec/metric_fu/configuration_spec.rb
index ea53de05a..8f116a562 100644
--- a/spec/metric_fu/configuration_spec.rb
+++ b/spec/metric_fu/configuration_spec.rb
@@ -126,6 +126,17 @@ def load_metric(metric)
should == { :start_date => %q("1 year ago"), :minimum_churn_count => 10}
end
+ it 'should set @cane to ' +
+ %q(:dirs_to_cane => @code_dirs, :abc_max => 15, :line_length => 80, :no_doc => 'n') do
+ load_metric 'cane'
+ @config.send(:cane).
+ should == {
+ :dirs_to_cane => MetricFu.code_dirs,
+ :filetypes => ["rb"],
+ :abc_max => 15,
+ :line_length => 80,
+ :no_doc => "n"}
+ end
it 'should set @rcov to ' +
%q(:test_files => Dir['{spec,test}/**/*_{spec,test}.rb'],
@@ -236,7 +247,7 @@ def load_metric(metric)
end
it 'should set the available metrics' do
- @config.metrics.should =~ [:churn, :flog, :flay, :reek, :roodi, :rcov, :hotspots, :saikuro]
+ @config.metrics.should =~ [:churn, :flog, :flay, :reek, :roodi, :rcov, :hotspots, :saikuro, :cane]
end
it 'should set the @code_dirs instance var to ["lib"]' do
@@ -268,13 +279,13 @@ def load_metric(metric)
before(:each) { get_new_config }
- [:churn, :flog, :flay, :reek, :roodi, :rcov, :hotspots, :saikuro].each do |metric|
+ [:churn, :flog, :flay, :reek, :roodi, :rcov, :hotspots, :saikuro, :cane].each do |metric|
it "should add a #{metric} class method to the MetricFu module " do
MetricFu.should respond_to(metric)
end
end
- [:churn, :flog, :flay, :reek, :roodi, :rcov, :hotspots, :saikuro].each do |graph|
+ [:churn, :flog, :flay, :reek, :roodi, :rcov, :hotspots, :saikuro, :cane].each do |graph|
it "should add a #{graph} class metrhod to the MetricFu module" do
MetricFu.should respond_to(graph)
end
diff --git a/spec/metric_fu/metrics/cane/cane_spec.rb b/spec/metric_fu/metrics/cane/cane_spec.rb
new file mode 100644
index 000000000..be04ebcae
--- /dev/null
+++ b/spec/metric_fu/metrics/cane/cane_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe Cane do
+ describe "emit method" do
+ def configure_cane_with(options={})
+ MetricFu::Configuration.run {|config|
+ config.add_metric(:cane)
+ config.configure_metric(:cane, options)
+ }
+ end
+
+ it "should execute cane command" do
+ configure_cane_with({})
+ @cane = MetricFu::Cane.new('base_dir')
+ @cane.should_receive(:`).with("mf-cane")
+ output = @cane.emit
+ end
+
+ it "should use abc max option" do
+ configure_cane_with({abc_max: 20})
+ @cane = MetricFu::Cane.new('base_dir')
+ @cane.should_receive(:`).with("mf-cane --abc-max 20")
+ output = @cane.emit
+ end
+
+ it "should use style max line length option" do
+ configure_cane_with({line_length: 100})
+ @cane = MetricFu::Cane.new('base_dir')
+ @cane.should_receive(:`).with("mf-cane --style-measure 100")
+ output = @cane.emit
+ end
+
+ it "should use no-doc if specified" do
+ configure_cane_with({no_doc: 'y'})
+ @cane = MetricFu::Cane.new('base_dir')
+ @cane.should_receive(:`).with("mf-cane --no-doc")
+ output = @cane.emit
+ end
+
+ it "should include doc violations if no_doc != 'y'" do
+ configure_cane_with({no_doc: 'n'})
+ @cane = MetricFu::Cane.new('base_dir')
+ @cane.should_receive(:`).with("mf-cane")
+ output = @cane.emit
+ end
+ end
+
+ describe "parse cane output" do
+ before :each do
+ lines = sample_cane_output
+ MetricFu::Configuration.run {}
+ File.stub!(:directory?).and_return(true)
+ @cane = MetricFu::Cane.new('base_dir')
+ @cane.instance_variable_set(:@output, lines)
+ end
+
+ describe "analyze method" do
+
+ it "should find total violations" do
+ @cane.analyze
+ @cane.total_violations.should == 6
+ end
+
+ it "should extract abc complexity violations" do
+ @cane.analyze
+ @cane.violations[:abc_complexity].should == [
+ {file: 'lib/abc/foo.rb', method: 'Abc::Foo#method', complexity: '11'},
+ {file: 'lib/abc/bar.rb', method: 'Abc::Bar#method', complexity: '22'}
+ ]
+ end
+
+ it "should extract line style violations" do
+ @cane.analyze
+ @cane.violations[:line_style].should == [
+ {line: 'lib/line/foo.rb:1', description: 'Line is >80 characters (135)'},
+ {line: 'lib/line/bar.rb:2', description: 'Line contains trailing whitespace'}
+ ]
+ end
+
+ it "should extract comment violations" do
+ @cane.analyze
+ @cane.violations[:comment].should == [
+ {line: 'lib/comments/foo.rb:1', class_name: 'Foo'},
+ {line: 'lib/comments/bar.rb:2', class_name: 'Bar'}
+ ]
+ end
+ end
+
+ describe "to_h method" do
+ it "should have total violations" do
+ @cane.analyze
+ @cane.to_h[:cane][:total_violations].should == 6
+ end
+
+ it "should have violations by category" do
+ @cane.analyze
+ @cane.to_h[:cane][:violations][:abc_complexity].should == [
+ {file: 'lib/abc/foo.rb', method: 'Abc::Foo#method', complexity: '11'},
+ {file: 'lib/abc/bar.rb', method: 'Abc::Bar#method', complexity: '22'}
+ ]
+ @cane.to_h[:cane][:violations][:line_style].should == [
+ {line: 'lib/line/foo.rb:1', description: 'Line is >80 characters (135)'},
+ {line: 'lib/line/bar.rb:2', description: 'Line contains trailing whitespace'}
+ ]
+ @cane.to_h[:cane][:violations][:comment].should == [
+ {line: 'lib/comments/foo.rb:1', class_name: 'Foo'},
+ {line: 'lib/comments/bar.rb:2', class_name: 'Bar'}
+ ]
+ end
+ end
+ end
+
+ def sample_cane_output
+ <<-OUTPUT
+Methods exceeded maximum allowed ABC complexity (33):
+
+ lib/abc/foo.rb Abc::Foo#method 11
+ lib/abc/bar.rb Abc::Bar#method 22
+
+Lines violated style requirements (340):
+
+ lib/line/foo.rb:1 Line is >80 characters (135)
+ lib/line/bar.rb:2 Line contains trailing whitespace
+
+Class definitions require explanatory comments on preceding line (2):
+
+ lib/comments/foo.rb:1 Foo
+ lib/comments/bar.rb:2 Bar
+
+Total Violations: 6
+ OUTPUT
+ end
+end