From a5a960d90095d57e7ce348f5e355c2d3808eb456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 7 Oct 2025 17:56:46 -0600 Subject: [PATCH 1/9] Implement HTML report generation and update report formats Closes #50 - Allow Skunk to HTML output the results it generates - Added support for generating HTML reports alongside existing JSON format. - Introduced new classes for HTML report generation, including Overview, OverviewData, and FileData. - Updated the Reporter module to include HTML in the report generator formats. - Created a template for the HTML report with a responsive design. - Refactored JSON report generation to improve code organization and clarity. --- .reek.yml | 5 + .rubocop_todo.yml | 9 +- lib/skunk/generators/html/file_data.rb | 21 ++ lib/skunk/generators/html/overview.rb | 37 +++ lib/skunk/generators/html/path_truncator.rb | 49 ++++ lib/skunk/generators/html/skunk_data.rb | 59 ++++ .../html/templates/skunk_overview.html.erb | 260 ++++++++++++++++++ lib/skunk/generators/html_report.rb | 45 +++ lib/skunk/generators/json/simple.rb | 18 +- lib/skunk/reporter.rb | 2 +- .../generators/html/path_truncator_test.rb | 179 ++++++++++++ 11 files changed, 669 insertions(+), 15 deletions(-) create mode 100644 lib/skunk/generators/html/file_data.rb create mode 100644 lib/skunk/generators/html/overview.rb create mode 100644 lib/skunk/generators/html/path_truncator.rb create mode 100644 lib/skunk/generators/html/skunk_data.rb create mode 100644 lib/skunk/generators/html/templates/skunk_overview.html.erb create mode 100644 lib/skunk/generators/html_report.rb create mode 100644 test/lib/skunk/generators/html/path_truncator_test.rb diff --git a/.reek.yml b/.reek.yml index 5efc18b..1cb1f88 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,6 +23,10 @@ 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 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d66caad..93bbd66 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-08 01:52:15 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' @@ -36,11 +35,11 @@ Lint/MissingSuper: Metrics/AbcSize: Max: 18 -# Offense count: 7 +# Offense count: 9 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 76 + Max: 131 # Offense count: 2 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 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..46e4838 --- /dev/null +++ b/lib/skunk/generators/html/overview.rb @@ -0,0 +1,37 @@ +# 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) + super + @analysed_modules = analysed_modules + @data = SkunkData.new(analysed_modules) + end + + def file_name + "skunk_overview.html" + end + + def render + data = @data + 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..fabb422 --- /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_f / @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..0630728 --- /dev/null +++ b/lib/skunk/generators/html/templates/skunk_overview.html.erb @@ -0,0 +1,260 @@ + + + + + + Skunk Report + + + +
+
+

Skunk Report

+

Code Quality vs. Code Coverage Analysis

+
+ +
+
+
<%= data.skunk_score_total.round(2) %>
+
SkunkScore Total
+
+
+
<%= data.analysed_modules_count %>
+
Modules Analysed
+
+
+
<%= data.skunk_score_average %>
+
SkunkScore Average
+
+
+ + <% if data.worst_score && data.worst_pathname %> +
+
+
<%= data.worst_pathname %>
+
Worst File
+
+
+
<%= data.worst_score %>
+
Worst SkunkScore
+
+
+ <% end %> + +
+

Skunk Analysis

