skunk 0.4.2 → 0.5.0

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