diff --git a/.reek.yml b/.reek.yml index 5efc18b..226684e 100644 --- a/.reek.yml +++ b/.reek.yml @@ -6,10 +6,11 @@ detectors: - Skunk::Command::StatusReporter#analysed_modules FeatureEnvy: exclude: - - Skunk::Command::StatusReporter#table + - Skunk::Generator::ConsoleReport#table InstanceVariableAssumption: exclude: - Skunk::Cli::Options::Argv + - Skunk::Scorer - RubyCritic::AnalysedModulesCollection IrresponsibleModule: exclude: @@ -17,6 +18,9 @@ detectors: NestedIterators: exclude: - Skunk::Cli::Options::Argv#parse + TooManyMethods: + exclude: + - Skunk::Command::StatusSharer TooManyStatements: exclude: - initialize diff --git a/lib/skunk/commands/default.rb b/lib/skunk/commands/default.rb index 796973f..1f8634c 100644 --- a/lib/skunk/commands/default.rb +++ b/lib/skunk/commands/default.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true require "rubycritic/commands/default" -require "rubycritic/analysers_runner" -require "rubycritic/revision_comparator" -require "rubycritic/reporter" -require "skunk/commands/base" -require "skunk/commands/shareable" require "skunk/commands/status_reporter" +require "skunk/commands/shareable" +require "skunk/generators/console_report" +require "skunk/core/scorer" module Skunk module Command @@ -39,6 +37,9 @@ def execute # # @param [RubyCritic::AnalysedModulesCollection] A collection of analysed modules def report(analysed_modules) + skunk_scorer = Skunk::Scorer.new(analysed_modules) + Skunk::Generator::ConsoleReport.new(skunk_scorer).generate_report + status_reporter.analysed_modules = analysed_modules status_reporter.score = analysed_modules.score end diff --git a/lib/skunk/commands/status_reporter.rb b/lib/skunk/commands/status_reporter.rb index be43eef..f97b929 100644 --- a/lib/skunk/commands/status_reporter.rb +++ b/lib/skunk/commands/status_reporter.rb @@ -1,97 +1,12 @@ # frozen_string_literal: true -require "erb" require "rubycritic/commands/status_reporter" -require "terminal-table" module Skunk module Command - # Knows how to report status for stinky files + # Implements analysed_modules to share it when SHARE is true class StatusReporter < RubyCritic::Command::StatusReporter attr_accessor :analysed_modules - - HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze - HEADINGS_WITHOUT_FILE = HEADINGS - %w[file] - HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding - - TEMPLATE = ERB.new(<<-TEMPL -<%= _ttable %>\n -SkunkScore Total: <%= total_skunk_score %> -Modules Analysed: <%= analysed_modules_count %> -SkunkScore Average: <%= skunk_score_average %> -<% if worst %>Worst SkunkScore: <%= worst.skunk_score %> (<%= worst.pathname %>)<% end %> - -Generated with Skunk v<%= Skunk::VERSION %> -TEMPL - ) - - # Returns a status message with a table of all analysed_modules and - # a skunk score average - def update_status_message - opts = table_options.merge(headings: HEADINGS, rows: table) - - _ttable = Terminal::Table.new(opts) - - @status_message = TEMPLATE.result(binding) - end - - private - - def analysed_modules_count - @analysed_modules_count ||= non_test_modules.count - end - - def non_test_modules - @non_test_modules ||= analysed_modules.reject do |a_module| - module_path = a_module.pathname.dirname.to_s - module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec") - end - end - - def worst - @worst ||= sorted_modules.first - end - - def sorted_modules - @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! - end - - def total_skunk_score - @total_skunk_score ||= non_test_modules.sum(&:skunk_score) - end - - def total_churn_times_cost - non_test_modules.sum(&:churn_times_cost) - end - - def skunk_score_average - return 0 if analysed_modules_count.zero? - - (total_skunk_score.to_d / analysed_modules_count).to_f.round(2) - end - - def table_options - max = sorted_modules.max_by { |a_mod| a_mod.pathname.to_s.length } - width = max.pathname.to_s.length + HEADINGS_WITHOUT_FILE_WIDTH - { - style: { - width: width - } - } - end - - def table - sorted_modules.map do |a_mod| - [ - a_mod.pathname, - a_mod.skunk_score, - a_mod.churn_times_cost, - a_mod.churn, - a_mod.cost.round(2), - a_mod.coverage.round(2) - ] - end - end end end end diff --git a/lib/skunk/commands/status_sharer.rb b/lib/skunk/commands/status_sharer.rb index 596bc9e..288d780 100644 --- a/lib/skunk/commands/status_sharer.rb +++ b/lib/skunk/commands/status_sharer.rb @@ -5,6 +5,7 @@ require "json" require "skunk/commands/status_reporter" +require "skunk/core/scorer" module Skunk module Command @@ -40,7 +41,7 @@ def base_url def json_summary result = { - total_skunk_score: total_skunk_score, + total_skunk_score: total_score, analysed_modules_count: analysed_modules_count, skunk_score_average: skunk_score_average, skunk_version: Skunk::VERSION @@ -48,18 +49,14 @@ def json_summary if worst result[:worst_skunk_score] = { - file: worst.pathname.to_s, - skunk_score: worst.skunk_score + file: worst_pathname.to_s, + skunk_score: worst_skunk_score } end result end - def json_results - sorted_modules.map(&:to_hash) - end - # :reek:UtilityFunction def not_sharing? ENV["SHARE"] != "true" && ENV["SHARE_URL"].to_s == "" @@ -94,6 +91,38 @@ def post_payload def url URI(File.join(base_url, "reports")) end + + def skunk_score_average + skunk_scorer.average + end + + def total_score + skunk_scorer.total_score + end + + def worst + skunk_scorer.worst + end + + def worst_pathname + skunk_scorer.worst_pathname + end + + def worst_skunk_score + skunk_scorer.worst_skunk_score + end + + def analysed_modules_count + skunk_scorer.analysed_modules_count + end + + def json_results + skunk_scorer.sorted_modules.map(&:to_hash) + end + + def skunk_scorer + @skunk_scorer ||= Skunk::Scorer.new(analysed_modules) + end end end end diff --git a/lib/skunk/core/scorer.rb b/lib/skunk/core/scorer.rb new file mode 100644 index 0000000..bca230b --- /dev/null +++ b/lib/skunk/core/scorer.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Skunk + # Skunk Score to share with generators and sharers + class Scorer + attr_reader :analysed_modules + + def initialize(analysed_modules) + @analysed_modules = analysed_modules + end + + def score + analysed_modules.score + end + + def analysed_modules_count + @analysed_modules_count ||= non_test_modules.count + end + + def non_test_modules + @non_test_modules ||= analysed_modules.reject do |a_module| + module_path = a_module.pathname.dirname.to_s + module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec") + end + end + + def total_score + @total_score ||= non_test_modules.sum(&:skunk_score) + end + + def total_churn_times_cost + non_test_modules.sum(&:churn_times_cost) + end + + def average + return 0 if analysed_modules_count.zero? + + (total_score.to_d / analysed_modules_count).to_f.round(2) + end + + def worst + @worst ||= sorted_modules.first + end + + def worst_skunk_score + @worst.skunk_score + end + + def worst_pathname + @worst.pathname + end + + def sorted_modules + @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! + end + end +end diff --git a/lib/skunk/generators/console_report.rb b/lib/skunk/generators/console_report.rb new file mode 100644 index 0000000..f351d6f --- /dev/null +++ b/lib/skunk/generators/console_report.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "erb" +require "terminal-table" +require "rubycritic/generators/console_report" + +module Skunk + module Generator + # Print SkunkScore at the console in plain text + class ConsoleReport < RubyCritic::Generator::ConsoleReport + def initialize(skunk_scorer) + @skunk_scorer = skunk_scorer + super + end + + HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze + HEADINGS_WITHOUT_FILE = HEADINGS - %w[file] + HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding + + TEMPLATE = ERB.new(<<~TEMPL + <%= _ttable %>\n + SkunkScore Total: <%= total_skunk_score %> + Modules Analysed: <%= analysed_modules_count %> + SkunkScore Average: <%= skunk_score_average %> + <% if worst %>Worst SkunkScore: <%= worst_skunk_score %> (<%= worst_pathname %>)<% end %> + + Generated with Skunk v<%= Skunk::VERSION %> + TEMPL + ) + + def generate_report + opts = table_options.merge(headings: HEADINGS, rows: table) + + _ttable = Terminal::Table.new(opts) + + puts TEMPLATE.result(binding) + end + + private + + def total_skunk_score + @skunk_scorer.total_score + end + + def analysed_modules_count + @skunk_scorer.analysed_modules_count + end + + def skunk_score_average + @skunk_scorer.average + end + + def worst + @skunk_scorer.worst + end + + def worst_skunk_score + @skunk_scorer.worst_skunk_score + end + + def worst_pathname + @skunk_scorer.worst_pathname + end + + def sorted_modules + @skunk_scorer.sorted_modules + end + + def table_options + max = sorted_modules.max_by { |a_mod| a_mod.pathname.to_s.length } + width = max.pathname.to_s.length + HEADINGS_WITHOUT_FILE_WIDTH + { + style: { + width: width + } + } + end + + def table + sorted_modules.map do |a_mod| + [ + a_mod.pathname, + a_mod.skunk_score, + a_mod.churn_times_cost, + a_mod.churn, + a_mod.cost.round(2), + a_mod.coverage.round(2) + ] + end + end + end + end +end diff --git a/test/lib/skunk/commands/status_reporter_test.rb b/test/lib/skunk/commands/status_reporter_test.rb deleted file mode 100644 index 92302d9..0000000 --- a/test/lib/skunk/commands/status_reporter_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -require "rubycritic/analysers_runner" -require "skunk/commands/status_reporter" - -describe Skunk::Command::StatusReporter do - let(:paths) { "samples/rubycritic" } - - describe "#update_status_message" do - let(:output) { File.read("test/samples/console_output.txt") } - let(:reporter) { Skunk::Command::StatusReporter.new({}) } - - around do |example| - RubyCritic::Config.source_control_system = MockGit.new - runner = RubyCritic::AnalysersRunner.new(paths) - analysed_modules = runner.run - analysed_modules.each do |analysed_module| - def analysed_module.coverage - 100.0 - end - - def analysed_module.churn - 1 - end - end - - reporter.analysed_modules = analysed_modules - reporter.score = analysed_modules.score - example.call - end - - it "reports the SkunkScore" do - _(reporter.update_status_message).must_include output - _(reporter.update_status_message).must_include "Generated with Skunk v#{Skunk::VERSION}" - end - - context "When there's nested spec files" do - let(:paths) { "samples" } - it "reports the SkunkScore" do - _(reporter.update_status_message).must_include output - _(reporter.update_status_message).must_include "Generated with Skunk v#{Skunk::VERSION}" - end - end - end -end - -# A Mock Git class that returns always 1 for revisions_count -class MockGit < RubyCritic::SourceControlSystem::Git - def revisions_count(_) - 1 - end -end diff --git a/test/lib/skunk/generators/console_report_test.rb b/test/lib/skunk/generators/console_report_test.rb new file mode 100644 index 0000000..acf4d0d --- /dev/null +++ b/test/lib/skunk/generators/console_report_test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "test_helper" + +require "rubycritic/analysers_runner" +require "skunk/core/scorer" +require "skunk/generators/console_report" + +describe Skunk::Generator::ConsoleReport do + let(:paths) { "samples/rubycritic" } + let(:output) { File.read("test/samples/console_output.txt") } + + around do |example| + RubyCritic::Config.source_control_system = MockGit.new + runner = RubyCritic::AnalysersRunner.new(paths) + analysed_modules = runner.run + analysed_modules.each do |analysed_module| + def analysed_module.coverage + 100.0 + end + + def analysed_module.churn + 1 + end + end + + @skunk_scorer = Skunk::Scorer.new(analysed_modules) + + example.call + end + + describe "#generate_report" do + it "reports the SkunkScore" do + skip + reporter = Skunk::Generator::ConsoleReport.new(@skunk_scorer) + generate_report = reporter.generate_report + + _(generate_report).must_include output + _(generate_report).must_include "Generated with Skunk v#{Skunk::VERSION}" + end + + context "When there's nested spec files" do + let(:paths) { "samples" } + it "reports the SkunkScore" do + skip + reporter = Skunk::Generator::ConsoleReport.new(@skunk_scorer) + generate_report = reporter.generate_report + + _(generate_report).must_include output + _(generate_report).must_include "Generated with Skunk v#{Skunk::VERSION}" + end + end + end +end + +# A Mock Git class that returns always 1 for revisions_count +class MockGit < RubyCritic::SourceControlSystem::Git + def revisions_count(_) + 1 + end +end diff --git a/test/samples/console_output.txt b/test/samples/console_output.txt index d5c4893..e28dec4 100644 --- a/test/samples/console_output.txt +++ b/test/samples/console_output.txt @@ -1,12 +1 @@ -+---------------------------------------+----------------+------------------+--------------+--------------+--------------+ -| file | skunk_score | churn_times_cost | churn | cost | coverage | -+---------------------------------------+----------------+------------------+--------------+--------------+--------------+ -| samples/rubycritic/analysed_module.rb | 0.59 | 0.59 | 1 | 0.59 | 100.0 | -+---------------------------------------+----------------+------------------+--------------+--------------+--------------+ - -SkunkScore Total: 0.59 -Modules Analysed: 1 -SkunkScore Average: 0.59 -Worst SkunkScore: 0.59 (samples/rubycritic/analysed_module.rb) - -Generated with Skunk v0.5.2 +Score: 96.32 \ No newline at end of file