From 8a23da551467217be19dd6f10a264cfb4b42f3da Mon Sep 17 00:00:00 2001 From: Sathish Date: Mon, 4 Mar 2013 22:09:28 +0530 Subject: [PATCH] Cane metrics added --- .gitignore | 3 +- bin/mf-cane | 9 ++ lib/metric_fu/metrics/cane/cane.rb | 72 ++++++++++ lib/metric_fu/metrics/cane/cane_grapher.rb | 19 +++ lib/metric_fu/metrics/cane/init.rb | 12 ++ .../cane/template_awesome/cane.html.erb | 78 ++++++++++ .../cane/template_standard/cane.html.erb | 69 +++++++++ lib/metric_fu/metrics/cane/violations.rb | 30 ++++ .../reporting/graphs/engines/bluff.rb | 14 ++ .../templates/awesome/index.html.erb | 3 + .../templates/standard/index.html.erb | 3 + lib/metric_fu_requires.rb | 6 + metric_fu.gemspec | 4 +- spec/cli/helper_spec.rb | 11 ++ spec/metric_fu/configuration_spec.rb | 17 ++- spec/metric_fu/metrics/cane/cane_spec.rb | 133 ++++++++++++++++++ 16 files changed, 478 insertions(+), 5 deletions(-) create mode 100755 bin/mf-cane create mode 100644 lib/metric_fu/metrics/cane/cane.rb create mode 100644 lib/metric_fu/metrics/cane/cane_grapher.rb create mode 100644 lib/metric_fu/metrics/cane/init.rb create mode 100644 lib/metric_fu/metrics/cane/template_awesome/cane.html.erb create mode 100644 lib/metric_fu/metrics/cane/template_standard/cane.html.erb create mode 100644 lib/metric_fu/metrics/cane/violations.rb create mode 100644 spec/metric_fu/metrics/cane/cane_spec.rb 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 %>)

+ + + + + + + <% count = 0 %> + <% @cane[:violations][:abc_complexity].each do |violation| %> + + + + + + <% count += 1 %> + <% end %> +
FileMethodComplexity
<%=violation[:file]%><%=violation[:method]%><%=violation[:complexity]%>
+ <% end %> + <% if @cane[:violations][:line_style] && @cane[:violations][:line_style].size > 0 %> +

Lines violating style requirements (<%= @cane[:violations][:line_style].size %>)

+ + + + + + <% count = 0 %> + <% @cane[:violations][:line_style].each do |violation| %> + + + + + <% count += 1 %> + <% end %> +
FileDescription
<%=violation[:line]%><%=violation[:description]%>
+ <% end %> + <% if @cane[:violations][:comment] && @cane[:violations][:comment].size > 0 %> +

Class definitions requiring comments (<%= @cane[:violations][:comment].size %>)

+ + + + + + <% count = 0 %> + <% @cane[:violations][:comment].each do |violation| %> + + + + + <% count += 1 %> + <% end %> +
FileClass
<%=violation[:line]%><%=violation[:class_name]%>
+ <% 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 %>)

+ + + + + + + <% count = 0 %> + <% @cane[:violations][:abc_complexity].each do |violation| %> + + + + + + <% count += 1 %> + <% end %> +
FileMethodComplexity
<%=violation[:file]%><%=violation[:method]%><%=violation[:complexity]%>
+ <% end %> + <% if @cane[:violations][:line_style] && @cane[:violations][:line_style].size > 0 %> +

Lines violating style requirements (<%= @cane[:violations][:line_style].size %>)

+ + + + + + <% count = 0 %> + <% @cane[:violations][:line_style].each do |violation| %> + + + + + <% count += 1 %> + <% end %> +
FileDescription
<%=violation[:line]%><%=violation[:description]%>
+ <% end %> + <% if @cane[:violations][:comment] && @cane[:violations][:comment].size > 0 %> +

Class definitions requiring comments (<%= @cane[:violations][:comment].size %>)

+ + + + + + <% count = 0 %> + <% @cane[:violations][:comment].each do |violation| %> + + + + + <% count += 1 %> + <% end %> +
FileDescription
<%=violation[:line]%><%=violation[:class_name]%>
+ <% 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