Skip to content

Commit 3f9ae8c

Browse files
committed
Add analysis class
1 parent 5efd555 commit 3f9ae8c

File tree

5 files changed

+392
-74
lines changed

5 files changed

+392
-74
lines changed

lib/skunk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "skunk/version"
4+
require "skunk/analysis"
45

56
# Knows how to calculate the `SkunkScore` for each file analyzed by `RubyCritic`
67
# and `SimpleCov`

lib/skunk/analysis.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
module Skunk
4+
# Centralized service for analyzing Skunk metrics from analysed modules.
5+
# This class encapsulates all the core business logic for calculating
6+
# Skunk scores, filtering test modules, and providing aggregated statistics.
7+
#
8+
# @example
9+
# analysis = Skunk::Analysis.new(analysed_modules)
10+
# puts "Total Skunk Score: #{analysis.skunk_score_total}"
11+
# puts "Average: #{analysis.skunk_score_average}"
12+
# puts "Worst module: #{analysis.worst_module.pathname}"
13+
class Analysis
14+
attr_reader :analysed_modules
15+
16+
# @param analysed_modules [RubyCritic::AnalysedModulesCollection] Collection of analysed modules
17+
def initialize(analysed_modules)
18+
@analysed_modules = analysed_modules
19+
end
20+
21+
# Returns the count of non-test modules
22+
# @return [Integer]
23+
def analysed_modules_count
24+
@analysed_modules_count ||= non_test_modules.count
25+
end
26+
27+
# Returns the total Skunk score across all non-test modules
28+
# @return [Float]
29+
def skunk_score_total
30+
@skunk_score_total ||= non_test_modules.sum(&:skunk_score)
31+
end
32+
33+
# Returns the average Skunk score across all non-test modules
34+
# @return [Float]
35+
def skunk_score_average
36+
return 0.0 if analysed_modules_count.zero?
37+
38+
(skunk_score_total.to_d / analysed_modules_count).to_f.round(2)
39+
end
40+
41+
# Returns the total churn times cost across all non-test modules
42+
# @return [Float]
43+
def total_churn_times_cost
44+
@total_churn_times_cost ||= non_test_modules.sum(&:churn_times_cost)
45+
end
46+
47+
# Returns the module with the highest Skunk score (worst performing)
48+
# @return [RubyCritic::AnalysedModule, nil]
49+
def worst_module
50+
@worst_module ||= sorted_modules.first
51+
end
52+
53+
# Returns modules sorted by Skunk score in descending order (worst first)
54+
# @return [Array<RubyCritic::AnalysedModule>]
55+
def sorted_modules
56+
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
57+
end
58+
59+
# Returns only non-test modules (excludes test and spec directories)
60+
# @return [Array<RubyCritic::AnalysedModule>]
61+
def non_test_modules
62+
@non_test_modules ||= analysed_modules.reject do |a_module|
63+
test_module?(a_module)
64+
end
65+
end
66+
67+
# Returns a hash representation of the analysis results
68+
# @return [Hash]
69+
def to_hash
70+
{
71+
analysed_modules_count: analysed_modules_count,
72+
skunk_score_total: skunk_score_total,
73+
skunk_score_average: skunk_score_average,
74+
total_churn_times_cost: total_churn_times_cost,
75+
worst_pathname: worst_module&.pathname,
76+
worst_score: worst_module&.skunk_score,
77+
files: files_as_hash
78+
}
79+
end
80+
81+
private
82+
83+
# Returns files as an array of hashes (for JSON serialization)
84+
# @return [Array<Hash>]
85+
def files_as_hash
86+
@files_as_hash ||= sorted_modules.map(&:to_hash)
87+
end
88+
89+
# Determines if a module is a test module based on its path
90+
# @param a_module [RubyCritic::AnalysedModule] The module to check
91+
# @return [Boolean]
92+
def test_module?(a_module)
93+
pathname = a_module.pathname
94+
module_path = pathname.dirname.to_s
95+
filename = pathname.basename.to_s
96+
97+
# Check if directory starts or ends with test/spec
98+
directory_is_test = module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
99+
100+
# Check if filename ends with _test.rb or _spec.rb
101+
filename_is_test = filename.end_with?("_test.rb", "_spec.rb")
102+
103+
directory_is_test || filename_is_test
104+
end
105+
end
106+
end

lib/skunk/commands/status_reporter.rb

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

3-
require "erb"
43
require "rubycritic/commands/status_reporter"
5-
require "terminal-table"
64

75
module Skunk
86
module Command
@@ -14,78 +12,8 @@ def initialize(options = {})
1412
super(options)
1513
end
1614

17-
HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze
18-
HEADINGS_WITHOUT_FILE = HEADINGS - %w[file]
19-
HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding
20-
21-
TEMPLATE = ERB.new(<<-TEMPL
22-
<%= _ttable %>\n
23-
SkunkScore Total: <%= total_skunk_score %>
24-
Modules Analysed: <%= analysed_modules_count %>
25-
SkunkScore Average: <%= skunk_score_average %>
26-
<% if worst %>Worst SkunkScore: <%= worst.skunk_score %> (<%= worst.pathname %>)<% end %>
27-
28-
Generated with Skunk v<%= Skunk::VERSION %>
29-
TEMPL
30-
)
31-
32-
# Returns a status message with a table of all analysed_modules and
33-
# a skunk score average
3415
def update_status_message
35-
opts = table_options.merge(headings: HEADINGS, rows: table)
36-
37-
_ttable = Terminal::Table.new(opts)
38-
39-
@status_message = TEMPLATE.result(binding)
40-
end
41-
42-
private
43-
44-
def analysed_modules_count
45-
analysed_modules.analysed_modules_count
46-
end
47-
48-
def worst
49-
analysed_modules.worst_module
50-
end
51-
52-
def sorted_modules
53-
analysed_modules.sorted_modules
54-
end
55-
56-
def total_skunk_score
57-
analysed_modules.skunk_score_total
58-
end
59-
60-
def total_churn_times_cost
61-
analysed_modules.total_churn_times_cost
62-
end
63-
64-
def skunk_score_average
65-
analysed_modules.skunk_score_average
66-
end
67-
68-
def table_options
69-
max = sorted_modules.max_by { |a_mod| a_mod.pathname.to_s.length }
70-
width = max.pathname.to_s.length + HEADINGS_WITHOUT_FILE_WIDTH
71-
{
72-
style: {
73-
width: width
74-
}
75-
}
76-
end
77-
78-
def table
79-
sorted_modules.map do |a_mod|
80-
[
81-
a_mod.pathname,
82-
a_mod.skunk_score,
83-
a_mod.churn_times_cost,
84-
a_mod.churn,
85-
a_mod.cost.round(2),
86-
a_mod.coverage.round(2)
87-
]
88-
end
16+
@status_message = "Skunk Report Completed"
8917
end
9018
end
9119
end

0 commit comments

Comments
 (0)