4d1c9dad17af98e55cb65b4efce27c42

I want to get some statistics like code to test ratio and loc on my project using RSpec. The class and method counts didn't seem relevant to the specs, so I added behavoirs and specs. Any thoughts on improving this?

Bonus points for any rspec metrics.

require 'spec_statistics'

namespace :spec do
  desc "Report code statistics on the application and specs code"
  task :stats do
    stats_directories = {
        "Specs" => "spec",
        "Application" => "lib"
      }.map {|name, dir| [name, "#{Dir.pwd}/#{dir}"]}
    SpecStatistics.new(*stats_directories).to_s
  end
end
require 'code_statistics' #borrow from http://dev.rubyonrails.org/svn/rails/trunk/railties/lib/code_statistics.rb
class SpecStatistics < CodeStatistics

  TEST_TYPES << "Specs"

  private
  def calculate_directory_statistics(directory, pattern = /.*\.rb$/)
    stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0, "specs" => 0, "behaviors" => 0 }


    Dir.foreach(directory) do |file_name| 
      if File.stat(directory + "/" + file_name).directory? and (/^\./ !~ file_name)
        newstats = calculate_directory_statistics(directory + "/" + file_name, pattern)
        stats.each { |k, v| stats[k] += newstats[k] }
      end

      next unless file_name =~ pattern

      f = File.open(directory + "/" + file_name)

      while line = f.gets
        stats["lines"]     += 1
        stats["classes"]   += 1 if line =~ /class [A-Z]/
        stats["methods"]   += 1 if line =~ /def [a-z]/
        stats["specs"]     += 1 if line.strip =~ /^it.*(do|\{)$/
        stats["behaviors"] += 1 if line =~ /describe.*(do|\{)$/
        stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ /^\s*#/
      end
    end

    stats
  end

  def calculate_total
    total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0, "specs" => 0, "behaviors" => 0 }
    @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } }
    total
  end

  def print_header
    print_splitter
    puts "| Name                 | Lines |   LOC | Classes | Methods | Behaviors | Specifications | M/C | LOC/M | S/B |"
    print_splitter
  end

  def print_splitter
    puts "+----------------------+-------+-------+---------+---------+-----------+----------------+-----+-------+-----+"
  end

  def print_line(name, statistics)
    m_over_c   = (statistics["methods"] / statistics["classes"])   rescue m_over_c = 0
    loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0
    
    s_over_b = (statistics["specs"] / statistics["behaviors"]) rescue 0
    
    start = if TEST_TYPES.include? name
      "| #{name.ljust(20)} "
    else
      "| #{name.ljust(20)} "
    end

    puts start + 
         "| #{statistics["lines"].to_s.rjust(5)} " +
         "| #{statistics["codelines"].to_s.rjust(5)} " +
         "| #{statistics["classes"].to_s.rjust(7)} " +
         "| #{statistics["methods"].to_s.rjust(7)} " +
         "| #{statistics["behaviors"].to_s.rjust(10)}" +
         "| #{statistics["specs"].to_s.rjust(15)}" +
         "| #{m_over_c.to_s.rjust(3)} " +
         "| #{loc_over_m.to_s.rjust(6)}" +
         "| #{s_over_b.to_s.rjust(3)} |"
  end
end

Refactorings

No refactoring yet !

C2a5bc0633a2eef9274744bd273063a4

Michal Hantl

November 14, 2008, November 14, 2008 23:46, permalink

No rating. Login to rate!

This is how i modified your code for my purpouse. It is very rough (i gave it an hour or so) but i hope there can be seen some refactoring there.

Speaking of refactoring... you can see that i'm not using "code_statistics". But in MY CASE i noticed at least two classes to be rescued (= factored out) from this code. Report - which holds "stats", knows which files and directories are in, and how to add itself to another report (to sum them). Then there is a Reporter, which knows what files should go to this report and what to other (for example it would separate reports into "main" folders and it should know which files are tests and which are the actual code.

This would leave us with a pure stat Analyzer class.

But this simple script serves me well right now so i will leave this refactoring for future.

