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.
data/fastruby-logo.png ADDED
Binary file
data/lib/skunk.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "skunk/version"
4
4
 
5
- # Knows how to calculate the `StinkScore` for each file analyzed by `RubyCritic`
5
+ # Knows how to calculate the `SkunkScore` for each file analyzed by `RubyCritic`
6
6
  # and `SimpleCov`
7
7
  module Skunk
8
8
  end
@@ -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
- parsed_options = @options.parse.to_h
21
- reporter = Skunk::Cli::CommandFactory.create(parsed_options).execute
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 => error
25
- warn "Error: #{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
@@ -14,6 +14,8 @@ module Skunk
14
14
  @options = options
15
15
  @status_reporter = Skunk::Command::StatusReporter.new(@options)
16
16
  end
17
+
18
+ def share(_); end
17
19
  end
18
20
  end
19
21
  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 stink score average
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.stink_score_average)
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 = "Base branch (#{::RubyCritic::Config.base_branch}) "\
33
- "average stink score: #{::RubyCritic::Config.base_branch_score} \n"\
34
- "Feature branch (#{::RubyCritic::Config.feature_branch}) "\
35
- "average stink score: #{::RubyCritic::Config.feature_branch_score} \n"
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
- @status_reporter = Skunk::Command::StatusReporter.new(@options)
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
- class Help < RubyCritic::Command::Help
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 stink_score churn_times_cost churn cost coverage].freeze
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
- StinkScore Total: <%= total_stink_score %>
19
+ SkunkScore Total: <%= total_skunk_score %>
20
20
  Modules Analysed: <%= analysed_modules_count %>
21
- StinkScore Average: <%= stink_score_average %>
22
- <% if worst %>Worst StinkScore: <%= worst.stink_score %> (<%= worst.pathname %>)<% end %>
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 stink score average
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.start_with?("test", "spec")
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(&:stink_score).reverse!
56
+ @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
54
57
  end
55
58
 
56
- def total_stink_score
57
- @total_stink_score ||= non_test_modules.map(&:stink_score).inject(0.0, :+)
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.map(&:churn_times_cost).sum
64
+ non_test_modules.sum(&:churn_times_cost)
62
65
  end
63
66
 
64
- def stink_score_average
67
+ def skunk_score_average
65
68
  return 0 if analysed_modules_count.zero?
66
69
 
67
- (total_stink_score.to_d / analysed_modules_count).to_f
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.stink_score,
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 stink_score method
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 stink_score of a module:
10
+ # Returns a numeric value that represents the skunk_score of a module:
11
11
  #
12
- # If module is perfectly covered, stink score is the same as the
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, stink score is a penalized value of
15
+ # If module has no coverage, skunk score is a penalized value of
16
16
  # `churn_times_cost`
17
17
  #
18
- # For now the stink_score is calculated by multiplying `churn_times_cost`
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
- # stink_score => 100
24
+ # skunk_score => 100
25
25
  #
26
26
  # When `churn_times_cost` is 100 and module is not covered at all:
27
- # stink_score => 100 * 100 = 10_000
27
+ # skunk_score => 100 * 100 = 10_000
28
28
  #
29
29
  # When `churn_times_cost` is 100 and module is covered at 75%:
30
- # stink_score => 100 * 25 (percentage uncovered) = 2_500
30
+ # skunk_score => 100 * 25 (percentage uncovered) = 2_500
31
31
  #
32
32
  # @return [Float]
33
- def stink_score
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