diff --git a/.reek.yml b/.reek.yml index 5efc18b..ad749c6 100644 --- a/.reek.yml +++ b/.reek.yml @@ -7,6 +7,7 @@ detectors: FeatureEnvy: exclude: - Skunk::Command::StatusReporter#table + - Skunk::Generator::HtmlReport#create_directories_and_files InstanceVariableAssumption: exclude: - Skunk::Cli::Options::Argv @@ -22,9 +23,25 @@ detectors: - initialize - Skunk::Cli::Application#execute - Skunk::Cli::Options::Argv#parse + TooManyInstanceVariables: + exclude: + - Skunk::Generator::Html::FileData + - Skunk::Generator::Html::SkunkData UtilityFunction: exclude: - capture_output_streams - Skunk::Command::Compare#analyse_modified_files - Skunk::Command::Compare#build_details_path - Skunk::Command::Shareable#sharing? + - Skunk::Command::Shareable#share_enabled? + - Skunk::Command::StatusSharer#share_enabled? + - Skunk::Command::StatusSharer#share_url_empty? + - Skunk::Configuration#supported_format? + - Skunk::Configuration#supported_formats + ManualDispatch: + exclude: + - Skunk::Config#self.method_missing + - Skunk::Config#self.respond_to_missing? + BooleanParameter: + exclude: + - Skunk::Config#self.respond_to_missing? diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d66caad..f0b68db 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,14 +1,13 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2025-05-02 20:16:50 UTC using RuboCop version 1.75.4. +# on 2025-10-09 00:04: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 # versions of RuboCop, may require this file to be generated again. # Offense count: 1 -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec +# Configuration parameters: Severity. Gemspec/RequiredRubyVersion: Exclude: - 'skunk.gemspec' @@ -25,22 +24,23 @@ Layout/HeredocIndentation: Exclude: - 'lib/skunk/commands/status_reporter.rb' -# Offense count: 1 +# Offense count: 2 # Configuration parameters: AllowedParentClasses. Lint/MissingSuper: Exclude: - 'lib/skunk/cli/application.rb' + - 'lib/skunk/generators/html/overview.rb' # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 18 -# Offense count: 7 +# Offense count: 8 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 76 + Max: 79 # Offense count: 2 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. diff --git a/CHANGELOG.md b/CHANGELOG.md index c02164b..5af544c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ 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) -* +* [FEATURE: Add Skunk HTML Report](https://github.com/fastruby/skunk/pull/123) +* [FEATURE: Add Skunk::Config class](https://github.com/fastruby/skunk/pull/123) ## v0.5.4 / 2025-05-05 [(commits)](https://github.com/fastruby/skunk/compare/v0.5.3...v0.5.4) diff --git a/README.md b/README.md index b1c32a4..c3659a3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Skunk is a RubyCritic extension to calculate a SkunkScore for a file or project. * [Help commands](#help-commands) * [Generate the SkunkCore for your project](#generate-the-skunkcore-for-your-project) * [Comparing Feature Branches](#comparing-feature-branches) +* [Configuration](#configuration) + * [Setting Output Formats](#setting-output-formats) * [Sharing your SkunkScore](#sharing-your-skunkscore) * [Contributing](#contributing) * [Sponsorship](#sponsorship) @@ -152,6 +154,36 @@ Score: 340.3 This should give you an idea if you're moving in the direction of maintaining the code quality or not. In this case, the feature branch is decreasing the code quality because it has a higher SkunkScore than the main branch. +## Configuration + +### Setting Output Formats + +Skunk provides a simple configuration class to control output formats programmatically. You can use `Skunk::Config` to set which formats should be generated when running Skunk. + +**Supported formats:** +- `:json` - JSON report (default) +- `:html` - HTML report with visual charts and tables + +```ruby +require 'skunk/config' + +# Set multiple formats +Skunk::Config.formats = [:json, :html] + +# Add a format to the existing list +Skunk::Config.add_format(:html) + +# Remove a format +Skunk::Config.remove_format(:json) + +# Check supported formats +Skunk::Config.supported_formats # => [:json, :html] +Skunk::Config.supported_format?(:json) # => true + +# Reset to defaults +Skunk::Config.reset +``` + ## Sharing your SkunkScore If you want to share the results of your Skunk report with the Ruby community, run: diff --git a/bin/console b/bin/console index 70c0b95..2e978e4 100755 --- a/bin/console +++ b/bin/console @@ -11,4 +11,7 @@ puts ARGV.inspect # Run skunk CLI application with the provided arguments require "skunk/cli/application" +require "skunk/config" + +Skunk::Config.formats = %i[json html] Skunk::Cli::Application.new(ARGV).execute diff --git a/lib/skunk/commands/default.rb b/lib/skunk/commands/default.rb index cb8ad0d..7c2d2a5 100644 --- a/lib/skunk/commands/default.rb +++ b/lib/skunk/commands/default.rb @@ -26,8 +26,6 @@ def initialize(options) # # @return [Skunk::Command::StatusReporter] def execute - RubyCritic::Config.formats = [:json] - report(critique) status_reporter diff --git a/lib/skunk/commands/shareable.rb b/lib/skunk/commands/shareable.rb index 29fe97e..e86a9fa 100644 --- a/lib/skunk/commands/shareable.rb +++ b/lib/skunk/commands/shareable.rb @@ -18,6 +18,13 @@ def share(reporter) # @return [Boolean] If the environment is set to share to an external # service def sharing? + share_enabled? + end + + private + + # @return [Boolean] Check if sharing is enabled via environment variable + def share_enabled? ENV["SHARE"] == "true" end end diff --git a/lib/skunk/commands/status_sharer.rb b/lib/skunk/commands/status_sharer.rb index 596bc9e..af23d49 100644 --- a/lib/skunk/commands/status_sharer.rb +++ b/lib/skunk/commands/status_sharer.rb @@ -23,7 +23,7 @@ def share response = post_payload @status_message = - if Net::HTTPOK === response + if response.is_a?(Net::HTTPOK) data = JSON.parse response.body "Shared at: #{File.join(base_url, data['id'])}" else @@ -62,7 +62,17 @@ def json_results # :reek:UtilityFunction def not_sharing? - ENV["SHARE"] != "true" && ENV["SHARE_URL"].to_s == "" + !share_enabled? && share_url_empty? + end + + # @return [Boolean] Check if sharing is enabled via environment variable + def share_enabled? + ENV["SHARE"] == "true" + end + + # @return [Boolean] Check if share URL is empty + def share_url_empty? + ENV["SHARE_URL"].to_s == "" end def payload diff --git a/lib/skunk/config.rb b/lib/skunk/config.rb new file mode 100644 index 0000000..62b6967 --- /dev/null +++ b/lib/skunk/config.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Skunk + # Utility module for format validation + module FormatValidator + # Supported output formats + SUPPORTED_FORMATS = %i[json html].freeze + + # Check if a format is supported + # @param format [Symbol] Format to check + # @return [Boolean] True if format is supported + def self.supported_format?(format) + SUPPORTED_FORMATS.include?(format) + end + + # Get all supported formats + # @return [Array] All supported formats + def self.supported_formats + SUPPORTED_FORMATS.dup + end + end + + # Configuration class for Skunk that supports formats + # Similar to RubyCritic::Configuration but focused only on Skunk's needs + class Configuration + # Default format + DEFAULT_FORMAT = :json + + def initialize + @formats = [DEFAULT_FORMAT] + end + + def set(options = {}) + self.formats = options[:formats] if options.key?(:formats) + end + + # Get the configured formats + # @return [Array] Array of format symbols + attr_reader :formats + + # Set the formats with validation + # @param format_list [Array, Symbol] Format(s) to set + def formats=(format_list) + format_array = Array(format_list) + @formats = format_array.select { |format| FormatValidator.supported_format?(format) } + @formats = [DEFAULT_FORMAT] if @formats.empty? + end + + # Add a format to the existing list + # @param format [Symbol] Format to add + def add_format(format) + return unless FormatValidator.supported_format?(format) + + @formats << format unless @formats.include?(format) + end + + # Remove a format from the list + # @param format [Symbol] Format to remove + def remove_format(format) + @formats.delete(format) + @formats = [DEFAULT_FORMAT] if @formats.empty? + end + + # Check if a format is supported + # @param format [Symbol] Format to check + # @return [Boolean] True if format is supported + def supported_format?(format) + FormatValidator.supported_format?(format) + end + + # Get all supported formats + # @return [Array] All supported formats + def supported_formats + FormatValidator.supported_formats + end + + # Reset to default configuration + def reset + @formats = [DEFAULT_FORMAT] + end + end + + # Config module that delegates to Configuration instance + # Similar to RubyCritic::Config pattern + module Config + def self.configuration + @configuration ||= Configuration.new + end + + def self.set(options = {}) + configuration.set(options) + end + + def self.method_missing(method, *args, &block) + if configuration.respond_to?(method) + configuration.public_send(method, *args, &block) + else + super + end + end + + def self.respond_to_missing?(symbol, include_private = false) + configuration.respond_to?(symbol, include_private) || super + end + end +end diff --git a/lib/skunk/generators/html/file_data.rb b/lib/skunk/generators/html/file_data.rb new file mode 100644 index 0000000..ff7308a --- /dev/null +++ b/lib/skunk/generators/html/file_data.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Skunk + module Generator + module Html + # Data object for individual file information in the HTML overview report + class FileData + attr_reader :file, :skunk_score, :churn_times_cost, :churn, :cost, :coverage + + def initialize(module_data) + @file = PathTruncator.truncate(module_data.pathname) + @skunk_score = module_data.skunk_score + @churn_times_cost = module_data.churn_times_cost + @churn = module_data.churn + @cost = module_data.cost.round(2) + @coverage = module_data.coverage.round(2) + end + end + end + end +end diff --git a/lib/skunk/generators/html/overview.rb b/lib/skunk/generators/html/overview.rb new file mode 100644 index 0000000..1accdfe --- /dev/null +++ b/lib/skunk/generators/html/overview.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rubycritic/generators/html/base" + +require "skunk/generators/html/path_truncator" +require "skunk/generators/html/skunk_data" + +module Skunk + module Generator + module Html + # Generates an HTML overview report for the analysed modules. + class Overview < RubyCritic::Generator::Html::Base + def self.erb_template(template_path) + ERB.new(File.read(File.join(TEMPLATES_DIR, template_path))) + end + + TEMPLATES_DIR = File.expand_path("templates", __dir__) + TEMPLATE = erb_template("skunk_overview.html.erb") + + def initialize(analysed_modules) + @analysed_modules = analysed_modules + @data = SkunkData.new(analysed_modules) + end + + def file_name + "skunk_overview.html" + end + + def render + TEMPLATE.result(binding) + end + end + end + end +end diff --git a/lib/skunk/generators/html/path_truncator.rb b/lib/skunk/generators/html/path_truncator.rb new file mode 100644 index 0000000..6e5b4bf --- /dev/null +++ b/lib/skunk/generators/html/path_truncator.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Skunk + module Generator + module Html + # Utility class for truncating file paths to show only the relevant project structure + class PathTruncator + attr_reader :file_path + + # Common project folder names to truncate from + PROJECT_FOLDERS = %w[app lib src test spec features db].freeze + + # Truncates a file path to show only the relevant project structure + # starting from the first project folder found + # + # @param file_path [String] The full file path to truncate + # @return [String] The truncated path starting from the project folder + # :reek:NilCheck + def self.truncate(file_path) + return file_path if file_path.nil? + + new(file_path).truncate + end + + def initialize(file_path) + @file_path = file_path.to_s + end + + # :reek:TooManyStatements + def truncate + return file_path if file_path.empty? + + path_parts = file_path.split("/") + folder_index = path_parts.find_index do |part| + PROJECT_FOLDERS.include?(part) + end + + if folder_index + # rubocop:disable Style/SlicingWithRange + path_parts[folder_index..-1].join("/") + # rubocop:enable Style/SlicingWithRange + else + file_path + end + end + end + end + end +end diff --git a/lib/skunk/generators/html/skunk_data.rb b/lib/skunk/generators/html/skunk_data.rb new file mode 100644 index 0000000..19eadb0 --- /dev/null +++ b/lib/skunk/generators/html/skunk_data.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "skunk/generators/html/file_data" +require "skunk/generators/html/path_truncator" + +module Skunk + module Generator + module Html + # Data object for the HTML overview report + class SkunkData + attr_reader :generated_at, :skunk_version, + :analysed_modules_count, :skunk_score_total, :skunk_score_average, + :worst_pathname, :worst_score, :files + + def initialize(analysed_modules) + @analysed_modules = analysed_modules + @generated_at = Time.now.strftime("%Y-%m-%d %H:%M:%S") + @skunk_version = Skunk::VERSION + + @analysed_modules_count = non_test_modules.count + @skunk_score_total = non_test_modules.sum(&:skunk_score) + @skunk_score_average = calculate_average + @worst_pathname = PathTruncator.truncate(find_worst_module&.pathname) + @worst_score = find_worst_module&.skunk_score + @files = build_files + end + + private + + 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 calculate_average + return 0 if @analysed_modules_count.zero? + + (@skunk_score_total.to_d / @analysed_modules_count).round(2) + end + + def find_worst_module + @find_worst_module ||= sorted_modules.first + end + + def sorted_modules + @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! + end + + def build_files + @build_files ||= sorted_modules.map do |module_data| + FileData.new(module_data) + end + end + end + end + end +end diff --git a/lib/skunk/generators/html/templates/skunk_overview.html.erb b/lib/skunk/generators/html/templates/skunk_overview.html.erb new file mode 100644 index 0000000..9fea17e --- /dev/null +++ b/lib/skunk/generators/html/templates/skunk_overview.html.erb @@ -0,0 +1,308 @@ + + + + + + Skunk Report + + + +
+
+

