code_metrics 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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