diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cc7705f..fe3af97 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-10-17 16:20:40 UTC using RuboCop version 1.81.1. +# on 2025-10-17 22:53:06 UTC using RuboCop version 1.81.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,16 +13,9 @@ Gemspec/RequiredRubyVersion: - 'skunk.gemspec' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/ClosingHeredocIndentation: - Exclude: - - 'lib/skunk/commands/status_reporter.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/HeredocIndentation: +Lint/IneffectiveAccessModifier: Exclude: - - 'lib/skunk/commands/status_reporter.rb' + - 'lib/skunk/generators/console/simple.rb' # Offense count: 2 # Configuration parameters: AllowedParentClasses. @@ -31,21 +24,21 @@ Lint/MissingSuper: - 'lib/skunk/cli/application.rb' - 'lib/skunk/generators/html/overview.rb' -# Offense count: 1 +# Offense count: 4 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 18 + Max: 24 -# Offense count: 12 +# Offense count: 14 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 233 + Max: 208 -# Offense count: 2 +# Offense count: 5 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 13 + Max: 18 # Offense count: 1 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. @@ -55,6 +48,16 @@ Naming/VariableNumber: Exclude: - 'lib/skunk/commands/status_sharer.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. +# SupportedStyles: nested, compact +# SupportedStylesForClasses: ~, nested, compact +# SupportedStylesForModules: ~, nested, compact +Style/ClassAndModuleChildren: + Exclude: + - 'test/test_helper.rb' + # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. @@ -63,3 +66,10 @@ Style/FrozenStringLiteralComment: Exclude: - '**/*.arb' - 'bin/console' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Max: 124 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb8fcb..112ad26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## main [(unreleased)](https://github.com/fastruby/skunk/compare/v0.5.4...HEAD) +* [REFACTOR: Move Console Report](https://github.com/fastruby/skunk/pull/128) * [BUGFIX: Set the right content type in the share HTTP request](https://github.com/fastruby/skunk/pull/129) * [REFACTOR: Centralize Skunk analysis into RubyCritic module](https://github.com/fastruby/skunk/pull/127) * [FEATURE: Add Skunk HTML Report](https://github.com/fastruby/skunk/pull/123) diff --git a/bin/console b/bin/console index 2e978e4..00c2b0a 100755 --- a/bin/console +++ b/bin/console @@ -13,5 +13,5 @@ puts ARGV.inspect require "skunk/cli/application" require "skunk/config" -Skunk::Config.formats = %i[json html] +Skunk::Config.formats = %i[json console html] Skunk::Cli::Application.new(ARGV).execute diff --git a/lib/skunk/cli/application.rb b/lib/skunk/cli/application.rb index ba915bc..f928098 100644 --- a/lib/skunk/cli/application.rb +++ b/lib/skunk/cli/application.rb @@ -5,6 +5,7 @@ require "skunk" require "skunk/rubycritic/analysed_module" +require "skunk/rubycritic/analysed_modules_collection" require "skunk/cli/options" require "skunk/command_factory" require "skunk/commands/status_sharer" diff --git a/lib/skunk/cli/options/argv.rb b/lib/skunk/cli/options/argv.rb index 7b8baa8..af2bec9 100644 --- a/lib/skunk/cli/options/argv.rb +++ b/lib/skunk/cli/options/argv.rb @@ -12,7 +12,7 @@ class Argv < RubyCritic::Cli::Options::Argv # :reek:Attribute attr_accessor :output_filename - def parse # rubocop:disable Metrics/MethodLength + def parse parser.new do |opts| opts.banner = "Usage: skunk [options] [paths]\n" diff --git a/lib/skunk/commands/default.rb b/lib/skunk/commands/default.rb index 7c2d2a5..807878f 100644 --- a/lib/skunk/commands/default.rb +++ b/lib/skunk/commands/default.rb @@ -39,7 +39,6 @@ def report(analysed_modules) Reporter.generate_report(analysed_modules) status_reporter.analysed_modules = analysed_modules - status_reporter.score = analysed_modules.score end end end diff --git a/lib/skunk/commands/status_reporter.rb b/lib/skunk/commands/status_reporter.rb index 315347a..8689c7d 100644 --- a/lib/skunk/commands/status_reporter.rb +++ b/lib/skunk/commands/status_reporter.rb @@ -1,12 +1,10 @@ # 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 + # Extends RubyCritic::Command::StatusReporter to silence the status message class StatusReporter < RubyCritic::Command::StatusReporter attr_accessor :analysed_modules @@ -14,78 +12,8 @@ def initialize(options = {}) super(options) 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 - ) - - # 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.analysed_modules_count - end - - def worst - analysed_modules.worst_module - end - - def sorted_modules - analysed_modules.sorted_modules - end - - def total_skunk_score - analysed_modules.skunk_score_total - end - - def total_churn_times_cost - analysed_modules.total_churn_times_cost - end - - def skunk_score_average - analysed_modules.skunk_score_average - 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 + @status_message = "" end end end diff --git a/lib/skunk/commands/status_sharer.rb b/lib/skunk/commands/status_sharer.rb index bc38415..f46971a 100644 --- a/lib/skunk/commands/status_sharer.rb +++ b/lib/skunk/commands/status_sharer.rb @@ -40,15 +40,16 @@ def base_url def json_summary result = { - total_skunk_score: total_skunk_score, - analysed_modules_count: analysed_modules_count, - skunk_score_average: skunk_score_average, + total_skunk_score: analysed_modules.skunk_score_total, + analysed_modules_count: analysed_modules.analysed_modules_count, + skunk_score_average: analysed_modules.skunk_score_average, skunk_version: Skunk::VERSION } - if worst + if analysed_modules&.worst_module + worst = analysed_modules.worst_module result[:worst_skunk_score] = { - file: worst.pathname.to_s, + file: worst.pathname, skunk_score: worst.skunk_score } end @@ -57,7 +58,7 @@ def json_summary end def json_results - sorted_modules.map(&:to_hash) + analysed_modules.sorted_modules.map(&:to_hash) end # :reek:UtilityFunction diff --git a/lib/skunk/config.rb b/lib/skunk/config.rb index 62b6967..769590d 100644 --- a/lib/skunk/config.rb +++ b/lib/skunk/config.rb @@ -4,7 +4,7 @@ module Skunk # Utility module for format validation module FormatValidator # Supported output formats - SUPPORTED_FORMATS = %i[json html].freeze + SUPPORTED_FORMATS = %i[json html console].freeze # Check if a format is supported # @param format [Symbol] Format to check @@ -24,7 +24,7 @@ def self.supported_formats # Similar to RubyCritic::Configuration but focused only on Skunk's needs class Configuration # Default format - DEFAULT_FORMAT = :json + DEFAULT_FORMAT = :console def initialize @formats = [DEFAULT_FORMAT] diff --git a/lib/skunk/generators/console/simple.rb b/lib/skunk/generators/console/simple.rb new file mode 100644 index 0000000..d0d81a5 --- /dev/null +++ b/lib/skunk/generators/console/simple.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "erb" +require "terminal-table" + +module Skunk + module Generator + module Console + # Generates a console report for the analysed modules. + class Simple + def initialize(analysed_modules) + @analysed_modules = analysed_modules + 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 %> + + 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 render + opts = table_options.merge(headings: HEADINGS, rows: table) + _ttable = Terminal::Table.new(opts) + TEMPLATE.result(binding) + end + + private + + def analysed_modules_count + @analysed_modules.analysed_modules_count + end + + def worst + @analysed_modules.worst_module + end + + def sorted_modules + @analysed_modules.sorted_modules + end + + def total_skunk_score + @analysed_modules.skunk_score_total + end + + def total_churn_times_cost + @analysed_modules.total_churn_times_cost + end + + def skunk_score_average + @analysed_modules.skunk_score_average + end + + def table_options + return { style: { width: 100 } } if sorted_modules.empty? + + 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 + @analysed_modules.files_as_hash.map { |file_hash| self.class.format_hash_row(file_hash) } + end + + def self.format_hash_row(file_hash) + [ + file_hash[:file], + file_hash[:skunk_score], + file_hash[:churn_times_cost], + file_hash[:churn], + file_hash[:cost], + file_hash[:coverage] + ] + end + end + 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..3b1958e --- /dev/null +++ b/lib/skunk/generators/console_report.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "erb" +require "terminal-table" + +require "skunk/generators/console/simple" + +module Skunk + module Generator + # Generates a console report for the analysed modules. + class ConsoleReport + def initialize(analysed_modules) + @analysed_modules = analysed_modules + end + + def generate_report + puts generator.render + end + + private + + def generator + @generator ||= Skunk::Generator::Console::Simple.new(@analysed_modules) + end + end + end +end diff --git a/lib/skunk/generators/html_report.rb b/lib/skunk/generators/html_report.rb index 095a654..1f938fc 100644 --- a/lib/skunk/generators/html_report.rb +++ b/lib/skunk/generators/html_report.rb @@ -18,7 +18,7 @@ def initialize(analysed_modules) def generate_report create_directories_and_files - puts "Skunk report generated at #{report_location}" + puts "#{report_name} generated at #{report_location}" browser.open unless RubyCritic::Config.no_browser end @@ -40,6 +40,13 @@ def generators def overview_generator @overview_generator ||= Skunk::Generator::Html::Overview.new(@analysed_modules) end + + def report_name + self.class.name.split("::").last + .gsub(/([a-z])([A-Z])/, '\1 \2') + .downcase + .capitalize + end end end end diff --git a/lib/skunk/generators/json_report.rb b/lib/skunk/generators/json_report.rb index be7742d..6da5fc5 100644 --- a/lib/skunk/generators/json_report.rb +++ b/lib/skunk/generators/json_report.rb @@ -1,23 +1,37 @@ # frozen_string_literal: true -require "rubycritic/generators/json_report" - require "skunk/generators/json/simple" module Skunk module Generator # Generates a JSON report for the analysed modules. - class JsonReport < RubyCritic::Generator::JsonReport + class JsonReport def initialize(analysed_modules) - super @analysed_modules = analysed_modules end + def generate_report + FileUtils.mkdir_p(generator.file_directory) + puts "#{report_name} generated at #{file_path}" + File.write(file_path, generator.render) + end + private def generator Skunk::Generator::Json::Simple.new(@analysed_modules) end + + def report_name + self.class.name.split("::").last + .gsub(/([a-z])([A-Z])/, '\1 \2') + .downcase + .capitalize + end + + def file_path + generator.file_pathname + end end end end diff --git a/lib/skunk/reporter.rb b/lib/skunk/reporter.rb index 8c7b399..34089d7 100644 --- a/lib/skunk/reporter.rb +++ b/lib/skunk/reporter.rb @@ -13,10 +13,13 @@ def self.generate_report(analysed_modules) end def self.report_generator_class(config_format) - return unless Config.supported_format?(config_format) - - require "skunk/generators/#{config_format}_report" - Generator.const_get("#{config_format.capitalize}Report") + if Config.supported_format?(config_format) + require "skunk/generators/#{config_format}_report" + Generator.const_get("#{config_format.capitalize}Report") + else + require "skunk/generators/console_report" + Generator::ConsoleReport + end end end end diff --git a/lib/skunk/rubycritic/analysed_modules_collection.rb b/lib/skunk/rubycritic/analysed_modules_collection.rb index d03fc05..a26763f 100644 --- a/lib/skunk/rubycritic/analysed_modules_collection.rb +++ b/lib/skunk/rubycritic/analysed_modules_collection.rb @@ -65,14 +65,14 @@ def to_hash } end - private - # Returns files as an array of hashes (for JSON serialization) # @return [Array] def files_as_hash @files_as_hash ||= sorted_modules.map(&:to_hash) end + private + # Determines if a module is a test module based on its path # @param a_module [RubyCritic::AnalysedModule] The module to check # @return [Boolean] diff --git a/test/lib/skunk/application_test.rb b/test/lib/skunk/application_test.rb index 4f24063..8055646 100644 --- a/test/lib/skunk/application_test.rb +++ b/test/lib/skunk/application_test.rb @@ -35,28 +35,6 @@ end end - context "when passing --out option with a file" do - require "fileutils" - - let(:argv) { ["--out=tmp/generated_report.txt", "samples/rubycritic"] } - let(:success_code) { 0 } - - it "writes output to the file" do - FileUtils.rm("tmp/generated_report.txt", force: true) - FileUtils.mkdir_p("tmp") - - RubyCritic::AnalysedModule.stub_any_instance(:churn, 1) do - RubyCritic::AnalysedModule.stub_any_instance(:coverage, 100.0) do - result = application.execute - _(result).must_equal success_code - end - end - - _(File.read("tmp/generated_report.txt")) - .must_include File.read("test/samples/console_output.txt") - end - end - context "when comparing two branches" do let(:argv) { ["-b main", "samples/rubycritic"] } let(:success_code) { 0 } diff --git a/test/lib/skunk/commands/status_reporter_test.rb b/test/lib/skunk/commands/status_reporter_test.rb index 26585ab..74ca28d 100644 --- a/test/lib/skunk/commands/status_reporter_test.rb +++ b/test/lib/skunk/commands/status_reporter_test.rb @@ -2,54 +2,14 @@ require "test_helper" -require "rubycritic/analysers_runner" -require "skunk/rubycritic/analysed_modules_collection" 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}" + it "reports a simple status message" do + _(reporter.update_status_message).must_equal "" 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/config_test.rb b/test/lib/skunk/config_test.rb index 3664d63..a04e1c6 100644 --- a/test/lib/skunk/config_test.rb +++ b/test/lib/skunk/config_test.rb @@ -12,7 +12,7 @@ def setup end def test_default_format - assert_equal [:json], Config.formats + assert_equal [:console], Config.formats end def test_set_formats_with_array @@ -32,23 +32,23 @@ def test_set_formats_filters_unsupported_formats def test_set_formats_with_empty_array_defaults_to_json Config.formats = [] - assert_equal [:json], Config.formats + assert_equal [:console], Config.formats end def test_add_format Config.add_format(:html) - assert_equal %i[json html], Config.formats + assert_equal %i[console html], Config.formats end def test_add_format_ignores_duplicates Config.add_format(:html) Config.add_format(:html) # This should be ignored as duplicate - assert_equal %i[json html], Config.formats + assert_equal %i[console html], Config.formats end def test_add_format_ignores_unsupported_formats Config.add_format(:unsupported) - assert_equal [:json], Config.formats + assert_equal [:console], Config.formats end def test_remove_format @@ -59,7 +59,7 @@ def test_remove_format def test_remove_format_defaults_to_json_when_empty Config.remove_format(:json) - assert_equal [:json], Config.formats + assert_equal [:console], Config.formats end def test_supported_format @@ -70,14 +70,14 @@ def test_supported_format end def test_supported_formats - expected = %i[json html] + expected = %i[json html console] assert_equal expected, Config.supported_formats end def test_reset Config.formats = [:html] Config.reset - assert_equal [:json], Config.formats + assert_equal [:console], Config.formats end end end diff --git a/test/lib/skunk/generators/console/simple_test.rb b/test/lib/skunk/generators/console/simple_test.rb new file mode 100644 index 0000000..4e2b1d8 --- /dev/null +++ b/test/lib/skunk/generators/console/simple_test.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require "test_helper" + +require "skunk/generators/console/simple" + +# Helper methods for this test file +module SkunkMethods + # Returns the count of non-test modules + # @return [Integer] + def analysed_modules_count + @analysed_modules_count ||= non_test_modules.count + end + + # Returns the total Skunk score across all non-test modules + # @return [Float] + def skunk_score_total + @skunk_score_total ||= non_test_modules.sum(&:skunk_score) + end + + # Returns the average Skunk score across all non-test modules + # @return [Float] + def skunk_score_average + return 0.0 if analysed_modules_count.zero? + + (skunk_score_total.to_d / analysed_modules_count).to_f.round(2) + end + + # Returns the total churn times cost across all non-test modules + # @return [Float] + def total_churn_times_cost + @total_churn_times_cost ||= non_test_modules.sum(&:churn_times_cost) + end + + # Returns the module with the highest Skunk score (worst performing) + # @return [Object, nil] + def worst_module + @worst_module ||= sorted_modules.first + end + + # Returns modules sorted by Skunk score in descending order (worst first) + # @return [Array] + def sorted_modules + @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! + end + + # Returns only non-test modules (excludes test and spec directories) + # @return [Array] + def non_test_modules + @non_test_modules ||= reject do |a_module| + test_module?(a_module) + end + end + + # Returns a hash representation of the analysis results + # @return [Hash] + def to_hash + { + analysed_modules_count: analysed_modules_count, + skunk_score_total: skunk_score_total, + skunk_score_average: skunk_score_average, + total_churn_times_cost: total_churn_times_cost, + worst_pathname: worst_module&.pathname, + worst_score: worst_module&.skunk_score, + files: files_as_hash + } + end + + # Returns files as an array of hashes (for JSON serialization) + # @return [Array] + def files_as_hash + @files_as_hash ||= sorted_modules.map(&:to_hash) + end + + private + + # Determines if a module is a test module based on its path + # @param a_module [Object] The module to check + # @return [Boolean] + def test_module?(a_module) + pathname = a_module.pathname + directory_is_test?(pathname) || filename_is_test?(pathname) + end + + # Checks if the directory path indicates a test module + # @param pathname [Pathname] The pathname to check + # @return [Boolean] + def directory_is_test?(pathname) + module_path = pathname.dirname.to_s + module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec") + end + + # Checks if the filename indicates a test module + # @param pathname [Pathname] The pathname to check + # @return [Boolean] + def filename_is_test?(pathname) + filename = pathname.basename.to_s + filename.end_with?("_test.rb", "_spec.rb") + end +end + +# Helper methods for creating mock objects +def create_analysed_module(path, options = {}) + # Create a simple object that responds to the methods we need + mock_module = Object.new + + # Define the methods we need + mock_module.define_singleton_method(:pathname) { Pathname.new(path) } + mock_module.define_singleton_method(:skunk_score) { options[:skunk_score] || 0.0 } + mock_module.define_singleton_method(:churn_times_cost) { options[:churn_times_cost] || 0.0 } + mock_module.define_singleton_method(:churn) { options[:churn] || 1 } + mock_module.define_singleton_method(:cost) { options[:cost] || 0.0 } + mock_module.define_singleton_method(:coverage) { options[:coverage] || 100.0 } + + # Add to_hash method for JSON serialization + mock_module.define_singleton_method(:to_hash) do + { + file: pathname.to_s, + skunk_score: skunk_score, + churn_times_cost: churn_times_cost, + churn: churn, + cost: cost.round(2), + coverage: coverage.round(2) + } + end + + mock_module +end + +# Creates a collection of analysed modules for testing +# @param analysed_modules [Array] Array of analysed modules +# @return [Object] A collection with the modules +def create_collection(analysed_modules) + # Create a simple array that responds to the methods we need + collection = analysed_modules.dup + + # Add the Skunk methods to the collection + collection.extend(SkunkMethods) + collection +end + +# Creates a simple mock collection for testing console generators +# @return [Object] A collection with one mock module +def create_simple_mock_collection + mock_module = create_analysed_module("samples/rubycritic/analysed_module.rb", + skunk_score: 0.59, + churn_times_cost: 0.59, + churn: 1, + cost: 0.59, + coverage: 100.0) + create_collection([mock_module]) +end + +module Skunk + module Generator + module Console + class SimpleTest < Minitest::Test + def setup + @analysed_modules = create_simple_mock_collection + @simple = Simple.new(@analysed_modules) + end + + def test_initializes_with_analysed_modules + assert_equal @analysed_modules, @simple.instance_variable_get(:@analysed_modules) + end + + def test_render_includes_expected_content + output = @simple.render + + assert_includes output, "SkunkScore Total:" + assert_includes output, "Modules Analysed:" + assert_includes output, "SkunkScore Average:" + assert_includes output, "Generated with Skunk" + end + + def test_render_includes_table_headers + output = @simple.render + + assert_includes output, "file" + assert_includes output, "skunk_score" + assert_includes output, "churn_times_cost" + assert_includes output, "churn" + assert_includes output, "cost" + assert_includes output, "coverage" + end + + def test_headings_constant + expected_headings = %w[file skunk_score churn_times_cost churn cost coverage] + assert_equal expected_headings, Simple::HEADINGS + end + end + end + 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..6f880de --- /dev/null +++ b/test/lib/skunk/generators/console_report_test.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +require "test_helper" + +require "skunk/generators/console_report" + +# Helper methods for this test file +module SkunkMethods + # Returns the count of non-test modules + # @return [Integer] + def analysed_modules_count + @analysed_modules_count ||= non_test_modules.count + end + + # Returns the total Skunk score across all non-test modules + # @return [Float] + def skunk_score_total + @skunk_score_total ||= non_test_modules.sum(&:skunk_score) + end + + # Returns the average Skunk score across all non-test modules + # @return [Float] + def skunk_score_average + return 0.0 if analysed_modules_count.zero? + + (skunk_score_total.to_d / analysed_modules_count).to_f.round(2) + end + + # Returns the total churn times cost across all non-test modules + # @return [Float] + def total_churn_times_cost + @total_churn_times_cost ||= non_test_modules.sum(&:churn_times_cost) + end + + # Returns the module with the highest Skunk score (worst performing) + # @return [Object, nil] + def worst_module + @worst_module ||= sorted_modules.first + end + + # Returns modules sorted by Skunk score in descending order (worst first) + # @return [Array] + def sorted_modules + @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! + end + + # Returns only non-test modules (excludes test and spec directories) + # @return [Array] + def non_test_modules + @non_test_modules ||= reject do |a_module| + test_module?(a_module) + end + end + + # Returns a hash representation of the analysis results + # @return [Hash] + def to_hash + { + analysed_modules_count: analysed_modules_count, + skunk_score_total: skunk_score_total, + skunk_score_average: skunk_score_average, + total_churn_times_cost: total_churn_times_cost, + worst_pathname: worst_module&.pathname, + worst_score: worst_module&.skunk_score, + files: files_as_hash + } + end + + # Returns files as an array of hashes (for JSON serialization) + # @return [Array] + def files_as_hash + @files_as_hash ||= sorted_modules.map(&:to_hash) + end + + private + + # Determines if a module is a test module based on its path + # @param a_module [Object] The module to check + # @return [Boolean] + def test_module?(a_module) + pathname = a_module.pathname + directory_is_test?(pathname) || filename_is_test?(pathname) + end + + # Checks if the directory path indicates a test module + # @param pathname [Pathname] The pathname to check + # @return [Boolean] + def directory_is_test?(pathname) + module_path = pathname.dirname.to_s + module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec") + end + + # Checks if the filename indicates a test module + # @param pathname [Pathname] The pathname to check + # @return [Boolean] + def filename_is_test?(pathname) + filename = pathname.basename.to_s + filename.end_with?("_test.rb", "_spec.rb") + end +end + +# Helper methods for creating mock objects +def create_analysed_module(path, options = {}) + # Create a simple object that responds to the methods we need + mock_module = Object.new + + # Define the methods we need + mock_module.define_singleton_method(:pathname) { Pathname.new(path) } + mock_module.define_singleton_method(:skunk_score) { options[:skunk_score] || 0.0 } + mock_module.define_singleton_method(:churn_times_cost) { options[:churn_times_cost] || 0.0 } + mock_module.define_singleton_method(:churn) { options[:churn] || 1 } + mock_module.define_singleton_method(:cost) { options[:cost] || 0.0 } + mock_module.define_singleton_method(:coverage) { options[:coverage] || 100.0 } + + # Add to_hash method for JSON serialization + mock_module.define_singleton_method(:to_hash) do + { + file: pathname.to_s, + skunk_score: skunk_score, + churn_times_cost: churn_times_cost, + churn: churn, + cost: cost.round(2), + coverage: coverage.round(2) + } + end + + mock_module +end + +# Creates a collection of analysed modules for testing +# @param analysed_modules [Array] Array of analysed modules +# @return [Object] A collection with the modules +def create_collection(analysed_modules) + # Create a simple array that responds to the methods we need + collection = analysed_modules.dup + + # Add the Skunk methods to the collection + collection.extend(SkunkMethods) + collection +end + +# Creates a simple mock collection for testing console generators +# @return [Object] A collection with one mock module +def create_simple_mock_collection + mock_module = create_analysed_module("samples/rubycritic/analysed_module.rb", + skunk_score: 0.59, + churn_times_cost: 0.59, + churn: 1, + cost: 0.59, + coverage: 100.0) + create_collection([mock_module]) +end + +module Skunk + module Generator + class ConsoleReportTest < Minitest::Test + def setup + @analysed_modules = create_simple_mock_collection + @console_report = ConsoleReport.new(@analysed_modules) + end + + def test_initializes_with_analysed_modules + # Test that the console report was initialized with the analysed modules + assert_equal @analysed_modules, @console_report.instance_variable_get(:@analysed_modules) + end + + def test_generator_returns_console_simple_instance + generator = @console_report.send(:generator) + assert_instance_of Skunk::Generator::Console::Simple, generator + end + + def test_generate_report_calls_generator_render + # Test that generate_report calls the generator's render method + @console_report.send(:generator) + + # Mock the generator to verify it's called + mock_generator = Minitest::Mock.new + mock_generator.expect :render, "test output" + + @console_report.instance_variable_set(:@generator, mock_generator) + + # Capture stdout to test the output + output = capture_stdout do + @console_report.generate_report + end + + assert_equal "test output\n", output + mock_generator.verify + end + end + end +end diff --git a/test/lib/skunk/rubycritic/analysed_modules_collection_test.rb b/test/lib/skunk/rubycritic/analysed_modules_collection_test.rb index 5bf4e92..5592bfc 100644 --- a/test/lib/skunk/rubycritic/analysed_modules_collection_test.rb +++ b/test/lib/skunk/rubycritic/analysed_modules_collection_test.rb @@ -5,6 +5,141 @@ require "skunk/rubycritic/analysed_modules_collection" require "skunk/rubycritic/analysed_module" +# Helper methods for this test file +module SkunkMethods + # Returns the count of non-test modules + # @return [Integer] + def analysed_modules_count + @analysed_modules_count ||= non_test_modules.count + end + + # Returns the total Skunk score across all non-test modules + # @return [Float] + def skunk_score_total + @skunk_score_total ||= non_test_modules.sum(&:skunk_score) + end + + # Returns the average Skunk score across all non-test modules + # @return [Float] + def skunk_score_average + return 0.0 if analysed_modules_count.zero? + + (skunk_score_total.to_d / analysed_modules_count).to_f.round(2) + end + + # Returns the total churn times cost across all non-test modules + # @return [Float] + def total_churn_times_cost + @total_churn_times_cost ||= non_test_modules.sum(&:churn_times_cost) + end + + # Returns the module with the highest Skunk score (worst performing) + # @return [Object, nil] + def worst_module + @worst_module ||= sorted_modules.first + end + + # Returns modules sorted by Skunk score in descending order (worst first) + # @return [Array] + def sorted_modules + @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! + end + + # Returns only non-test modules (excludes test and spec directories) + # @return [Array] + def non_test_modules + @non_test_modules ||= reject do |a_module| + test_module?(a_module) + end + end + + # Returns a hash representation of the analysis results + # @return [Hash] + def to_hash + { + analysed_modules_count: analysed_modules_count, + skunk_score_total: skunk_score_total, + skunk_score_average: skunk_score_average, + total_churn_times_cost: total_churn_times_cost, + worst_pathname: worst_module&.pathname, + worst_score: worst_module&.skunk_score, + files: files_as_hash + } + end + + # Returns files as an array of hashes (for JSON serialization) + # @return [Array] + def files_as_hash + @files_as_hash ||= sorted_modules.map(&:to_hash) + end + + private + + # Determines if a module is a test module based on its path + # @param a_module [Object] The module to check + # @return [Boolean] + def test_module?(a_module) + pathname = a_module.pathname + directory_is_test?(pathname) || filename_is_test?(pathname) + end + + # Checks if the directory path indicates a test module + # @param pathname [Pathname] The pathname to check + # @return [Boolean] + def directory_is_test?(pathname) + module_path = pathname.dirname.to_s + module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec") + end + + # Checks if the filename indicates a test module + # @param pathname [Pathname] The pathname to check + # @return [Boolean] + def filename_is_test?(pathname) + filename = pathname.basename.to_s + filename.end_with?("_test.rb", "_spec.rb") + end +end + +# Helper methods for creating mock objects +def create_analysed_module(path, options = {}) + # Create a simple object that responds to the methods we need + mock_module = Object.new + + # Define the methods we need + mock_module.define_singleton_method(:pathname) { Pathname.new(path) } + mock_module.define_singleton_method(:skunk_score) { options[:skunk_score] || 0.0 } + mock_module.define_singleton_method(:churn_times_cost) { options[:churn_times_cost] || 0.0 } + mock_module.define_singleton_method(:churn) { options[:churn] || 1 } + mock_module.define_singleton_method(:cost) { options[:cost] || 0.0 } + mock_module.define_singleton_method(:coverage) { options[:coverage] || 100.0 } + + # Add to_hash method for JSON serialization + mock_module.define_singleton_method(:to_hash) do + { + file: pathname.to_s, + skunk_score: skunk_score, + churn_times_cost: churn_times_cost, + churn: churn, + cost: cost.round(2), + coverage: coverage.round(2) + } + end + + mock_module +end + +# Creates a collection of analysed modules for testing +# @param analysed_modules [Array] Array of analysed modules +# @return [Object] A collection with the modules +def create_collection(analysed_modules) + # Create a simple array that responds to the methods we need + collection = analysed_modules.dup + + # Add the Skunk methods to the collection + collection.extend(SkunkMethods) + collection +end + describe RubyCritic::AnalysedModulesCollection do let(:analysed_modules) { [] } let(:collection) { create_collection(analysed_modules) } @@ -248,38 +383,4 @@ _(hash[:files].first[:skunk_score]).must_equal 10.0 end end - - private - - def create_collection(modules) - # Create a collection by manually setting the @modules instance variable - # This bypasses the complex initialization that expects file paths - collection = RubyCritic::AnalysedModulesCollection.new([], []) - collection.instance_variable_set(:@modules, modules) - collection - end - - def create_analysed_module(path, skunk_score: 0.0, churn_times_cost: 0.0) - module_path = Pathname.new(path) - analysed_module = RubyCritic::AnalysedModule.new( - pathname: module_path, - smells: [], - churn: 1, - committed_at: Time.now - ) - - add_mock_methods_to_module(analysed_module, skunk_score, churn_times_cost) - end - - def add_mock_methods_to_module(analysed_module, skunk_score, churn_times_cost) - # Mock the skunk_score and churn_times_cost methods - analysed_module.define_singleton_method(:skunk_score) { @skunk_score ||= 0.0 } - analysed_module.define_singleton_method(:skunk_score=) { |value| @skunk_score = value } - analysed_module.define_singleton_method(:churn_times_cost) { @churn_times_cost ||= 0.0 } - analysed_module.define_singleton_method(:churn_times_cost=) { |value| @churn_times_cost = value } - - analysed_module.skunk_score = skunk_score - analysed_module.churn_times_cost = churn_times_cost - analysed_module - end end diff --git a/test/samples/console_output.txt b/test/samples/console_output.txt deleted file mode 100644 index 96dc3eb..0000000 --- a/test/samples/console_output.txt +++ /dev/null @@ -1,10 +0,0 @@ -+---------------------------------------+----------------+------------------+--------------+--------------+--------------+ -| 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) diff --git a/test/test_helper.rb b/test/test_helper.rb index 3d4c2f1..658d345 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -5,12 +5,16 @@ require "simplecov-console" require "codecov" - SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ + formatters = [ SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::Console, - SimpleCov::Formatter::Codecov + SimpleCov::Formatter::Console ] + # Only add Codecov formatter if CODECOV_TOKEN is set + formatters << SimpleCov::Formatter::Codecov if ENV["CODECOV_TOKEN"] + + SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(formatters) + SimpleCov.start do add_filter "lib/skunk/version.rb" add_filter "test/lib" @@ -30,6 +34,27 @@ require "skunk" require "skunk/rubycritic/analysed_module" +# Helper modules for testing +module MockHelpers + # Helper methods for mocking in tests + + # Captures stdout output for testing + # @return [String] The captured output + def capture_stdout + old_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = old_stdout + end +end + +# Include helper modules in Minitest::Test +class Minitest::Test + include MockHelpers +end + def context(*args, &block) describe(*args, &block) end