Skunk Report

+

Code Quality vs. Code Coverage Analysis

+
+ +
+
+
<%= @data.analysed_modules_count %>
+
Modules Analysed
+
+
+
<%= @data.skunk_score_total %>
+
Total Skunk Score
+
+
+
<%= @data.skunk_score_average %>
+
Average Skunk Score
+
+
+ +
+

Skunk Analysis

+ + + + + + + + + + + + + <% @data.files.each do |item| %> + + + + + + + + + <% end %> + +
FileSkunk ScoreChurn × CostChurnCostCoverage
+ <%= item.file %> + + + <%= item.skunk_score %> + + + <%= item.churn_times_cost %> + + + <%= item.churn %> + + + <%= item.cost %> + + + <%= item.coverage %> +
+
+ + +
+ + diff --git a/lib/skunk/generators/html_report.rb b/lib/skunk/generators/html_report.rb new file mode 100644 index 0000000..095a654 --- /dev/null +++ b/lib/skunk/generators/html_report.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "fileutils" + +require "rubycritic/browser" +require "rubycritic/generators/html_report" + +require "skunk/generators/html/overview" + +module Skunk + module Generator + # Generates an HTML report for the analysed modules. + class HtmlReport < RubyCritic::Generator::HtmlReport + def initialize(analysed_modules) + super + @analysed_modules = analysed_modules + end + + def generate_report + create_directories_and_files + puts "Skunk report generated at #{report_location}" + browser.open unless RubyCritic::Config.no_browser + end + + def create_directories_and_files + Array(generators).each do |generator| + FileUtils.mkdir_p(generator.file_directory) + File.write(generator.file_pathname, generator.render) + end + end + + private + + def generators + @generators ||= [ + overview_generator + ] + end + + def overview_generator + @overview_generator ||= Skunk::Generator::Html::Overview.new(@analysed_modules) + end + end + end +end diff --git a/lib/skunk/generators/json/simple.rb b/lib/skunk/generators/json/simple.rb index b5eb5a6..1dc2086 100644 --- a/lib/skunk/generators/json/simple.rb +++ b/lib/skunk/generators/json/simple.rb @@ -1,30 +1,50 @@ # frozen_string_literal: true -require "rubycritic/generators/json/simple" +require "pathname" + +require "rubycritic/configuration" module Skunk module Generator module Json # Generates a JSON report for the analysed modules. - class Simple < RubyCritic::Generator::Json::Simple + class Simple + def initialize(analysed_modules) + @analysed_modules = analysed_modules + end + + FILE_NAME = "skunk_report.json" + + def render + JSON.dump(data) + end + def data { analysed_modules_count: analysed_modules_count, - skunk_score_average: skunk_score_average, skunk_score_total: skunk_score_total, - worst_pathname: worst&.pathname, - worst_score: worst&.skunk_score, - files: files + skunk_score_average: calculate_average, + worst_pathname: find_worst_module&.pathname, + worst_score: find_worst_module&.skunk_score, + files: build_files } end + def file_directory + @file_directory ||= Pathname.new(RubyCritic::Config.root) + end + + def file_pathname + Pathname.new(file_directory).join(FILE_NAME) + end + private def analysed_modules_count @analysed_modules_count ||= non_test_modules.count end - def skunk_score_average + def calculate_average return 0 if analysed_modules_count.zero? (skunk_score_total.to_d / analysed_modules_count).to_f.round(2) @@ -41,16 +61,16 @@ def non_test_modules end end - def worst - @worst ||= sorted_modules.first + def find_worst_module + @find_worst_module ||= sorted_modules.first end def sorted_modules @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse! end - def files - @files ||= sorted_modules.map(&:to_hash) + def build_files + @build_files ||= sorted_modules.map(&:to_hash) end end end diff --git a/lib/skunk/reporter.rb b/lib/skunk/reporter.rb index 2110601..8c7b399 100644 --- a/lib/skunk/reporter.rb +++ b/lib/skunk/reporter.rb @@ -1,25 +1,22 @@ # frozen_string_literal: true +require "skunk/config" + module Skunk # Pick the right report generator based on the format specified in the # configuration. If the format is not supported, it will default to ConsoleReport. module Reporter - REPORT_GENERATOR_CLASS_FORMATS = %i[json].freeze - def self.generate_report(analysed_modules) - RubyCritic::Config.formats.uniq.each do |format| + Config.formats.uniq.each do |format| report_generator_class(format).new(analysed_modules).generate_report end end def self.report_generator_class(config_format) - if REPORT_GENERATOR_CLASS_FORMATS.include? config_format - require "skunk/generators/#{config_format}_report" - Generator.const_get("#{config_format.capitalize}Report") - else - require "skunk/generators/console_report" - Generator::ConsoleReport - end + return unless Config.supported_format?(config_format) + + require "skunk/generators/#{config_format}_report" + Generator.const_get("#{config_format.capitalize}Report") end end end diff --git a/test/lib/skunk/application_test.rb b/test/lib/skunk/application_test.rb index 2752f36..41e369f 100644 --- a/test/lib/skunk/application_test.rb +++ b/test/lib/skunk/application_test.rb @@ -84,15 +84,17 @@ FileUtils.rm("tmp/shared_report.txt", force: true) FileUtils.mkdir_p("tmp") - env = ENV.to_hash.merge("SHARE" => "true") - - Object.stub_const(:ENV, env) do - 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 - output = File.read("tmp/shared_report.txt") - _(output).must_include(shared_message) + RubyCritic::AnalysedModule.stub_any_instance(:churn, 1) do + RubyCritic::AnalysedModule.stub_any_instance(:coverage, 100.0) do + Skunk::Command::Default.stub_any_instance(:share_enabled?, true) do + Skunk::Command::StatusSharer.stub_any_instance(:not_sharing?, false) do + Skunk::Command::StatusSharer.stub_any_instance(:share, "Shared at: https://skunk.fastruby.io/j") do + result = application.execute + _(result).must_equal success_code + output = File.read("tmp/shared_report.txt") + _(output).must_include(shared_message) + end + end end end end diff --git a/test/lib/skunk/config_test.rb b/test/lib/skunk/config_test.rb new file mode 100644 index 0000000..ceaf304 --- /dev/null +++ b/test/lib/skunk/config_test.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require_relative "../../../lib/skunk/config" + +module Skunk + class ConfigTest < Minitest::Test + def setup + Config.reset + end + + def test_default_format + assert_equal [:json], Config.formats + end + + def test_set_formats_with_array + Config.formats = %i[html json] + assert_equal %i[html json], Config.formats + end + + def test_set_formats_with_single_format + Config.formats = :html + assert_equal [:html], Config.formats + end + + def test_set_formats_filters_unsupported_formats + Config.formats = %i[html json unsupported xml] + assert_equal %i[html json], Config.formats + end + + def test_set_formats_with_empty_array_defaults_to_json + Config.formats = [] + assert_equal [:json], Config.formats + end + + def test_add_format + Config.add_format(:html) + assert_equal %i[json html], Config.formats + end + + def test_add_format_ignores_duplicates + Config.add_format(:html) + Config.add_format(:html) + assert_equal %i[json html], Config.formats + end + + def test_add_format_ignores_unsupported_formats + Config.add_format(:unsupported) + assert_equal [:json], Config.formats + end + + def test_remove_format + Config.formats = %i[json html] + Config.remove_format(:html) + assert_equal [:json], Config.formats + end + + def test_remove_format_defaults_to_json_when_empty + Config.remove_format(:json) + assert_equal [:json], Config.formats + end + + def test_supported_format + assert Config.supported_format?(:json) + assert Config.supported_format?(:html) + refute Config.supported_format?(:xml) + refute Config.supported_format?(:unsupported) + end + + def test_supported_formats + expected = %i[json html] + assert_equal expected, Config.supported_formats + end + + def test_reset + Config.formats = [:html] + Config.reset + assert_equal [:json], Config.formats + end + end +end diff --git a/test/lib/skunk/generators/html/path_truncator_test.rb b/test/lib/skunk/generators/html/path_truncator_test.rb new file mode 100644 index 0000000..e8236ee --- /dev/null +++ b/test/lib/skunk/generators/html/path_truncator_test.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "test_helper" + +require "skunk/generators/html/path_truncator" + +# rubocop:disable Metrics/BlockLength +describe Skunk::Generator::Html::PathTruncator do + describe ".truncate" do + context "when path contains app folder" do + it "truncates to show from app folder onwards" do + path = "/Users/juan/code/project/app/services/dummy_service.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "app/services/dummy_service.rb" + end + end + + context "when path contains lib folder" do + it "truncates to show from lib folder onwards" do + path = "/Users/juan/code/project/lib/skunk/generators/html/overview.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "lib/skunk/generators/html/overview.rb" + end + end + + context "when path contains spec folder" do + it "truncates to show from spec folder onwards" do + path = "/Users/juan/code/project/spec/skunk/generators/html_report_spec.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "spec/skunk/generators/html_report_spec.rb" + end + end + + context "when path contains test folder" do + it "truncates to show from test folder onwards" do + path = "/Users/juan/code/project/test/lib/skunk/generators/html/path_truncator_test.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "test/lib/skunk/generators/html/path_truncator_test.rb" + end + end + + context "when path contains src folder" do + it "truncates to show from src folder onwards" do + path = "/Users/juan/code/project/src/components/header.js" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "src/components/header.js" + end + end + + context "when path contains features folder" do + it "truncates to show from features folder onwards" do + path = "/Users/juan/code/project/features/user_authentication.feature" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "features/user_authentication.feature" + end + end + + context "when path contains db folder" do + it "truncates to show from db folder onwards" do + path = "/Users/juan/code/project/db/migrate/20231201_create_users.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "db/migrate/20231201_create_users.rb" + end + end + + context "when path is already relative" do + it "returns the path unchanged" do + path = "app/services/dummy_service.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "app/services/dummy_service.rb" + end + end + + context "when path is already relative with lib folder" do + it "returns the path unchanged" do + path = "lib/skunk/generators/html/overview.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "lib/skunk/generators/html/overview.rb" + end + end + + context "when path does not contain any project folders" do + it "returns the original path" do + path = "/Users/juan/Documents/random_file.txt" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "/Users/juan/Documents/random_file.txt" + end + end + + context "when path is empty string" do + it "returns empty string" do + path = "" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "" + end + end + + context "when path is nil" do + it "returns nil" do + path = nil + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_be_nil + end + end + + context "when path is a Pathname object" do + it "converts to string and truncates correctly" do + require "pathname" + path = Pathname.new("/Users/juan/code/project/app/services/dummy_service.rb") + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "app/services/dummy_service.rb" + end + end + + context "when multiple project folders exist" do + it "truncates from the first project folder found" do + path = "/Users/juan/code/project/app/lib/services/dummy_service.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "app/lib/services/dummy_service.rb" + end + end + + context "when path has nested project structure" do + it "truncates from the first project folder" do + path = "/Users/juan/code/project/app/services/free/my_service.rb" + result = Skunk::Generator::Html::PathTruncator.truncate(path) + + _(result).must_equal "app/services/free/my_service.rb" + end + end + end + + describe "#initialize" do + it "stores the file path" do + path = "/Users/juan/code/project/app/services/dummy_service.rb" + truncator = Skunk::Generator::Html::PathTruncator.new(path) + + _(truncator.instance_variable_get(:@file_path)).must_equal path + end + end + + describe "#truncate" do + let(:path) { "/Users/juan/code/project/app/services/dummy_service.rb" } + let(:truncator) { Skunk::Generator::Html::PathTruncator.new(path) } + + it "returns the truncated path" do + result = truncator.truncate + + _(result).must_equal "app/services/dummy_service.rb" + end + end + + describe "PROJECT_FOLDERS constant" do + it "contains the expected project folder names" do + expected_folders = %w[app lib src test spec features db] + + _(Skunk::Generator::Html::PathTruncator::PROJECT_FOLDERS).must_equal expected_folders + end + + it "is frozen" do + _(Skunk::Generator::Html::PathTruncator::PROJECT_FOLDERS).must_be :frozen? + end + end +end +# rubocop:enable Metrics/BlockLength