skunk 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +47 -0
- data/.github/workflows/main.yml +74 -4
- data/.github/workflows/skunk.yml +8 -3
- data/.gitignore +1 -0
- data/.rubocop_todo.yml +39 -17
- data/CHANGELOG.md +23 -16
- data/CODEOWNERS +5 -0
- data/Gemfile-Ruby-2-4 +10 -0
- data/README.md +64 -20
- data/fastruby-logo.png +0 -0
- data/lib/skunk.rb +1 -1
- data/lib/skunk/cli/application.rb +34 -4
- data/lib/skunk/cli/commands/base.rb +2 -0
- data/lib/skunk/cli/commands/compare.rb +10 -6
- data/lib/skunk/cli/commands/compare_score.rb +39 -0
- data/lib/skunk/cli/commands/default.rb +22 -1
- data/lib/skunk/cli/commands/help.rb +11 -1
- data/lib/skunk/cli/commands/status_reporter.rb +16 -13
- data/lib/skunk/cli/commands/status_sharer.rb +100 -0
- data/lib/skunk/cli/options/argv.rb +11 -0
- data/lib/skunk/rubycritic/analysed_module.rb +29 -9
- data/lib/skunk/rubycritic/analysed_modules_collection.rb +2 -2
- data/lib/skunk/version.rb +1 -1
- data/logo.png +0 -0
- data/samples/engines/spec/nested_sample_spec.rb +5 -0
- data/samples/rubycritic/analysed_module.rb +9 -9
- data/skunk.gemspec +13 -7
- metadata +100 -28
- data/.travis.yml +0 -5
- data/Gemfile.lock +0 -119
data/fastruby-logo.png
ADDED
Binary file
|
data/lib/skunk.rb
CHANGED
@@ -7,24 +7,54 @@ require "skunk"
|
|
7
7
|
require "skunk/rubycritic/analysed_module"
|
8
8
|
require "skunk/cli/options"
|
9
9
|
require "skunk/cli/command_factory"
|
10
|
+
require "skunk/cli/commands/status_sharer"
|
10
11
|
|
11
12
|
module Skunk
|
12
13
|
module Cli
|
13
14
|
# Knows how to execute command line commands
|
15
|
+
# :reek:InstanceVariableAssumption
|
14
16
|
class Application < RubyCritic::Cli::Application
|
17
|
+
COVERAGE_FILE = "coverage/.resultset.json"
|
18
|
+
|
15
19
|
def initialize(argv)
|
16
20
|
@options = Skunk::Cli::Options.new(argv)
|
17
21
|
end
|
18
22
|
|
23
|
+
# :reek:UncommunicativeVariableName
|
19
24
|
def execute
|
20
|
-
|
21
|
-
|
25
|
+
warn_coverage_info unless File.exist?(COVERAGE_FILE)
|
26
|
+
|
27
|
+
# :reek:NilCheck
|
28
|
+
@parsed_options = @options.parse.to_h
|
29
|
+
command = Skunk::Cli::CommandFactory.create(@parsed_options)
|
30
|
+
reporter = command.execute
|
31
|
+
|
22
32
|
print(reporter.status_message)
|
33
|
+
share_status_message = command.share(reporter)
|
34
|
+
print(share_status_message)
|
35
|
+
|
23
36
|
reporter.status
|
24
|
-
rescue OptionParser::InvalidOption =>
|
25
|
-
warn "Error: #{
|
37
|
+
rescue OptionParser::InvalidOption => e
|
38
|
+
warn "Error: #{e}"
|
26
39
|
STATUS_ERROR
|
27
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def warn_coverage_info
|
45
|
+
warn "warning: Couldn't find coverage info at #{COVERAGE_FILE}."
|
46
|
+
warn "warning: Having no coverage metrics will make your SkunkScore worse."
|
47
|
+
end
|
48
|
+
|
49
|
+
# :reek:NilCheck
|
50
|
+
def print(message)
|
51
|
+
filename = @parsed_options[:output_filename]
|
52
|
+
if filename.nil?
|
53
|
+
$stdout.puts(message)
|
54
|
+
else
|
55
|
+
File.open(filename, "a") { |file| file << message }
|
56
|
+
end
|
57
|
+
end
|
28
58
|
end
|
29
59
|
end
|
30
60
|
end
|
@@ -3,17 +3,18 @@
|
|
3
3
|
require "rubycritic/commands/compare"
|
4
4
|
require "skunk/rubycritic/analysed_modules_collection"
|
5
5
|
require "skunk/cli/commands/output"
|
6
|
+
require "skunk/cli/commands/compare_score"
|
6
7
|
|
7
8
|
# nodoc #
|
8
9
|
module Skunk
|
9
10
|
module Command
|
10
|
-
# Knows how to compare two branches and their
|
11
|
+
# Knows how to compare two branches and their skunk score average
|
11
12
|
class Compare < RubyCritic::Command::Compare
|
12
13
|
# switch branch and analyse files but don't generate a report
|
13
14
|
def analyse_branch(branch)
|
14
15
|
::RubyCritic::SourceControlSystem::Git.switch_branch(::RubyCritic::Config.send(branch))
|
15
16
|
critic = critique(branch)
|
16
|
-
::RubyCritic::Config.send(:"#{branch}_score=", critic.
|
17
|
+
::RubyCritic::Config.send(:"#{branch}_score=", critic.skunk_score_average)
|
17
18
|
::RubyCritic::Config.root = branch_directory(branch)
|
18
19
|
end
|
19
20
|
|
@@ -29,10 +30,13 @@ module Skunk
|
|
29
30
|
|
30
31
|
# create a txt file with the branch score details
|
31
32
|
def build_details
|
32
|
-
details =
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
details = CompareScore.new(
|
34
|
+
::RubyCritic::Config.base_branch,
|
35
|
+
::RubyCritic::Config.feature_branch,
|
36
|
+
::RubyCritic::Config.base_branch_score.to_f.round(2),
|
37
|
+
::RubyCritic::Config.feature_branch_score.to_f.round(2)
|
38
|
+
).message
|
39
|
+
|
36
40
|
Skunk::Command::Output.create_directory(::RubyCritic::Config.compare_root_directory)
|
37
41
|
File.open(build_details_path, "w") { |file| file.write(details) }
|
38
42
|
puts details
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# nodoc #
|
4
|
+
module Skunk
|
5
|
+
module Command
|
6
|
+
# Knows how to describe score evolution between two branches
|
7
|
+
class CompareScore
|
8
|
+
def initialize(base_branch, feature_branch, base_branch_score, feature_branch_score)
|
9
|
+
@base_branch = base_branch
|
10
|
+
@feature_branch = feature_branch
|
11
|
+
@base_branch_score = base_branch_score
|
12
|
+
@feature_branch_score = feature_branch_score
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
"Base branch (#{@base_branch}) "\
|
17
|
+
"average skunk score: #{@base_branch_score} \n"\
|
18
|
+
"Feature branch (#{@feature_branch}) "\
|
19
|
+
"average skunk score: #{@feature_branch_score} \n"\
|
20
|
+
"#{score_evolution_message}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def score_evolution_message
|
24
|
+
"Skunk score average is #{score_evolution} #{score_evolution_appreciation} \n"
|
25
|
+
end
|
26
|
+
|
27
|
+
def score_evolution_appreciation
|
28
|
+
@feature_branch_score > @base_branch_score ? "worse" : "better"
|
29
|
+
end
|
30
|
+
|
31
|
+
def score_evolution
|
32
|
+
return "Infinitely" if @base_branch_score.zero?
|
33
|
+
|
34
|
+
precentage = (100 * (@base_branch_score - @feature_branch_score) / @base_branch_score)
|
35
|
+
"#{precentage.round(0).abs}%"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -16,20 +16,41 @@ module Skunk
|
|
16
16
|
class Default < RubyCritic::Command::Default
|
17
17
|
def initialize(options)
|
18
18
|
super
|
19
|
-
@
|
19
|
+
@options = options
|
20
|
+
@status_reporter = Skunk::Command::StatusReporter.new(options)
|
20
21
|
end
|
21
22
|
|
23
|
+
# It generates a report and it returns an instance of
|
24
|
+
# Skunk::Command::StatusReporter
|
25
|
+
#
|
26
|
+
# @return [Skunk::Command::StatusReporter]
|
22
27
|
def execute
|
23
28
|
RubyCritic::Config.formats = []
|
24
29
|
|
25
30
|
report(critique)
|
31
|
+
|
26
32
|
status_reporter
|
27
33
|
end
|
28
34
|
|
35
|
+
# It connects the Skunk::Command::StatusReporter with the collection
|
36
|
+
# of analysed modules.
|
37
|
+
#
|
38
|
+
# @param [RubyCritic::AnalysedModulesCollection] A collection of analysed modules
|
29
39
|
def report(analysed_modules)
|
30
40
|
status_reporter.analysed_modules = analysed_modules
|
31
41
|
status_reporter.score = analysed_modules.score
|
32
42
|
end
|
43
|
+
|
44
|
+
# It shares the report using SHARE_URL or https://skunk.fastruby.io. It
|
45
|
+
# will post all results in JSON format and return a status message.
|
46
|
+
#
|
47
|
+
# @param [Skunk::Command::StatusReporter] A status reporter with analysed modules
|
48
|
+
# :reek:FeatureEnvy
|
49
|
+
def share(reporter)
|
50
|
+
sharer = Skunk::Command::StatusSharer.new(@options)
|
51
|
+
sharer.status_reporter = reporter
|
52
|
+
sharer.share
|
53
|
+
end
|
33
54
|
end
|
34
55
|
end
|
35
56
|
end
|
@@ -6,7 +6,17 @@ require "rubycritic/commands/help"
|
|
6
6
|
module Skunk
|
7
7
|
module Cli
|
8
8
|
module Command
|
9
|
-
|
9
|
+
# Knows how to guide user into using `skunk` properly
|
10
|
+
class Help < Skunk::Cli::Command::Base
|
11
|
+
# Outputs a help message
|
12
|
+
def execute
|
13
|
+
puts options[:help_text]
|
14
|
+
status_reporter
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :options, :status_reporter
|
10
20
|
end
|
11
21
|
end
|
12
22
|
end
|
@@ -10,21 +10,23 @@ module Skunk
|
|
10
10
|
class StatusReporter < RubyCritic::Command::StatusReporter
|
11
11
|
attr_accessor :analysed_modules
|
12
12
|
|
13
|
-
HEADINGS = %w[file
|
13
|
+
HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze
|
14
14
|
HEADINGS_WITHOUT_FILE = HEADINGS - %w[file]
|
15
15
|
HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding
|
16
16
|
|
17
17
|
TEMPLATE = ERB.new(<<-TEMPL
|
18
18
|
<%= _ttable %>\n
|
19
|
-
|
19
|
+
SkunkScore Total: <%= total_skunk_score %>
|
20
20
|
Modules Analysed: <%= analysed_modules_count %>
|
21
|
-
|
22
|
-
<% if worst %>Worst
|
21
|
+
SkunkScore Average: <%= skunk_score_average %>
|
22
|
+
<% if worst %>Worst SkunkScore: <%= worst.skunk_score %> (<%= worst.pathname %>)<% end %>
|
23
|
+
|
24
|
+
Generated with Skunk v<%= Skunk::VERSION %>
|
23
25
|
TEMPL
|
24
26
|
)
|
25
27
|
|
26
28
|
# Returns a status message with a table of all analysed_modules and
|
27
|
-
# a
|
29
|
+
# a skunk score average
|
28
30
|
def update_status_message
|
29
31
|
opts = table_options.merge(headings: HEADINGS, rows: table)
|
30
32
|
|
@@ -41,7 +43,8 @@ TEMPL
|
|
41
43
|
|
42
44
|
def non_test_modules
|
43
45
|
@non_test_modules ||= analysed_modules.reject do |a_module|
|
44
|
-
a_module.pathname.to_s
|
46
|
+
module_path = a_module.pathname.dirname.to_s
|
47
|
+
module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
@@ -50,21 +53,21 @@ TEMPL
|
|
50
53
|
end
|
51
54
|
|
52
55
|
def sorted_modules
|
53
|
-
@sorted_modules ||= non_test_modules.sort_by(&:
|
56
|
+
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
|
54
57
|
end
|
55
58
|
|
56
|
-
def
|
57
|
-
@
|
59
|
+
def total_skunk_score
|
60
|
+
@total_skunk_score ||= non_test_modules.sum(&:skunk_score)
|
58
61
|
end
|
59
62
|
|
60
63
|
def total_churn_times_cost
|
61
|
-
non_test_modules.
|
64
|
+
non_test_modules.sum(&:churn_times_cost)
|
62
65
|
end
|
63
66
|
|
64
|
-
def
|
67
|
+
def skunk_score_average
|
65
68
|
return 0 if analysed_modules_count.zero?
|
66
69
|
|
67
|
-
(
|
70
|
+
(total_skunk_score.to_d / analysed_modules_count).to_f.round(2)
|
68
71
|
end
|
69
72
|
|
70
73
|
def table_options
|
@@ -81,7 +84,7 @@ TEMPL
|
|
81
84
|
sorted_modules.map do |a_mod|
|
82
85
|
[
|
83
86
|
a_mod.pathname,
|
84
|
-
a_mod.
|
87
|
+
a_mod.skunk_score,
|
85
88
|
a_mod.churn_times_cost,
|
86
89
|
a_mod.churn,
|
87
90
|
a_mod.cost.round(2),
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "net/https"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
require "skunk/cli/commands/status_reporter"
|
8
|
+
|
9
|
+
module Skunk
|
10
|
+
module Command
|
11
|
+
# Knows how to share status to an API
|
12
|
+
class StatusSharer < Skunk::Command::StatusReporter
|
13
|
+
attr_reader :status_message
|
14
|
+
|
15
|
+
DEFAULT_URL = "https://skunk.fastruby.io"
|
16
|
+
def status_reporter=(status_reporter)
|
17
|
+
self.analysed_modules = status_reporter.analysed_modules
|
18
|
+
self.score = analysed_modules.score
|
19
|
+
end
|
20
|
+
|
21
|
+
def share
|
22
|
+
return "" if not_sharing?
|
23
|
+
|
24
|
+
response = post_payload
|
25
|
+
|
26
|
+
@status_message =
|
27
|
+
if Net::HTTPOK === response
|
28
|
+
data = JSON.parse response.body
|
29
|
+
"Shared at: #{File.join(base_url, data['id'])}"
|
30
|
+
else
|
31
|
+
"Error sharing report: #{response}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# :reek:UtilityFunction
|
38
|
+
def base_url
|
39
|
+
ENV["SHARE_URL"] || DEFAULT_URL
|
40
|
+
end
|
41
|
+
|
42
|
+
def json_summary
|
43
|
+
result = {
|
44
|
+
total_skunk_score: total_skunk_score,
|
45
|
+
analysed_modules_count: analysed_modules_count,
|
46
|
+
skunk_score_average: skunk_score_average,
|
47
|
+
skunk_version: Skunk::VERSION
|
48
|
+
}
|
49
|
+
|
50
|
+
if worst
|
51
|
+
result[:worst_skunk_score] = {
|
52
|
+
file: worst.pathname.to_s,
|
53
|
+
skunk_score: worst.skunk_score
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
def json_results
|
61
|
+
sorted_modules.map(&:to_hash)
|
62
|
+
end
|
63
|
+
|
64
|
+
# :reek:UtilityFunction
|
65
|
+
def not_sharing?
|
66
|
+
ENV["SHARE"] != "true" && ENV["SHARE_URL"].to_s == ""
|
67
|
+
end
|
68
|
+
|
69
|
+
def payload
|
70
|
+
JSON.generate(
|
71
|
+
"entries" => json_results,
|
72
|
+
"summary" => json_summary,
|
73
|
+
"options" => {
|
74
|
+
"compare" => "false"
|
75
|
+
}
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
# :reek:TooManyStatements
|
80
|
+
def post_payload
|
81
|
+
req = Net::HTTP::Post.new(url)
|
82
|
+
req.body = payload
|
83
|
+
|
84
|
+
http = Net::HTTP.new(url.hostname, url.port)
|
85
|
+
if url.scheme == "https"
|
86
|
+
http.use_ssl = true
|
87
|
+
http.ssl_version = :TLSv1_2
|
88
|
+
end
|
89
|
+
|
90
|
+
http.start do |connection|
|
91
|
+
connection.request req
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def url
|
96
|
+
URI(File.join(base_url, "reports"))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -9,6 +9,9 @@ module Skunk
|
|
9
9
|
# Extends RubyCritic::Cli::Options::Argv to parse a subset of the
|
10
10
|
# parameters accepted by RubyCritic
|
11
11
|
class Argv < RubyCritic::Cli::Options::Argv
|
12
|
+
# :reek:Attribute
|
13
|
+
attr_accessor :output_filename
|
14
|
+
|
12
15
|
def parse # rubocop:disable Metrics/MethodLength
|
13
16
|
parser.new do |opts|
|
14
17
|
opts.banner = "Usage: skunk [options] [paths]\n"
|
@@ -19,6 +22,10 @@ module Skunk
|
|
19
22
|
self.mode = :compare_branches
|
20
23
|
end
|
21
24
|
|
25
|
+
opts.on("-o", "--out FILE", "Output report to file") do |filename|
|
26
|
+
self.output_filename = filename
|
27
|
+
end
|
28
|
+
|
22
29
|
opts.on_tail("-v", "--version", "Show gem's version") do
|
23
30
|
self.mode = :version
|
24
31
|
end
|
@@ -28,6 +35,10 @@ module Skunk
|
|
28
35
|
end
|
29
36
|
end.parse!(@argv)
|
30
37
|
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
super.merge(output_filename: output_filename)
|
41
|
+
end
|
31
42
|
end
|
32
43
|
end
|
33
44
|
end
|
@@ -3,34 +3,34 @@
|
|
3
3
|
require "rubycritic/core/analysed_module"
|
4
4
|
|
5
5
|
module RubyCritic
|
6
|
-
# Monkey-patches RubyCritic::AnalysedModule to add a
|
6
|
+
# Monkey-patches RubyCritic::AnalysedModule to add a skunk_score method
|
7
7
|
class AnalysedModule
|
8
8
|
PERFECT_COVERAGE = 100
|
9
9
|
|
10
|
-
# Returns a numeric value that represents the
|
10
|
+
# Returns a numeric value that represents the skunk_score of a module:
|
11
11
|
#
|
12
|
-
# If module is perfectly covered,
|
12
|
+
# If module is perfectly covered, skunk score is the same as the
|
13
13
|
# `churn_times_cost`
|
14
14
|
#
|
15
|
-
# If module has no coverage,
|
15
|
+
# If module has no coverage, skunk score is a penalized value of
|
16
16
|
# `churn_times_cost`
|
17
17
|
#
|
18
|
-
# For now the
|
18
|
+
# For now the skunk_score is calculated by multiplying `churn_times_cost`
|
19
19
|
# times the lack of coverage.
|
20
20
|
#
|
21
21
|
# For example:
|
22
22
|
#
|
23
23
|
# When `churn_times_cost` is 100 and module is perfectly covered:
|
24
|
-
#
|
24
|
+
# skunk_score => 100
|
25
25
|
#
|
26
26
|
# When `churn_times_cost` is 100 and module is not covered at all:
|
27
|
-
#
|
27
|
+
# skunk_score => 100 * 100 = 10_000
|
28
28
|
#
|
29
29
|
# When `churn_times_cost` is 100 and module is covered at 75%:
|
30
|
-
#
|
30
|
+
# skunk_score => 100 * 25 (percentage uncovered) = 2_500
|
31
31
|
#
|
32
32
|
# @return [Float]
|
33
|
-
def
|
33
|
+
def skunk_score
|
34
34
|
return cost.round(2) if coverage == PERFECT_COVERAGE
|
35
35
|
|
36
36
|
(cost * (PERFECT_COVERAGE - coverage.to_i)).round(2)
|
@@ -43,5 +43,25 @@ module RubyCritic
|
|
43
43
|
safe_churn = churn.positive? ? churn : 1
|
44
44
|
@churn_times_cost ||= (safe_churn * cost).round(2)
|
45
45
|
end
|
46
|
+
|
47
|
+
# Returns a hash with these attributes:
|
48
|
+
# - file
|
49
|
+
# - skunk_score
|
50
|
+
# - churn_times_cost
|
51
|
+
# - churn
|
52
|
+
# - cost
|
53
|
+
# - coverage
|
54
|
+
#
|
55
|
+
# @return [Hash]
|
56
|
+
def to_hash
|
57
|
+
{
|
58
|
+
file: pathname.to_s,
|
59
|
+
skunk_score: skunk_score,
|
60
|
+
churn_times_cost: churn_times_cost,
|
61
|
+
churn: churn,
|
62
|
+
cost: cost.round(2),
|
63
|
+
coverage: coverage.round(2)
|
64
|
+
}
|
65
|
+
end
|
46
66
|
end
|
47
67
|
end
|