skunk 0.3.2 → 0.5.1
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 +89 -0
- data/.github/workflows/skunk.yml +29 -0
- data/.gitignore +1 -0
- data/.reek.yml +2 -7
- data/.rubocop_todo.yml +41 -18
- data/CHANGELOG.md +46 -6
- data/CODEOWNERS +5 -0
- data/Gemfile-Ruby-2-4 +10 -0
- data/README.md +78 -25
- 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 +52 -30
- data/lib/skunk/cli/commands/compare_score.rb +41 -0
- data/lib/skunk/cli/commands/default.rb +14 -1
- data/lib/skunk/cli/commands/help.rb +11 -1
- data/lib/skunk/cli/commands/output.rb +12 -0
- data/lib/skunk/cli/commands/shareable.rb +21 -0
- data/lib/skunk/cli/commands/status_reporter.rb +23 -16
- data/lib/skunk/cli/commands/status_sharer.rb +99 -0
- data/lib/skunk/cli/commands/version.rb +16 -0
- data/lib/skunk/cli/options/argv.rb +11 -7
- data/lib/skunk/rubycritic/analysed_module.rb +30 -10
- 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 +14 -8
- metadata +105 -30
- data/.travis.yml +0 -5
- data/Gemfile.lock +0 -119
- data/bin/console +0 -16
- data/bin/setup +0 -8
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
|
@@ -2,42 +2,64 @@
|
|
2
2
|
|
3
3
|
require "rubycritic/commands/compare"
|
4
4
|
require "skunk/rubycritic/analysed_modules_collection"
|
5
|
+
require "skunk/cli/commands/output"
|
6
|
+
require "skunk/cli/commands/shareable"
|
7
|
+
require "skunk/cli/commands/compare_score"
|
5
8
|
|
6
9
|
# nodoc #
|
7
10
|
module Skunk
|
8
|
-
module
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
::RubyCritic::SourceControlSystem::Git.switch_branch(::RubyCritic::Config.send(branch))
|
14
|
-
critic = critique(branch)
|
15
|
-
::RubyCritic::Config.send(:"#{branch}_score=", critic.stink_score_average)
|
16
|
-
::RubyCritic::Config.root = branch_directory(branch)
|
17
|
-
end
|
11
|
+
module Cli
|
12
|
+
module Command
|
13
|
+
# Knows how to compare two branches and their skunk score average
|
14
|
+
class Compare < RubyCritic::Command::Compare
|
15
|
+
include Skunk::Cli::Command::Shareable
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
::RubyCritic::AnalysedModulesCollection.new(modified_files.map(&:path),
|
25
|
-
modified_files)
|
26
|
-
::RubyCritic::Config.root = "#{::RubyCritic::Config.root}/compare"
|
27
|
-
end
|
17
|
+
def initialize(options)
|
18
|
+
super
|
19
|
+
@options = options
|
20
|
+
@status_reporter = Skunk::Command::StatusReporter.new(options)
|
21
|
+
end
|
28
22
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
23
|
+
def execute
|
24
|
+
compare_branches
|
25
|
+
status_reporter
|
26
|
+
end
|
27
|
+
|
28
|
+
# switch branch and analyse files but don't generate a report
|
29
|
+
def analyse_branch(branch)
|
30
|
+
::RubyCritic::SourceControlSystem::Git.switch_branch(::RubyCritic::Config.send(branch))
|
31
|
+
critic = critique(branch)
|
32
|
+
::RubyCritic::Config.send(:"#{branch}_score=", critic.skunk_score_average)
|
33
|
+
::RubyCritic::Config.root = branch_directory(branch)
|
34
|
+
end
|
35
|
+
|
36
|
+
# generate report only for modified files but don't report it
|
37
|
+
def analyse_modified_files
|
38
|
+
modified_files = ::RubyCritic::Config
|
39
|
+
.feature_branch_collection
|
40
|
+
.where(::RubyCritic::SourceControlSystem::Git.modified_files)
|
41
|
+
::RubyCritic::AnalysedModulesCollection.new(modified_files.map(&:path),
|
42
|
+
modified_files)
|
43
|
+
::RubyCritic::Config.root = "#{::RubyCritic::Config.root}/compare"
|
44
|
+
end
|
45
|
+
|
46
|
+
# create a txt file with the branch score details
|
47
|
+
def build_details
|
48
|
+
details = CompareScore.new(
|
49
|
+
::RubyCritic::Config.base_branch,
|
50
|
+
::RubyCritic::Config.feature_branch,
|
51
|
+
::RubyCritic::Config.base_branch_score.to_f.round(2),
|
52
|
+
::RubyCritic::Config.feature_branch_score.to_f.round(2)
|
53
|
+
).message
|
54
|
+
|
55
|
+
Skunk::Command::Output.create_directory(::RubyCritic::Config.compare_root_directory)
|
56
|
+
File.open(build_details_path, "w") { |file| file.write(details) }
|
57
|
+
puts details
|
58
|
+
end
|
38
59
|
|
39
|
-
|
40
|
-
|
60
|
+
def build_details_path
|
61
|
+
"#{::RubyCritic::Config.compare_root_directory}/build_details.txt"
|
62
|
+
end
|
41
63
|
end
|
42
64
|
end
|
43
65
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# nodoc #
|
4
|
+
module Skunk
|
5
|
+
module Cli
|
6
|
+
module Command
|
7
|
+
# Knows how to describe score evolution between two branches
|
8
|
+
class CompareScore
|
9
|
+
def initialize(base_branch, feature_branch, base_branch_score, feature_branch_score)
|
10
|
+
@base_branch = base_branch
|
11
|
+
@feature_branch = feature_branch
|
12
|
+
@base_branch_score = base_branch_score
|
13
|
+
@feature_branch_score = feature_branch_score
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"Base branch (#{@base_branch}) "\
|
18
|
+
"average skunk score: #{@base_branch_score} \n"\
|
19
|
+
"Feature branch (#{@feature_branch}) "\
|
20
|
+
"average skunk score: #{@feature_branch_score} \n"\
|
21
|
+
"#{score_evolution_message}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def score_evolution_message
|
25
|
+
"Skunk score average is #{score_evolution} #{score_evolution_appreciation} \n"
|
26
|
+
end
|
27
|
+
|
28
|
+
def score_evolution_appreciation
|
29
|
+
@feature_branch_score > @base_branch_score ? "worse" : "better"
|
30
|
+
end
|
31
|
+
|
32
|
+
def score_evolution
|
33
|
+
return "Infinitely" if @base_branch_score.zero?
|
34
|
+
|
35
|
+
precentage = (100 * (@base_branch_score - @feature_branch_score) / @base_branch_score)
|
36
|
+
"#{precentage.round(0).abs}%"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -6,6 +6,7 @@ require "rubycritic/revision_comparator"
|
|
6
6
|
require "rubycritic/reporter"
|
7
7
|
|
8
8
|
require "skunk/cli/commands/base"
|
9
|
+
require "skunk/cli/commands/shareable"
|
9
10
|
require "skunk/cli/commands/status_reporter"
|
10
11
|
|
11
12
|
module Skunk
|
@@ -14,18 +15,30 @@ module Skunk
|
|
14
15
|
# Default command runs a critique using RubyCritic and uses
|
15
16
|
# Skunk::Command::StatusReporter to report status
|
16
17
|
class Default < RubyCritic::Command::Default
|
18
|
+
include Skunk::Cli::Command::Shareable
|
19
|
+
|
17
20
|
def initialize(options)
|
18
21
|
super
|
19
|
-
@
|
22
|
+
@options = options
|
23
|
+
@status_reporter = Skunk::Command::StatusReporter.new(options)
|
20
24
|
end
|
21
25
|
|
26
|
+
# It generates a report and it returns an instance of
|
27
|
+
# Skunk::Command::StatusReporter
|
28
|
+
#
|
29
|
+
# @return [Skunk::Command::StatusReporter]
|
22
30
|
def execute
|
23
31
|
RubyCritic::Config.formats = []
|
24
32
|
|
25
33
|
report(critique)
|
34
|
+
|
26
35
|
status_reporter
|
27
36
|
end
|
28
37
|
|
38
|
+
# It connects the Skunk::Command::StatusReporter with the collection
|
39
|
+
# of analysed modules.
|
40
|
+
#
|
41
|
+
# @param [RubyCritic::AnalysedModulesCollection] A collection of analysed modules
|
29
42
|
def report(analysed_modules)
|
30
43
|
status_reporter.analysed_modules = analysed_modules
|
31
44
|
status_reporter.score = analysed_modules.score
|
@@ -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
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skunk
|
4
|
+
module Command
|
5
|
+
# Implements the needed methods for a successful compare output
|
6
|
+
class Output
|
7
|
+
def self.create_directory(directory)
|
8
|
+
FileUtils.mkdir_p(directory) unless File.exist?(directory)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skunk
|
4
|
+
module Cli
|
5
|
+
module Command
|
6
|
+
# This is a module that will be used for sharing reports to a server
|
7
|
+
module Shareable
|
8
|
+
# It shares the report using SHARE_URL or https://skunk.fastruby.io. It
|
9
|
+
# will post all results in JSON format and return a status message.
|
10
|
+
#
|
11
|
+
# @param [Skunk::Command::StatusReporter] A status reporter with analysed modules
|
12
|
+
# :reek:FeatureEnvy
|
13
|
+
def share(reporter)
|
14
|
+
sharer = Skunk::Command::StatusSharer.new(@options)
|
15
|
+
sharer.status_reporter = reporter
|
16
|
+
sharer.share
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -10,19 +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
|
+
HEADINGS_WITHOUT_FILE = HEADINGS - %w[file]
|
15
|
+
HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding
|
14
16
|
|
15
17
|
TEMPLATE = ERB.new(<<-TEMPL
|
16
18
|
<%= _ttable %>\n
|
17
|
-
|
19
|
+
SkunkScore Total: <%= total_skunk_score %>
|
18
20
|
Modules Analysed: <%= analysed_modules_count %>
|
19
|
-
|
20
|
-
<% 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 %>
|
21
25
|
TEMPL
|
22
26
|
)
|
23
27
|
|
24
28
|
# Returns a status message with a table of all analysed_modules and
|
25
|
-
# a
|
29
|
+
# a skunk score average
|
26
30
|
def update_status_message
|
27
31
|
opts = table_options.merge(headings: HEADINGS, rows: table)
|
28
32
|
|
@@ -39,7 +43,8 @@ TEMPL
|
|
39
43
|
|
40
44
|
def non_test_modules
|
41
45
|
@non_test_modules ||= analysed_modules.reject do |a_module|
|
42
|
-
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")
|
43
48
|
end
|
44
49
|
end
|
45
50
|
|
@@ -48,27 +53,29 @@ TEMPL
|
|
48
53
|
end
|
49
54
|
|
50
55
|
def sorted_modules
|
51
|
-
@sorted_modules ||= non_test_modules.sort_by(&:
|
56
|
+
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
|
52
57
|
end
|
53
58
|
|
54
|
-
def
|
55
|
-
@
|
59
|
+
def total_skunk_score
|
60
|
+
@total_skunk_score ||= non_test_modules.sum(&:skunk_score)
|
56
61
|
end
|
57
62
|
|
58
63
|
def total_churn_times_cost
|
59
|
-
non_test_modules.
|
64
|
+
non_test_modules.sum(&:churn_times_cost)
|
60
65
|
end
|
61
66
|
|
62
|
-
def
|
67
|
+
def skunk_score_average
|
63
68
|
return 0 if analysed_modules_count.zero?
|
64
69
|
|
65
|
-
(
|
70
|
+
(total_skunk_score.to_d / analysed_modules_count).to_f.round(2)
|
66
71
|
end
|
67
72
|
|
68
73
|
def table_options
|
74
|
+
max = sorted_modules.max_by { |a_mod| a_mod.pathname.to_s.length }
|
75
|
+
width = max.pathname.to_s.length + HEADINGS_WITHOUT_FILE_WIDTH
|
69
76
|
{
|
70
77
|
style: {
|
71
|
-
width:
|
78
|
+
width: width
|
72
79
|
}
|
73
80
|
}
|
74
81
|
end
|
@@ -77,11 +84,11 @@ TEMPL
|
|
77
84
|
sorted_modules.map do |a_mod|
|
78
85
|
[
|
79
86
|
a_mod.pathname,
|
80
|
-
a_mod.
|
87
|
+
a_mod.skunk_score,
|
81
88
|
a_mod.churn_times_cost,
|
82
89
|
a_mod.churn,
|
83
|
-
a_mod.cost,
|
84
|
-
a_mod.coverage
|
90
|
+
a_mod.cost.round(2),
|
91
|
+
a_mod.coverage.round(2)
|
85
92
|
]
|
86
93
|
end
|
87
94
|
end
|
@@ -0,0 +1,99 @@
|
|
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
|
+
end
|
19
|
+
|
20
|
+
def share
|
21
|
+
return "" if not_sharing?
|
22
|
+
|
23
|
+
response = post_payload
|
24
|
+
|
25
|
+
@status_message =
|
26
|
+
if Net::HTTPOK === response
|
27
|
+
data = JSON.parse response.body
|
28
|
+
"Shared at: #{File.join(base_url, data['id'])}"
|
29
|
+
else
|
30
|
+
"Error sharing report: #{response}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# :reek:UtilityFunction
|
37
|
+
def base_url
|
38
|
+
ENV["SHARE_URL"] || DEFAULT_URL
|
39
|
+
end
|
40
|
+
|
41
|
+
def json_summary
|
42
|
+
result = {
|
43
|
+
total_skunk_score: total_skunk_score,
|
44
|
+
analysed_modules_count: analysed_modules_count,
|
45
|
+
skunk_score_average: skunk_score_average,
|
46
|
+
skunk_version: Skunk::VERSION
|
47
|
+
}
|
48
|
+
|
49
|
+
if worst
|
50
|
+
result[:worst_skunk_score] = {
|
51
|
+
file: worst.pathname.to_s,
|
52
|
+
skunk_score: worst.skunk_score
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def json_results
|
60
|
+
sorted_modules.map(&:to_hash)
|
61
|
+
end
|
62
|
+
|
63
|
+
# :reek:UtilityFunction
|
64
|
+
def not_sharing?
|
65
|
+
ENV["SHARE"] != "true" && ENV["SHARE_URL"].to_s == ""
|
66
|
+
end
|
67
|
+
|
68
|
+
def payload
|
69
|
+
JSON.generate(
|
70
|
+
"entries" => json_results,
|
71
|
+
"summary" => json_summary,
|
72
|
+
"options" => {
|
73
|
+
"compare" => "false"
|
74
|
+
}
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# :reek:TooManyStatements
|
79
|
+
def post_payload
|
80
|
+
req = Net::HTTP::Post.new(url)
|
81
|
+
req.body = payload
|
82
|
+
|
83
|
+
http = Net::HTTP.new(url.hostname, url.port)
|
84
|
+
if url.scheme == "https"
|
85
|
+
http.use_ssl = true
|
86
|
+
http.ssl_version = :TLSv1_2
|
87
|
+
end
|
88
|
+
|
89
|
+
http.start do |connection|
|
90
|
+
connection.request req
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def url
|
95
|
+
URI(File.join(base_url, "reports"))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|