skunk 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|