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.
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
@@ -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 Command
9
- # Knows how to compare two branches and their stink score average
10
- class Compare < RubyCritic::Command::Compare
11
- # switch branch and analyse files but don't generate a report
12
- def analyse_branch(branch)
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
- # generate report only for modified files but don't report it
20
- def analyse_modified_files
21
- modified_files = ::RubyCritic::Config
22
- .feature_branch_collection
23
- .where(::RubyCritic::SourceControlSystem::Git.modified_files)
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
- # create a txt file with the branch score details
30
- def build_details
31
- details = "Base branch (#{::RubyCritic::Config.base_branch}) "\
32
- "average stink score: #{::RubyCritic::Config.base_branch_score} \n"\
33
- "Feature branch (#{::RubyCritic::Config.feature_branch}) "\
34
- "average stink score: #{::RubyCritic::Config.feature_branch_score} \n"
35
- File.open(build_details_path, "w") { |file| file.write(details) }
36
- puts details
37
- end
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
- def build_details_path
40
- "#{::RubyCritic::Config.compare_root_directory}/build_details.txt"
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
- @status_reporter = Skunk::Command::StatusReporter.new(@options)
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
- 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
@@ -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 stink_score churn_times_cost churn cost coverage].freeze
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
- StinkScore Total: <%= total_stink_score %>
19
+ SkunkScore Total: <%= total_skunk_score %>
18
20
  Modules Analysed: <%= analysed_modules_count %>
19
- StinkScore Average: <%= stink_score_average %>
20
- <% 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 %>
21
25
  TEMPL
22
26
  )
23
27
 
24
28
  # Returns a status message with a table of all analysed_modules and
25
- # a stink score average
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.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")
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(&:stink_score).reverse!
56
+ @sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
52
57
  end
53
58
 
54
- def total_stink_score
55
- @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)
56
61
  end
57
62
 
58
63
  def total_churn_times_cost
59
- non_test_modules.map(&:churn_times_cost).sum
64
+ non_test_modules.sum(&:churn_times_cost)
60
65
  end
61
66
 
62
- def stink_score_average
67
+ def skunk_score_average
63
68
  return 0 if analysed_modules_count.zero?
64
69
 
65
- (total_stink_score.to_d / analysed_modules_count).to_f
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: 200
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.stink_score,
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