code_metrics 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +33 -0
  4. data/bin/code_metrics +22 -0
  5. data/bin/code_metrics-profile +15 -0
  6. data/lib/code_metrics.rb +10 -0
  7. data/lib/code_metrics/profiler.rb +96 -0
  8. data/lib/code_metrics/railtie.rb +11 -0
  9. data/lib/code_metrics/statistics.rb +106 -0
  10. data/lib/code_metrics/statistics_calculator.rb +81 -0
  11. data/lib/code_metrics/stats_directories.rb +28 -0
  12. data/lib/code_metrics/version.rb +3 -0
  13. data/lib/tasks/statistics.rake +14 -0
  14. data/test/active_support_testing_isolation.rb +136 -0
  15. data/test/code_metrics_test.rb +7 -0
  16. data/test/dummy/README.rdoc +28 -0
  17. data/test/dummy/Rakefile +6 -0
  18. data/test/dummy/app/assets/javascripts/application.js +13 -0
  19. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  20. data/test/dummy/app/controllers/application_controller.rb +5 -0
  21. data/test/dummy/app/helpers/application_helper.rb +2 -0
  22. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  23. data/test/dummy/bin/bundle +3 -0
  24. data/test/dummy/bin/rails +4 -0
  25. data/test/dummy/bin/rake +4 -0
  26. data/test/dummy/config.ru +4 -0
  27. data/test/dummy/config/application.rb +24 -0
  28. data/test/dummy/config/boot.rb +5 -0
  29. data/test/dummy/config/database.yml +25 -0
  30. data/test/dummy/config/environment.rb +5 -0
  31. data/test/dummy/config/environments/development.rb +29 -0
  32. data/test/dummy/config/environments/production.rb +80 -0
  33. data/test/dummy/config/environments/test.rb +36 -0
  34. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  36. data/test/dummy/config/initializers/inflections.rb +16 -0
  37. data/test/dummy/config/initializers/mime_types.rb +5 -0
  38. data/test/dummy/config/initializers/secret_token.rb +12 -0
  39. data/test/dummy/config/initializers/session_store.rb +3 -0
  40. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  41. data/test/dummy/config/locales/en.yml +23 -0
  42. data/test/dummy/config/routes.rb +56 -0
  43. data/test/dummy/db/test.sqlite3 +0 -0
  44. data/test/dummy/log/test.log +309 -0
  45. data/test/dummy/public/404.html +58 -0
  46. data/test/dummy/public/422.html +58 -0
  47. data/test/dummy/public/500.html +57 -0
  48. data/test/dummy/public/favicon.ico +0 -0
  49. data/test/isolation_abstract_unit.rb +287 -0
  50. data/test/rake_test.rb +15 -0
  51. data/test/statistics_calculator_test.rb +288 -0
  52. data/test/test_helper.rb +15 -0
  53. metadata +177 -0
@@ -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.
@@ -0,0 +1,3 @@
1
+ = CodeStatistics
2
+
3
+ This project rocks and uses MIT-LICENSE.
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ module CodeMetrics
2
+ require 'code_metrics/version'
3
+ require 'code_metrics/statistics'
4
+ $VERBOSE = nil
5
+ if defined?(Rails)
6
+ require 'code_metrics/railtie'
7
+ else
8
+ load "tasks/statistics.rake"
9
+ end
10
+ end
@@ -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,11 @@
1
+ require 'code_metrics'
2
+ require 'rails'
3
+ module CodeMetrics
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :code_metrics
6
+
7
+ rake_tasks do
8
+ load "tasks/statistics.rake"
9
+ end
10
+ end
11
+ 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,3 @@
1
+ module CodeMetrics
2
+ VERSION = "0.0.1"
3
+ 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