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 !
Michal Hantl
November 14, 2008, November 14, 2008 23:46, permalink
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
Fabio Kung
September 4, 2009, September 04, 2009 15:14, permalink
Is anyone willing to transform these pieces of code to a gem and host in github?
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.