diff --git a/README.md b/README.md index 939af76..ac3c3ab 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,18 @@ Directory: /users/brian/examples/spree ``` +#### JSON Format + +If you want to export the details using JSON, you can use this command: + +``` +$ rake stats\[test/dummy,json\] + +Directory: /Users/etagwerker/Projects/fastruby/rails_stats/test/dummy + +[{"name":"Mailers","lines":"4","loc":"4","classes":"1","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Models","lines":"3","loc":"3","classes":"1","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Javascripts","lines":"27","loc":"7","classes":"0","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Jobs","lines":"7","loc":"2","classes":"1","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Controllers","lines":"7","loc":"6","classes":"1","methods":"1","m_over_c":"1","loc_over_m":"4"},{"name":"Helpers","lines":"3","loc":"3","classes":"0","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Channels","lines":"8","loc":"8","classes":"2","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Configuration","lines":"417","loc":"111","classes":"1","methods":"0","m_over_c":"0","loc_over_m":"0"},{"name":"Total","lines":"476","loc":"144","classes":"7","methods":"1","m_over_c":"0","loc_over_m":"142","code_to_test_ratio":"0.0","total":true}] +``` + ### Testing In order to run the tests for this gem: diff --git a/lib/rails_stats/all.rb b/lib/rails_stats/all.rb index e678948..6894c1c 100644 --- a/lib/rails_stats/all.rb +++ b/lib/rails_stats/all.rb @@ -1,3 +1,7 @@ +require 'rails_stats/stats_calculator' +require 'rails_stats/stats_formatter' +require 'rails_stats/json_formatter' +require 'rails_stats/console_formatter' require 'rails_stats/inflector' require 'rails_stats/code_statistics_calculator' require 'rails_stats/util' diff --git a/lib/rails_stats/code_statistics.rb b/lib/rails_stats/code_statistics.rb index bc2ec74..0900407 100644 --- a/lib/rails_stats/code_statistics.rb +++ b/lib/rails_stats/code_statistics.rb @@ -2,160 +2,23 @@ module RailsStats class CodeStatistics - - RAILS_APP_FOLDERS = ['models', - 'controllers', - 'helpers', - 'mailers', - 'views', - 'assets'] - - def initialize(root_directory) - @root_directory = root_directory - @key_concepts = calculate_key_concepts - @projects = calculate_projects - @statistics = calculate_statistics - @code_total, @tests_total, @grand_total = calculate_totals + def initialize(root_directory, opts = {}) + @calculator = RailsStats::StatsCalculator.new(root_directory) + @formatter = load_formatter(opts) end def to_s - print_header - sorted_keys = @statistics.keys.sort - sorted_keys.each { |key| print_line(key, @statistics[key]) } - print_splitter - - print_line("Code", @code_total) - print_line("Tests", @tests_total) - print_line("Total", @grand_total) - print_splitter - - print_code_test_stats + @formatter.to_s end private - def calculate_key_concepts - # returns names of main things like models, controllers, services, etc - concepts = {} - app_projects.each do |project| - project.key_concepts.each do |key| - concepts[key] = true - end - end - - # TODO: maybe gem names? - - concepts.keys - end - - def calculate_projects - out = [] - out += app_projects - out += calculate_root_projects - out += calculate_gem_projects - out += calculate_spec_projects - out += calculate_test_projects - out += calculate_cucumber_projects - out - end - - def app_projects - @app_projects ||= calculate_app_projects - end - - def calculate_app_projects - apps = Util.calculate_projects(@root_directory, "**", "app", RAILS_APP_FOLDERS) - apps.collect do |root_path| - AppStatistics.new(root_path) - end - end - - def calculate_gem_projects - gems = Util.calculate_projects(@root_directory, "*", "**", "*.gemspec") - gems.collect do |root_path| - GemStatistics.new(root_path) - end - end - - def calculate_spec_projects - specs = Util.calculate_shared_projects("spec", @root_directory, "**", "spec", "**", "*_spec.rb") - specs.collect do |root_path| - SpecStatistics.new(root_path, @key_concepts) - end - end - - def calculate_test_projects - tests = Util.calculate_shared_projects("test", @root_directory, "**", "test", "**", "*_test.rb") - tests.collect do |root_path| - TestStatistics.new(root_path, @key_concepts) - end - end - - def calculate_root_projects - [RootStatistics.new(@root_directory)] - end - - def calculate_cucumber_projects - cukes = Util.calculate_projects(@root_directory, "**", "*.feature") - cukes.collect do |root_path| - CucumberStatistics.new(root_path) - end - end - def calculate_statistics - out = {} - @projects.each do |project| - project.statistics.each do |key, stats| - out[key] ||= CodeStatisticsCalculator.new(project.test) - out[key].add(stats) - end + def load_formatter(opts = {}) + if opts[:format] == "json" + RailsStats::JSONFormatter.new(@calculator, opts) + else + RailsStats::ConsoleFormatter.new(@calculator, opts) end - out - end - - def calculate_totals - # TODO: make this a single loop - code_total = @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, code_total| - code_total.add(pair.last) unless pair.last.test - end - - tests_total = @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, tests_total| - tests_total.add(pair.last) if pair.last.test - end - - grand_total = @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, total| - total.add(pair.last) - end - - [code_total, tests_total, grand_total] - end - - def print_header - print_splitter - puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" - 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.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 - - puts "| #{name.ljust(20)} " \ - "| #{statistics.lines.to_s.rjust(7)} " \ - "| #{statistics.code_lines.to_s.rjust(7)} " \ - "| #{statistics.classes.to_s.rjust(7)} " \ - "| #{statistics.methods.to_s.rjust(7)} " \ - "| #{m_over_c.to_s.rjust(3)} " \ - "| #{loc_over_m.to_s.rjust(5)} |" - end - - def print_code_test_stats - code_to_test_ratio = @tests_total.code_lines.to_f / @code_total.code_lines - puts " Code LOC: #{@code_total.code_lines} Test LOC: #{@tests_total.code_lines} Code to Test Ratio: 1:#{sprintf("%.1f", code_to_test_ratio)}" - puts "" end end end diff --git a/lib/rails_stats/console_formatter.rb b/lib/rails_stats/console_formatter.rb new file mode 100644 index 0000000..60e7a31 --- /dev/null +++ b/lib/rails_stats/console_formatter.rb @@ -0,0 +1,52 @@ +module RailsStats + class ConsoleFormatter < StatsFormatter + def to_s + print_header + sorted_keys = @statistics.keys.sort + sorted_keys.each { |key| print_line(key, @statistics[key]) } + print_splitter + + if @grand_total + print_line("Code", @code_total) + print_line("Tests", @tests_total) + print_line("Total", @grand_total) + print_splitter + end + + print_code_test_stats + end + + private + + def print_header + print_splitter + puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" + 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.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 + + puts "| #{name.ljust(20)} " \ + "| #{statistics.lines.to_s.rjust(7)} " \ + "| #{statistics.code_lines.to_s.rjust(7)} " \ + "| #{statistics.classes.to_s.rjust(7)} " \ + "| #{statistics.methods.to_s.rjust(7)} " \ + "| #{m_over_c.to_s.rjust(3)} " \ + "| #{loc_over_m.to_s.rjust(5)} |" + end + + def print_code_test_stats + code = calculator.code_loc + tests = calculator.test_loc + + puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}" + puts "" + end + end +end \ No newline at end of file diff --git a/lib/rails_stats/json_formatter.rb b/lib/rails_stats/json_formatter.rb new file mode 100644 index 0000000..c5f2a52 --- /dev/null +++ b/lib/rails_stats/json_formatter.rb @@ -0,0 +1,45 @@ +require "json" + +module RailsStats + class JSONFormatter < StatsFormatter + def result + @result = @statistics.map { |key, stats| stat_hash(key, stats) } + + if @grand_total + @result << stat_hash("Total", @grand_total).merge(code_test_hash) + end + + @result + end + + def to_s + puts result.to_json + end + + private + + def code_test_hash + code = calculator.code_loc + tests = calculator.test_loc + + { + "code_to_test_ratio" => "#{sprintf("%.1f", tests.to_f/code)}", "total" => true + } + end + + def stat_hash(name, statistics) + m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0 + loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 + + { + "name" => name, + "lines" => statistics.lines.to_s, + "loc" => statistics.code_lines.to_s, + "classes" => statistics.classes.to_s, + "methods" => statistics.methods.to_s, + "m_over_c" => m_over_c.to_s, + "loc_over_m" => loc_over_m.to_s + } + end + end +end \ No newline at end of file diff --git a/lib/rails_stats/stats_calculator.rb b/lib/rails_stats/stats_calculator.rb new file mode 100644 index 0000000..c5da90e --- /dev/null +++ b/lib/rails_stats/stats_calculator.rb @@ -0,0 +1,133 @@ +module RailsStats + class StatsCalculator + RAILS_APP_FOLDERS = ['models', + 'controllers', + 'helpers', + 'mailers', + 'views', + 'assets'] + + def initialize(root_directory) + @root_directory = root_directory + @key_concepts = calculate_key_concepts + @projects = calculate_projects + @statistics = calculate_statistics + @code_loc = calculate_code + @test_loc = calculate_tests + @code_total, @tests_total, @grand_total = calculate_totals + end + + attr_reader :code_loc, :code_total, :grand_total, :statistics, :test_loc, :tests_total + + private + + def calculate_key_concepts + # returns names of main things like models, controllers, services, etc + concepts = {} + app_projects.each do |project| + project.key_concepts.each do |key| + concepts[key] = true + end + end + + # TODO: maybe gem names? + + concepts.keys + end + + def calculate_projects + out = [] + out += app_projects + out += calculate_root_projects + out += calculate_gem_projects + out += calculate_spec_projects + out += calculate_test_projects + out += calculate_cucumber_projects + out + end + + + def app_projects + @app_projects ||= calculate_app_projects + end + + def calculate_app_projects + apps = Util.calculate_projects(@root_directory, "**", "app", RAILS_APP_FOLDERS) + apps.collect do |root_path| + AppStatistics.new(root_path) + end + end + + def calculate_gem_projects + gems = Util.calculate_projects(@root_directory, "*", "**", "*.gemspec") + gems.collect do |root_path| + GemStatistics.new(root_path) + end + end + + def calculate_spec_projects + specs = Util.calculate_shared_projects("spec", @root_directory, "**", "spec", "**", "*_spec.rb") + specs.collect do |root_path| + SpecStatistics.new(root_path, @key_concepts) + end + end + + def calculate_test_projects + tests = Util.calculate_shared_projects("test", @root_directory, "**", "test", "**", "*_test.rb") + tests.collect do |root_path| + TestStatistics.new(root_path, @key_concepts) + end + end + + + def calculate_root_projects + [RootStatistics.new(@root_directory)] + end + + def calculate_cucumber_projects + cukes = Util.calculate_projects(@root_directory, "**", "*.feature") + cukes.collect do |root_path| + CucumberStatistics.new(root_path) + end + end + + def calculate_statistics + out = {} + @projects.each do |project| + project.statistics.each do |key, stats| + out[key] ||= CodeStatisticsCalculator.new(project.test) + out[key].add(stats) + end + end + out + end + + def calculate_totals + code_total = @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, code_total| + code_total.add(pair.last) unless pair.last.test + end + + tests_total = @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, tests_total| + tests_total.add(pair.last) if pair.last.test + end + + grand_total = @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, total| + total.add(pair.last) + end + + [code_total, tests_total, grand_total] + end + + def calculate_code + @code_loc = 0 + @statistics.each { |k, v| @code_loc += v.code_lines unless v.test } + @code_loc + end + + def calculate_tests + @test_loc = 0 + @statistics.each { |k, v| @test_loc += v.code_lines if v.test } + @test_loc + end + end +end \ No newline at end of file diff --git a/lib/rails_stats/stats_formatter.rb b/lib/rails_stats/stats_formatter.rb new file mode 100644 index 0000000..f03760a --- /dev/null +++ b/lib/rails_stats/stats_formatter.rb @@ -0,0 +1,13 @@ +module RailsStats + class StatsFormatter + def initialize(calculator, opts = {}) + @calculator = calculator + @statistics = calculator.statistics + @code_total = calculator.code_total + @tests_total = calculator.tests_total + @grand_total = calculator.grand_total + end + + attr_reader :calculator + end +end \ No newline at end of file diff --git a/lib/rails_stats/tasks.rb b/lib/rails_stats/tasks.rb index 546bf94..92f968a 100644 --- a/lib/rails_stats/tasks.rb +++ b/lib/rails_stats/tasks.rb @@ -1,13 +1,14 @@ desc "Report code statistics (KLOCs, etc) from the current (or given) application" -task :stats, [:path] do |t, args| +task :stats, [:path, :format] do |t, args| Rake::Task["stats"].clear # clear out normal one if there require 'rails_stats/all' path = args[:path] path = Rails.root.to_s if defined?(Rails) + fmt = args[:format] || "" raise "no path given for stats" unless path root_directory = File.absolute_path(path) puts "\nDirectory: #{root_directory}\n\n" if args[:path] - RailsStats::CodeStatistics.new(root_directory).to_s + RailsStats::CodeStatistics.new(root_directory, format: fmt).to_s end diff --git a/rails_stats.gemspec b/rails_stats.gemspec index 28fe705..31d32f8 100644 --- a/rails_stats.gemspec +++ b/rails_stats.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "codecov" spec.add_development_dependency "minitest" spec.add_development_dependency "minitest-around" + spec.add_development_dependency "minitest-spec-context" spec.add_development_dependency "simplecov" spec.add_development_dependency "simplecov-console" end diff --git a/test/lib/rails_stats/json_formatter_test.rb b/test/lib/rails_stats/json_formatter_test.rb new file mode 100644 index 0000000..f358e18 --- /dev/null +++ b/test/lib/rails_stats/json_formatter_test.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require "test_helper" + +describe RailsStats::JSONFormatter do + describe "#result" do + JSON_STRING = <<~EOS + [{ + "name": "Mailers", + "lines": "4", + "loc": "4", + "classes": "1", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Models", + "lines": "3", + "loc": "3", + "classes": "1", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Javascripts", + "lines": "27", + "loc": "7", + "classes": "0", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Jobs", + "lines": "7", + "loc": "2", + "classes": "1", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Controllers", + "lines": "7", + "loc": "6", + "classes": "1", + "methods": "1", + "m_over_c": "1", + "loc_over_m": "4" + }, { + "name": "Helpers", + "lines": "3", + "loc": "3", + "classes": "0", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Channels", + "lines": "8", + "loc": "8", + "classes": "2", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Configuration", + "lines": "417", + "loc": "111", + "classes": "1", + "methods": "0", + "m_over_c": "0", + "loc_over_m": "0" + }, { + "name": "Total", + "lines": "476", + "loc": "144", + "classes": "7", + "methods": "1", + "m_over_c": "0", + "loc_over_m": "142", + "code_to_test_ratio": "0.0", + "total": true + }] + EOS + + it "outputs useful stats for a Rails project" do + root_directory = File.absolute_path("./test/dummy") + + calculator = RailsStats::StatsCalculator.new(root_directory) + formatter = RailsStats::JSONFormatter.new(calculator) + + sorted_expectation = JSON.parse(JSON_STRING).sort {|x, y| x["name"] <=> y["name"] } + sorted_result = formatter.result.sort {|x, y| x["name"] <=> y["name"] } + + sorted_expectation.each_with_index do |x, i| + assert_equal x, sorted_result[i] + end + end + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 0e84dab..dbfbcd9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,5 +23,5 @@ require "minitest/autorun" require "minitest/pride" require "minitest/around/spec" - +require "minitest-spec-context" require "rails_stats/all"