+ <% data.files.each do |file| %> +
+
+
<%= file.file %>
+
+
+
+
Skunk Score
+
+ <%= file.skunk_score %> +
+
+
+
Churn × Cost
+
<%= file.churn_times_cost %>
+
+
+
Churn
+
<%= file.churn %>
+
+
+
Cost
+
<%= file.cost %>
+
+
+
Coverage
+
<%= file.coverage %>%
+
+
+
+ <% end %> +
+ + +
+ + 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..38af1b8 100644 --- a/lib/skunk/generators/json/simple.rb +++ b/lib/skunk/generators/json/simple.rb @@ -10,11 +10,11 @@ class Simple < RubyCritic::Generator::Json::Simple 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 @@ -24,7 +24,7 @@ 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 +41,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..eb5e4dd 100644 --- a/lib/skunk/reporter.rb +++ b/lib/skunk/reporter.rb @@ -4,7 +4,7 @@ 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 + REPORT_GENERATOR_CLASS_FORMATS = %i[json html].freeze def self.generate_report(analysed_modules) RubyCritic::Config.formats.uniq.each do |format| 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 From a43d18da9f845054ca60c0cbf436491bfcb5353b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 14:19:29 -0600 Subject: [PATCH 2/9] Improbe table content --- .../html/templates/skunk_overview.html.erb | 213 ++++++------------ 1 file changed, 63 insertions(+), 150 deletions(-) diff --git a/lib/skunk/generators/html/templates/skunk_overview.html.erb b/lib/skunk/generators/html/templates/skunk_overview.html.erb index 0630728..2dbfb12 100644 --- a/lib/skunk/generators/html/templates/skunk_overview.html.erb +++ b/lib/skunk/generators/html/templates/skunk_overview.html.erb @@ -32,43 +32,46 @@ p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; } + /* End Modern CSS Reset */ :root { /* Color Palette */ - --primary-gradient-start:rgb(0, 210, 66); - --primary-gradient-end: rgb(147, 234, 174);; + --primary-gradient-start:rgb(8, 58, 19); + --primary-gradient-end: rgb(8, 98, 44);; --background-color: #f5f5f5; --text-color: #333; --text-color-light: #666; --white: #ffffff; --border-color: #eee; - --card-shadow: rgba(0,0,0,0.1); - --container-shadow: rgba(0,0,0,0.1); + --shadow-color: rgba(0,0,0,0.1); - /* Header Colors */ - --header-text: white; - --header-text-opacity: 0.9; + --cards-background-color: #f8f9fa; + --card-background-color: #ffffff; + + /* Skunk Score Colors */ + --score-high: #e74c3c; + --score-medium: #f39c12; + --score-low: #27ae60; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; - padding: clamp(10px, 2vw, 25px); + padding: 2rem; background-color: var(--background-color); color: var(--text-color); } .container { - max-width: min(1200px, 95vw); margin: 0 auto; background: var(--white); - border-radius: clamp(4px, 1vw, 12px); - box-shadow: 0 2px 10px var(--container-shadow); + border-radius: 2rem; + box-shadow: 0 2px 10px var(--shadow-color); overflow: hidden; } .header { background: linear-gradient(135deg, var(--primary-gradient-start) 0%, var(--primary-gradient-end) 100%); - color: var(--header-text); - padding: clamp(20px, 4vw, 40px); + color: var(--white); + padding: 2rem; text-align: center; display: flex; flex-direction: column; @@ -76,109 +79,61 @@ justify-content: center; } .header h1 { - margin: 0; - font-size: clamp(1.8em, 5vw, 3em); + font-size: 3rem; font-weight: 300; } .header p { - margin: clamp(8px, 2vw, 15px) 0 0 0; - opacity: var(--header-text-opacity); - font-size: clamp(0.9em, 2vw, 1.1em); + font-size: 1.5rem; } - .stats { + .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: clamp(15px, 3vw, 25px); - padding: clamp(20px, 4vw, 40px); - background: #f8f9fa; + gap: 2rem; + padding: 2rem; + background: var(--cards-background-color); } - .stat-card { - background: var(--white); - padding: clamp(15px, 3vw, 25px); - border-radius: 8px; + + .card { + background: var(--card-background-color); + padding: 2rem; + border-radius: 2rem; text-align: center; - box-shadow: 0 2px 4px var(--card-shadow); display: flex; flex-direction: column; justify-content: center; } - .stat-value { - font-size: clamp(1.5em, 4vw, 2.5em); + + .value { + font-size: clamp(2rem, 4vw, 3rem); font-weight: bold; color: var(--text-color); - margin-bottom: 5px; + margin-bottom: 0.5rem; } - .stat-label { + + .label { color: var(--text-color-light); - font-size: clamp(0.8em, 2vw, 1em); + font-size: clamp(1rem, 2vw, 2rem); text-transform: uppercase; letter-spacing: 0.5px; } - .files-container { - padding: clamp(20px, 4vw, 40px); - } - .files-container h2 { - margin: 0 0 clamp(15px, 3vw, 25px) 0; - color: var(--text-color); - font-size: clamp(1.3em, 3vw, 1.8em); - } - .file-card { - background: var(--white); - border-radius: 8px; - box-shadow: 0 2px 4px var(--card-shadow); - margin-bottom: clamp(10px, 2vw, 20px); - padding: clamp(15px, 3vw, 25px); - border-left: 4px solid var(--primary-gradient-start); - transition: transform 0.2s ease, box-shadow 0.2s ease; - } - .file-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px var(--card-shadow); - } - .file-header { - margin-bottom: clamp(10px, 2vw, 20px); - } - .file-name { - font-family: 'Monaco', 'Menlo', monospace; - font-size: clamp(1em, 2.5vw, 1.2em); - font-weight: 600; - color: var(--text-color); - flex: 1; - min-width: clamp(150px, 30vw, 250px); - } - .file-metrics { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(clamp(100px, 20vw, 150px), 1fr)); - gap: clamp(10px, 2vw, 20px); - } - .metric { + + .footer { + background: var(--cards-background-color); + padding: 1rem 2rem; text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - } - .metric-label { - font-size: clamp(0.7em, 1.8vw, 0.9em); color: var(--text-color-light); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: clamp(3px, 1vw, 8px); - } - .metric-value { - font-size: clamp(1em, 2.5vw, 1.4em); - font-weight: 600; - color: var(--text-color); + border-top: 1px solid var(--border-color); + font-size: 1rem; } /* Modern responsive design using CSS Grid and Flexbox */ /* All responsive behavior is now handled by clamp(), min(), and auto-fit */ - .footer { - background: #f8f9fa; - padding: clamp(15px, 3vw, 25px) clamp(20px, 4vw, 35px); - text-align: center; - color: var(--text-color-light); - border-top: 1px solid var(--border-color); - font-size: clamp(0.8em, 2vw, 1em); + + /* Mobile-first responsive table */ + @media (max-width: 768px) { + body { + background-color: red; + } } @@ -189,72 +144,30 @@

Code Quality vs. Code Coverage Analysis

-
-
-
<%= data.skunk_score_total.round(2) %>
-
SkunkScore Total
+
+
+
1
+
Modules Analysed
-
-
<%= data.analysed_modules_count %>
-
Modules Analysed
+
+
833.04
+
Total Skunk Score
-
-
<%= data.skunk_score_average %>
-
SkunkScore Average
+
+
833.04
+
Average Skunk Score
-
+
- <% if data.worst_score && data.worst_pathname %> -
-
-
<%= data.worst_pathname %>
-
Worst File
-
-
-
<%= data.worst_score %>
-
Worst SkunkScore
-
-
- <% end %> - -
+

Skunk Analysis

- <% data.files.each do |file| %> -
-
-
<%= file.file %>
-
-
-
-
Skunk Score
-
- <%= file.skunk_score %> -
-
-
-
Churn × Cost
-
<%= file.churn_times_cost %>
-
-
-
Churn
-
<%= file.churn %>
-
-
-
Cost
-
<%= file.cost %>
-
-
-
Coverage
-
<%= file.coverage %>%
-
-
-
- <% end %> -
+ +
+ - +
From dc1440163c70b1d8b2d95bfef0657b935eb816a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 16:26:40 -0600 Subject: [PATCH 3/9] Add Table and Card views --- .rubocop_todo.yml | 9 +- lib/skunk/generators/html/overview.rb | 1 - .../html/templates/skunk_overview.html.erb | 161 ++++++++++++++++-- 3 files changed, 153 insertions(+), 18 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 93bbd66..9055fa2 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-08 01:52:15 UTC using RuboCop version 1.81.1. +# on 2025-10-08 22:28:33 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 @@ -24,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: 9 +# Offense count: 7 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: - Max: 131 + Max: 76 # Offense count: 2 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. diff --git a/lib/skunk/generators/html/overview.rb b/lib/skunk/generators/html/overview.rb index 46e4838..a9ec514 100644 --- a/lib/skunk/generators/html/overview.rb +++ b/lib/skunk/generators/html/overview.rb @@ -18,7 +18,6 @@ def self.erb_template(template_path) TEMPLATE = erb_template("skunk_overview.html.erb") def initialize(analysed_modules) - super @analysed_modules = analysed_modules @data = SkunkData.new(analysed_modules) end diff --git a/lib/skunk/generators/html/templates/skunk_overview.html.erb b/lib/skunk/generators/html/templates/skunk_overview.html.erb index 2dbfb12..768db0d 100644 --- a/lib/skunk/generators/html/templates/skunk_overview.html.erb +++ b/lib/skunk/generators/html/templates/skunk_overview.html.erb @@ -104,7 +104,7 @@ } .value { - font-size: clamp(2rem, 4vw, 3rem); + font-size: clamp(1rem, 2vw, 1.5rem); font-weight: bold; color: var(--text-color); margin-bottom: 0.5rem; @@ -112,7 +112,7 @@ .label { color: var(--text-color-light); - font-size: clamp(1rem, 2vw, 2rem); + font-size: clamp(1rem, 2vw, 1.5rem); text-transform: uppercase; letter-spacing: 0.5px; } @@ -126,13 +126,109 @@ font-size: 1rem; } - /* Modern responsive design using CSS Grid and Flexbox */ - /* All responsive behavior is now handled by clamp(), min(), and auto-fit */ + .table-section { + padding: 2rem; + background: var(--white); + } + + .table-section h2 { + margin-bottom: 1.5rem; + color: var(--text-color); + font-size: clamp(1.5rem, 3vw, 2rem); + } + + /* Responsive Table Styles */ + .skunk-results { + width: 100%; + border-collapse: collapse; + background: var(--white); + border-radius: 1rem; + overflow: hidden; + box-shadow: 0 2px 8px var(--shadow-color); + } + + .skunk-results thead { + background: var(--primary-gradient-end); + color: var(--white); + } + + .skunk-results th { + padding: 1rem; + text-align: right; + font-weight: 600; + font-size: 1rem; + } + + .skunk-results th:first-child { + text-align: left; + } + + .table-row { + border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s ease; + } + + .table-row:hover { + background-color: var(--cards-background-color); + } + + .table-row td { + padding: 1rem; + border: none; + text-align: right; + } + + .table-row .filename { + text-align: left; + } + + .table-row td label { + display: none; + } + + .filename { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + word-break: break-all; + } + + .score { + font-weight: bold; + } + + /* Mobile Card Layout */ + @media screen and (max-width: 800px) { + .skunk-results thead { + display: none; + } + + .skunk-results { + box-shadow: none; + } + + .table-row { + display: grid; + grid-template-columns: 1fr; + height: auto; + margin-bottom: 1rem; + border: 1px solid var(--border-color); + border-radius: 1rem; + background: var(--card-background-color); + box-shadow: 0 2px 4px var(--shadow-color); + } + + .table-row .filename { + background: var(--cards-background-color); + } + + .table-row td { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0.5rem; + } - /* Mobile-first responsive table */ - @media (max-width: 768px) { - body { - background-color: red; + .table-row td label { + display: inline-block; } } @@ -146,22 +242,61 @@
-
1
+
<%= data.analysed_modules_count %>
Modules Analysed
-
833.04
+
<%= data.skunk_score_total %>
Total Skunk Score
-
833.04
+
<%= 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 %> +
From 6789fa7cd777264dbdf0958baee4d3d57f6438a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 17:02:50 -0600 Subject: [PATCH 4/9] Enable HTML report --- lib/skunk/commands/default.rb | 2 -- lib/skunk/generators/html/skunk_data.rb | 2 +- lib/skunk/reporter.rb | 11 ++++------- 3 files changed, 5 insertions(+), 10 deletions(-) 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/generators/html/skunk_data.rb b/lib/skunk/generators/html/skunk_data.rb index fabb422..19eadb0 100644 --- a/lib/skunk/generators/html/skunk_data.rb +++ b/lib/skunk/generators/html/skunk_data.rb @@ -37,7 +37,7 @@ def non_test_modules def calculate_average return 0 if @analysed_modules_count.zero? - (@skunk_score_total.to_f / @analysed_modules_count).round(2) + (@skunk_score_total.to_d / @analysed_modules_count).round(2) end def find_worst_module diff --git a/lib/skunk/reporter.rb b/lib/skunk/reporter.rb index eb5e4dd..850149e 100644 --- a/lib/skunk/reporter.rb +++ b/lib/skunk/reporter.rb @@ -13,13 +13,10 @@ def self.generate_report(analysed_modules) 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 if REPORT_GENERATOR_CLASS_FORMATS.none? { |format| format == config_format } + + require "skunk/generators/#{config_format}_report" + Generator.const_get("#{config_format.capitalize}Report") end end end From 0b33ec477c17f21b84bff604b5ab616a15fa30eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 17:22:55 -0600 Subject: [PATCH 5/9] Set :json format as default --- .reek.yml | 9 +++ README.md | 32 ++++++++++ lib/skunk/config.rb | 106 ++++++++++++++++++++++++++++++++++ lib/skunk/reporter.rb | 8 +-- test/lib/skunk/config_test.rb | 81 ++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 lib/skunk/config.rb create mode 100644 test/lib/skunk/config_test.rb diff --git a/.reek.yml b/.reek.yml index 1cb1f88..c4f99e9 100644 --- a/.reek.yml +++ b/.reek.yml @@ -33,3 +33,12 @@ detectors: - Skunk::Command::Compare#analyse_modified_files - Skunk::Command::Compare#build_details_path - Skunk::Command::Shareable#sharing? + - 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/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/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/reporter.rb b/lib/skunk/reporter.rb index 850149e..8c7b399 100644 --- a/lib/skunk/reporter.rb +++ b/lib/skunk/reporter.rb @@ -1,19 +1,19 @@ # 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 html].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) - return if REPORT_GENERATOR_CLASS_FORMATS.none? { |format| format == config_format } + return unless Config.supported_format?(config_format) require "skunk/generators/#{config_format}_report" Generator.const_get("#{config_format.capitalize}Report") 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 From 4aa19a11882a849c0d40216d7b0298745238acc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 17:44:12 -0600 Subject: [PATCH 6/9] Update json report file name --- bin/console | 3 +++ lib/skunk/generators/json/simple.rb | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) 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/generators/json/simple.rb b/lib/skunk/generators/json/simple.rb index 38af1b8..1dc2086 100644 --- a/lib/skunk/generators/json/simple.rb +++ b/lib/skunk/generators/json/simple.rb @@ -1,12 +1,24 @@ # 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, @@ -18,6 +30,14 @@ def data } 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 From d0939b60a4e087fc1975fe531c0bb6351a344419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 17:51:36 -0600 Subject: [PATCH 7/9] Fix segmentation fault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1: Unused Variable Warning ✅ File: lib/skunk/generators/html/overview.rb:30 Problem: data = @data was assigned but never used Fix: Removed the unused variable assignment Issue 2: Segmentation Fault ✅ File: lib/skunk/commands/status_sharer.rb:26 Problem: Net::HTTPOK === response causes segfault in Ruby 2.7 Fix: Changed to safer response.is_a?(Net::HTTPOK) comparison The Problem: The ERB template was using a local variable data that was assigned from @data, but Ruby's static analysis couldn't detect that the variable was being used by the ERB template, causing: Ruby 2.6: NameError: undefined local variable or method 'data' Ruby 3.3: Same error Warning: "assigned but unused variable - data" --- lib/skunk/commands/status_sharer.rb | 2 +- lib/skunk/generators/html/overview.rb | 1 - .../generators/html/templates/skunk_overview.html.erb | 10 +++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/skunk/commands/status_sharer.rb b/lib/skunk/commands/status_sharer.rb index 596bc9e..ab0165c 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 diff --git a/lib/skunk/generators/html/overview.rb b/lib/skunk/generators/html/overview.rb index a9ec514..1accdfe 100644 --- a/lib/skunk/generators/html/overview.rb +++ b/lib/skunk/generators/html/overview.rb @@ -27,7 +27,6 @@ def file_name end def render - data = @data TEMPLATE.result(binding) end end diff --git a/lib/skunk/generators/html/templates/skunk_overview.html.erb b/lib/skunk/generators/html/templates/skunk_overview.html.erb index 768db0d..9fea17e 100644 --- a/lib/skunk/generators/html/templates/skunk_overview.html.erb +++ b/lib/skunk/generators/html/templates/skunk_overview.html.erb @@ -242,15 +242,15 @@
-
<%= data.analysed_modules_count %>
+
<%= @data.analysed_modules_count %>
Modules Analysed
-
<%= data.skunk_score_total %>
+
<%= @data.skunk_score_total %>
Total Skunk Score
-
<%= data.skunk_score_average %>
+
<%= @data.skunk_score_average %>
Average Skunk Score
@@ -269,7 +269,7 @@ - <% data.files.each do |item| %> + <% @data.files.each do |item| %> <%= item.file %> @@ -301,7 +301,7 @@
-

Generated with Skunk v<%= data.skunk_version %> on <%= data.generated_at %>

+

Generated with Skunk v<%= @data.skunk_version %> on <%= @data.generated_at %>

From e59ce7ed54d050c4324c8a2d6389ea2deff84766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 17:59:58 -0600 Subject: [PATCH 8/9] Fix segmentation failure at shareable class --- .reek.yml | 3 +++ .rubocop_todo.yml | 6 +++--- lib/skunk/commands/shareable.rb | 7 +++++++ lib/skunk/commands/status_sharer.rb | 12 +++++++++++- test/lib/skunk/application_test.rb | 20 +++++++++++--------- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.reek.yml b/.reek.yml index c4f99e9..ad749c6 100644 --- a/.reek.yml +++ b/.reek.yml @@ -33,6 +33,9 @@ detectors: - 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: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9055fa2..f0b68db 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-08 22:28:33 UTC using RuboCop version 1.81.1. +# 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 @@ -36,11 +36,11 @@ Lint/MissingSuper: 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/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 ab0165c..af23d49 100644 --- a/lib/skunk/commands/status_sharer.rb +++ b/lib/skunk/commands/status_sharer.rb @@ -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/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 From de9130275819e4e7a8623bf805de3401efa6caaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Wed, 8 Oct 2025 18:06:19 -0600 Subject: [PATCH 9/9] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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)