code_metrics 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +33 -0
- data/bin/code_metrics +22 -0
- data/bin/code_metrics-profile +15 -0
- data/lib/code_metrics.rb +10 -0
- data/lib/code_metrics/profiler.rb +96 -0
- data/lib/code_metrics/railtie.rb +11 -0
- data/lib/code_metrics/statistics.rb +106 -0
- data/lib/code_metrics/statistics_calculator.rb +81 -0
- data/lib/code_metrics/stats_directories.rb +28 -0
- data/lib/code_metrics/version.rb +3 -0
- data/lib/tasks/statistics.rake +14 -0
- data/test/active_support_testing_isolation.rb +136 -0
- data/test/code_metrics_test.rb +7 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +24 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +309 -0
- data/test/dummy/public/404.html +58 -0
- data/test/dummy/public/422.html +58 -0
- data/test/dummy/public/500.html +57 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/isolation_abstract_unit.rb +287 -0
- data/test/rake_test.rb +15 -0
- data/test/statistics_calculator_test.rb +288 -0
- data/test/test_helper.rb +15 -0
- metadata +177 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'CodeMetrics'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
load File.expand_path('../lib/tasks/statistics.rake', __FILE__)
|
24
|
+
|
25
|
+
Rake::TestTask.new(:test) do |t|
|
26
|
+
t.libs << 'lib'
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
task default: :test
|
data/bin/code_metrics
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'code_metrics'
|
3
|
+
require 'code_metrics/statistics'
|
4
|
+
# TODO, remove this duplicate code
|
5
|
+
root = defined?(Rails) ? Rails.root : Dir.pwd
|
6
|
+
STATS_DIRECTORIES = [
|
7
|
+
%w(Controllers app/controllers),
|
8
|
+
%w(Helpers app/helpers),
|
9
|
+
%w(Models app/models),
|
10
|
+
%w(Mailers app/mailers),
|
11
|
+
%w(Javascripts app/assets/javascripts),
|
12
|
+
%w(Libraries lib/),
|
13
|
+
%w(APIs app/apis),
|
14
|
+
%w(Controller\ tests test/controllers),
|
15
|
+
%w(Helper\ tests test/helpers),
|
16
|
+
%w(Model\ tests test/models),
|
17
|
+
%w(Mailer\ tests test/mailers),
|
18
|
+
%w(Integration\ tests test/integration),
|
19
|
+
%w(Functional\ tests\ (old) test/functional),
|
20
|
+
%w(Unit\ tests \ (old) test/unit)
|
21
|
+
].collect { |name, dir| [ name, "#{root}/#{dir}" ] }.select { |name, dir| File.directory?(dir) }
|
22
|
+
CodeMetrics::Statistics.new(*STATS_DIRECTORIES).to_s
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Example:
|
3
|
+
# code_metrics-profile activesupport/lib/active_support.rb [ruby-prof mode]
|
4
|
+
|
5
|
+
require 'code_metrics'
|
6
|
+
require 'code_metrics/profiler'
|
7
|
+
if (filename = ARGV.shift)
|
8
|
+
path = File.expand_path(filename)
|
9
|
+
mode = ARGV.shift
|
10
|
+
CodeMetrics::Profiler.new(path, mode).profile_requires rescue STDERR.puts(
|
11
|
+
"#{$!.message}\t #{$!.class} on #{$!.backtrace.detect {|line| line =~ /code_metrics/ }}"
|
12
|
+
)
|
13
|
+
else
|
14
|
+
STDERR.puts "No file path entered. Usage is code_metrics-profile path/to/file.rb [ruby-prof mode]"
|
15
|
+
end
|
data/lib/code_metrics.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module CodeMetrics
|
2
|
+
|
3
|
+
class Profiler
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
|
6
|
+
attr_reader :path, :mode
|
7
|
+
def initialize(path, mode=nil)
|
8
|
+
assert_ruby_file_exists(path)
|
9
|
+
@path, @mode = path, mode
|
10
|
+
Gem.refresh
|
11
|
+
# H/T https://github.com/pry/pry/blob/b02d0a4863/lib/pry/plugins.rb#L72
|
12
|
+
Gem::Specification.respond_to?(:each) ? Gem::Specification.all : Gem.source_index
|
13
|
+
require 'benchmark'
|
14
|
+
end
|
15
|
+
|
16
|
+
def profile_requires
|
17
|
+
GC.start
|
18
|
+
before_rss = `ps -o rss= -p #{Process.pid}`.to_i
|
19
|
+
|
20
|
+
if mode
|
21
|
+
require 'ruby-prof'
|
22
|
+
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
|
23
|
+
RubyProf.start
|
24
|
+
else
|
25
|
+
Object.instance_eval { include RequireProfiler }
|
26
|
+
end
|
27
|
+
|
28
|
+
elapsed = Benchmark.realtime { require path }
|
29
|
+
results = RubyProf.stop if mode
|
30
|
+
|
31
|
+
GC.start
|
32
|
+
after_rss = `ps -o rss= -p #{Process.pid}`.to_i
|
33
|
+
|
34
|
+
if mode
|
35
|
+
if printer = ARGV.shift
|
36
|
+
RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout)
|
37
|
+
elsif RubyProf.const_defined?(:CallStackPrinter)
|
38
|
+
File.open("#{File.basename(path, '.rb')}.#{mode}.html", 'w') do |out|
|
39
|
+
RubyProf::CallStackPrinter.new(results).print(out)
|
40
|
+
end
|
41
|
+
else
|
42
|
+
File.open("#{File.basename(path, '.rb')}.#{mode}.callgrind", 'w') do |out|
|
43
|
+
RubyProf::CallTreePrinter.new(results).print(out)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
RequireProfiler.stats.each do |file, depth, sec|
|
49
|
+
if sec
|
50
|
+
puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
|
51
|
+
else
|
52
|
+
puts "#{' ' * (13 + depth)}#{file}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss]
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def assert_ruby_file_exists(path)
|
61
|
+
raise Error.new("No such file") unless File.exists?(path)
|
62
|
+
ruby_extension = path[/\.rb\Z/]
|
63
|
+
ruby_executable = File.open(path, 'rb').readline[/\A#!.*ruby/]
|
64
|
+
raise Error.new("Not a ruby file") unless ruby_extension or ruby_executable
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
module RequireProfiler
|
70
|
+
private
|
71
|
+
def require(file, *args) RequireProfiler.profile(file) { super } end
|
72
|
+
def load(file, *args) RequireProfiler.profile(file) { super } end
|
73
|
+
|
74
|
+
@depth, @stats = 0, []
|
75
|
+
class << self
|
76
|
+
attr_accessor :depth
|
77
|
+
attr_accessor :stats
|
78
|
+
|
79
|
+
def profile(file)
|
80
|
+
stats << [file, depth]
|
81
|
+
self.depth += 1
|
82
|
+
result = nil
|
83
|
+
elapsed = Benchmark.realtime { result = yield }
|
84
|
+
self.depth -= 1
|
85
|
+
stats.pop if stats.last.first == file
|
86
|
+
stats << [file, depth, elapsed] if result
|
87
|
+
result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
if $0 == __FILE__
|
93
|
+
path = File.expand_path(ARGV.shift)
|
94
|
+
mode = ARGV.shift
|
95
|
+
CodeMetrics::Profiler.new(path, mode).profile_requires
|
96
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'code_metrics/statistics_calculator'
|
2
|
+
require 'code_metrics/stats_directories'
|
3
|
+
|
4
|
+
module CodeMetrics #:nodoc:
|
5
|
+
class Statistics
|
6
|
+
|
7
|
+
TEST_TYPES = ['Controller tests',
|
8
|
+
'Helper tests',
|
9
|
+
'Model tests',
|
10
|
+
'Mailer tests',
|
11
|
+
'Integration tests',
|
12
|
+
'Functional tests (old)',
|
13
|
+
'Unit tests (old)']
|
14
|
+
|
15
|
+
def initialize(*pairs)
|
16
|
+
@pairs = pairs
|
17
|
+
@statistics = calculate_statistics
|
18
|
+
@total = calculate_total if pairs.length > 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
print_header
|
23
|
+
@pairs.each { |pair| print_line(pair.first, @statistics[pair.first]) }
|
24
|
+
print_splitter
|
25
|
+
|
26
|
+
if @total
|
27
|
+
print_line("Total", @total)
|
28
|
+
print_splitter
|
29
|
+
end
|
30
|
+
|
31
|
+
print_code_test_stats
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def calculate_statistics
|
36
|
+
Hash[@pairs.map{|pair| [pair.first, calculate_directory_statistics(pair.last)]}]
|
37
|
+
end
|
38
|
+
|
39
|
+
def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/)
|
40
|
+
stats = StatisticsCalculator.new
|
41
|
+
|
42
|
+
Dir.foreach(directory) do |file_name|
|
43
|
+
path = "#{directory}/#{file_name}"
|
44
|
+
|
45
|
+
if File.directory?(path) && (/^\./ !~ file_name)
|
46
|
+
stats.add(calculate_directory_statistics(path, pattern))
|
47
|
+
end
|
48
|
+
|
49
|
+
next unless file_name =~ pattern
|
50
|
+
|
51
|
+
stats.add_by_file_path(path)
|
52
|
+
end
|
53
|
+
|
54
|
+
stats
|
55
|
+
end
|
56
|
+
|
57
|
+
def calculate_total
|
58
|
+
@statistics.each_with_object(StatisticsCalculator.new) do |pair, total|
|
59
|
+
total.add(pair.last)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def calculate_code
|
64
|
+
code_loc = 0
|
65
|
+
@statistics.each { |k, v| code_loc += v.code_lines unless TEST_TYPES.include? k }
|
66
|
+
code_loc
|
67
|
+
end
|
68
|
+
|
69
|
+
def calculate_tests
|
70
|
+
test_loc = 0
|
71
|
+
@statistics.each { |k, v| test_loc += v.code_lines if TEST_TYPES.include? k }
|
72
|
+
test_loc
|
73
|
+
end
|
74
|
+
|
75
|
+
def print_header
|
76
|
+
print_splitter
|
77
|
+
puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |"
|
78
|
+
print_splitter
|
79
|
+
end
|
80
|
+
|
81
|
+
def print_splitter
|
82
|
+
puts "+----------------------+-------+-------+---------+---------+-----+-------+"
|
83
|
+
end
|
84
|
+
|
85
|
+
def print_line(name, statistics)
|
86
|
+
m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0
|
87
|
+
loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0
|
88
|
+
|
89
|
+
puts "| #{name.ljust(20)} " \
|
90
|
+
"| #{statistics.lines.to_s.rjust(5)} " \
|
91
|
+
"| #{statistics.code_lines.to_s.rjust(5)} " \
|
92
|
+
"| #{statistics.classes.to_s.rjust(7)} " \
|
93
|
+
"| #{statistics.methods.to_s.rjust(7)} " \
|
94
|
+
"| #{m_over_c.to_s.rjust(3)} " \
|
95
|
+
"| #{loc_over_m.to_s.rjust(5)} |"
|
96
|
+
end
|
97
|
+
|
98
|
+
def print_code_test_stats
|
99
|
+
code = calculate_code
|
100
|
+
tests = calculate_tests
|
101
|
+
|
102
|
+
puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}"
|
103
|
+
puts ""
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module CodeMetrics
|
2
|
+
class StatisticsCalculator #:nodoc:
|
3
|
+
attr_reader :lines, :code_lines, :classes, :methods
|
4
|
+
|
5
|
+
PATTERNS = {
|
6
|
+
rb: {
|
7
|
+
line_comment: /^\s*#/,
|
8
|
+
begin_block_comment: /^=begin/,
|
9
|
+
end_block_comment: /^=end/,
|
10
|
+
class: /^\s*class\s+[_A-Z]/,
|
11
|
+
method: /^\s*def\s+[_a-z]/,
|
12
|
+
},
|
13
|
+
js: {
|
14
|
+
line_comment: %r{^\s*//},
|
15
|
+
begin_block_comment: %r{^\s*/\*},
|
16
|
+
end_block_comment: %r{\*/},
|
17
|
+
method: /function(\s+[_a-zA-Z][\da-zA-Z]*)?\s*\(/,
|
18
|
+
},
|
19
|
+
coffee: {
|
20
|
+
line_comment: /^\s*#/,
|
21
|
+
begin_block_comment: /^\s*###/,
|
22
|
+
end_block_comment: /^\s*###/,
|
23
|
+
class: /^\s*class\s+[_A-Z]/,
|
24
|
+
method: /[-=]>/,
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0)
|
29
|
+
@lines = lines
|
30
|
+
@code_lines = code_lines
|
31
|
+
@classes = classes
|
32
|
+
@methods = methods
|
33
|
+
end
|
34
|
+
|
35
|
+
def add(code_metrics_calculator)
|
36
|
+
@lines += code_metrics_calculator.lines
|
37
|
+
@code_lines += code_metrics_calculator.code_lines
|
38
|
+
@classes += code_metrics_calculator.classes
|
39
|
+
@methods += code_metrics_calculator.methods
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_by_file_path(file_path)
|
43
|
+
File.open(file_path) do |f|
|
44
|
+
self.add_by_io(f, file_type(file_path))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_by_io(io, file_type)
|
49
|
+
patterns = PATTERNS[file_type] || {}
|
50
|
+
|
51
|
+
comment_started = false
|
52
|
+
|
53
|
+
while line = io.gets
|
54
|
+
@lines += 1
|
55
|
+
|
56
|
+
if comment_started
|
57
|
+
if patterns[:end_block_comment] && line =~ patterns[:end_block_comment]
|
58
|
+
comment_started = false
|
59
|
+
end
|
60
|
+
next
|
61
|
+
else
|
62
|
+
if patterns[:begin_block_comment] && line =~ patterns[:begin_block_comment]
|
63
|
+
comment_started = true
|
64
|
+
next
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
@classes += 1 if patterns[:class] && line =~ patterns[:class]
|
69
|
+
@methods += 1 if patterns[:method] && line =~ patterns[:method]
|
70
|
+
if line !~ /^\s*$/ && (patterns[:line_comment].nil? || line !~ patterns[:line_comment])
|
71
|
+
@code_lines += 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def file_type(file_path)
|
78
|
+
File.extname(file_path).sub(/\A\./, '').downcase.to_sym
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Try to get the root of the current project
|
2
|
+
module CodeMetrics
|
3
|
+
class StatsDirectories
|
4
|
+
def initialize
|
5
|
+
@root = (defined?(Rails) && Rails.root) || Dir.pwd
|
6
|
+
end
|
7
|
+
def directories
|
8
|
+
[
|
9
|
+
%w(Controllers app/controllers),
|
10
|
+
%w(Helpers app/helpers),
|
11
|
+
%w(Models app/models),
|
12
|
+
%w(Mailers app/mailers),
|
13
|
+
%w(Javascripts app/assets/javascripts),
|
14
|
+
%w(Libraries lib/),
|
15
|
+
%w(APIs app/apis),
|
16
|
+
%w(Controller\ tests test/controllers),
|
17
|
+
%w(Helper\ tests test/helpers),
|
18
|
+
%w(Model\ tests test/models),
|
19
|
+
%w(Mailer\ tests test/mailers),
|
20
|
+
%w(Integration\ tests test/integration),
|
21
|
+
%w(Functional\ tests\ (old) test/functional),
|
22
|
+
%w(Unit\ tests \ (old) test/unit)
|
23
|
+
].collect { |name, dir| [ name, "#{@root}/#{dir}" ] }
|
24
|
+
.select { |name, dir| File.directory?(dir) }
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
begin
|
2
|
+
require 'rake'
|
3
|
+
namespace :code_metrics do
|
4
|
+
|
5
|
+
desc "Report code statistics (KLOCs, etc) from the application"
|
6
|
+
task :stats do
|
7
|
+
require 'code_metrics/statistics'
|
8
|
+
STATS_DIRECTORIES = CodeMetrics::StatsDirectories.new.directories
|
9
|
+
CodeMetrics::Statistics.new(*STATS_DIRECTORIES).to_s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
rescue LoadError
|
13
|
+
STDERR.puts "Cannot load rake code_metrics:stats task, rake not available"
|
14
|
+
end
|