task :stats do
  #thanks to http://refactormycode.com/codes/91-rake-stats-for-rspec
  Loader.load 'misc/statistics/statistics'
  s = MyNamespace::Statistics.new(
    :root => File.dirname(__FILE__) + "/../lib", 
    :pattern => /.*\.rb$/,
    :test_pattern => /.*test\.rb$/
  )
  s.include_dir("component", "component")
  s.include_dir("control", "control")
  s.include_dir("core", "core")
  s.include_dir("visual", "visual")
  s.print_report
end


class MyNamespace
  class Statistics
    
    def initialize options
      @root = options[:root]
      @pattern = options[:pattern]
      @test_pattern = options[:test_pattern]
      @report = []
      @total_code = []
      @total_test = []
    end
    
    def include_dir(label, relative_directory)
      absolute_directory = @root + '/' + relative_directory
      stats = stats_for_dir(absolute_directory)
      @report << [label, stats[0]]
      @total_code << [label, stats[0]]
      @report << [label + " - test", stats[1]]
      @total_test << [label + " - test", stats[1]]
      stats
    end
    
  private

    def stats_for_dir(directory, append_to_stats = nil)
      pattern ||= @default_pattern
      stats = append_to_stats || [empty_stats, empty_stats]
      Dir.foreach(directory) do |file_name| 
        if File.stat(directory + "/" + file_name).directory? and (/^\./ !~ file_name)
          stats = stats_for_dir(directory + "/" + file_name, stats)
        end
        if file_name =~ @test_pattern
          puts "test " + file_name
          stats[1] = stats_for_file(directory + "/" + file_name, stats[1])
        elsif file_name =~ @pattern
          puts file_name
          stats[0] = stats_for_file(directory + "/" + file_name, stats[0])
        end
      end
      stats
    end
    
    def empty_stats
      {
        "lines" => 0, 
        "codelines" => 0, 
        "classes" => 0, 
        "methods" => 0, 
        }
    end

    def stats_for_file file, append_to_stats = nil
      stats = append_to_stats || empty_stats
      f = File.open(file)
      
      while line = f.gets
        stats["lines"]     += 1
        stats["classes"]   += 1 if line =~ /class [A-Z]/
        stats["methods"]   += 1 if line =~ /def [a-z]/
        stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ /^\s*#/
      end
      stats
    end

  public
    
    def print_report
      puts "+---------------------+-------+-------+---------+---------+-----+-------+"
      puts "| Name                | Lines |   LOC | Classes | Methods | M/C | LOC/M |"
      puts "+---------------------+-------+-------+---------+---------+-----+-------+"

      @report.each do |row|
        report_line row[0], row[1]
      end
      
      puts "+---------------------+-------+-------+---------+---------+-----+-------+"
      report_line "code", code_sum = sum_stats(@total_code)
      report_line "test", test_sum = sum_stats(@total_test)
      ctt = empty_stats
      code_sum.each { |k, v| 
        ctt[k] = (test_sum[k]*1.0 / code_sum[k]*100).floor.to_s+'%'
        }
      report_line "test ratio", ctt
      puts "+---------------------+-------+-------+---------+---------+-----+-------+"
      report_line "total", sum_stats(@report)
      puts "+---------------------+-------+-------+---------+---------+-----+-------+"
    end

  private
  
    def report_line name, stats
      m_over_c   = (stats["methods"] / stats["classes"]) rescue m_over_c = 0
      loc_over_m = (stats["codelines"] / stats["methods"]) - 2 rescue loc_over_m = 0

      puts "| #{name.ljust(19)} " +
           "| #{stats["lines"].to_s.rjust(5)} " +
           "| #{stats["codelines"].to_s.rjust(5)} " +
           "| #{stats["classes"].to_s.rjust(7)} " +
           "| #{stats["methods"].to_s.rjust(7)} " +
           "| #{m_over_c.to_s.rjust(3)} " +
           "| #{loc_over_m.to_s.rjust(5)} |" 
    end
    
    
    
    def sum_stats stats
      to_stats = empty_stats
      stats.each { |name, stats| stats.each { |k, v| to_stats[k] += v } }
      to_stats
    end    
  end
end
251c190c5a8277a3bc6197491f32da51

Fabio Kung

September 4, 2009, September 04, 2009 15:14, permalink

No rating. Login to rate!

Is anyone willing to transform these pieces of code to a gem and host in github?

Your refactoring





Format Copy from initial code

or Cancel