skunk 0.3.2 → 0.5.1

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
@@ -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