skunk 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,25 +1,27 @@
1
1
  # Skunk
2
2
 
3
- [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](code-of-conduct.md) [![Build Status](https://travis-ci.org/fastruby/skunk.svg?branch=master)](https://travis-ci.org/fastruby/skunk) [![Maintainability](https://api.codeclimate.com/v1/badges/3e33d701ced16eee2420/maintainability)](https://codeclimate.com/github/fastruby/skunk/maintainability)
3
+ ![skunk](https://github.com/fastruby/skunk/raw/master/logo.png)
4
4
 
5
- A RubyCritic extension to calculate StinkScore for a file or project.
5
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) [![Build Status](https://travis-ci.org/fastruby/skunk.svg?branch=master)](https://travis-ci.org/fastruby/skunk) [![Maintainability](https://api.codeclimate.com/v1/badges/3e33d701ced16eee2420/maintainability)](https://codeclimate.com/github/fastruby/skunk/maintainability) [![Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/skunk) [![codecov](https://codecov.io/gh/fastruby/skunk/branch/master/graph/badge.svg)](https://codecov.io/gh/fastruby/skunk)
6
6
 
7
- ## What is the StinkScore?
7
+ A RubyCritic extension to calculate SkunkScore for a file or project.
8
8
 
9
- The StinkScore is a value that assesses the quality of a module. It takes into
10
- account:
9
+ ## What is the SkunkScore?
10
+
11
+ The SkunkScore is a value that assesses the technical debt of a module. It takes
12
+ into account:
11
13
 
12
14
  - Code Complexity
13
15
  - Code Smells
14
16
  - Code Coverage
15
17
 
16
- The main goal of the StinkScore is to serve as a compass in your next
18
+ The main goal of the SkunkScore is to serve as a compass in your next
17
19
  refactoring adventure. It will help you answer these questions:
18
20
 
19
21
  - What can I do to pay off technical debt?
20
22
  - What are the most complicated files with the least code coverage?
21
23
  - What are good candidates for your next test-writing efforts?
22
- - What are good candidates for your nest refactoring efforts?
24
+ - What are good candidates for your next refactoring efforts?
23
25
 
24
26
  The formula is not perfect and it is certainly controversial, so any feedback is
25
27
  welcome as a new issue!
@@ -42,7 +44,20 @@ Or install it yourself as:
42
44
 
43
45
  ## Usage
44
46
 
45
- ### Getting a sorted list of stinkiest files
47
+ ### Help details
48
+
49
+ There are not that many options but here they are:
50
+
51
+ ```
52
+ skunk -h
53
+ Usage: skunk [options] [paths]
54
+ -b, --branch BRANCH Set branch to compare
55
+ -o, --out FILE Output report to file
56
+ -v, --version Show gem's version
57
+ -h, --help Show this message
58
+ ```
59
+
60
+ ### Getting a sorted list of smelly files
46
61
 
47
62
  To get the best results, make sure that you have `coverage/.resultset.json` in
48
63
  your application directory. That way `skunk` knows what's the status of your
@@ -54,13 +69,10 @@ Then simply run:
54
69
  skunk
55
70
  ```
56
71
 
57
- Then get a list of stinky files:
72
+ Then get a list of smelly files:
58
73
 
59
74
  ```
60
75
  $ skunk
61
- warning: parser/current is loading parser/ruby26, which recognizes
62
- warning: 2.6.5-compliant syntax, but you are running 2.6.2.
63
- warning: please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
64
76
  running flay smells
65
77
 
66
78
  running flog smells
@@ -77,7 +89,7 @@ running simple_cov
77
89
  .............
78
90
  New critique at file:////Users/etagwerker/Projects/fastruby/skunk/tmp/rubycritic/overview.html
79
91
  +-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
80
- | file | stink_score | churn_times_cost | churn | cost | coverage |
92
+ | file | skunk_score | churn_times_cost | churn | cost | coverage |
81
93
  +-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
82
94
  | lib/skunk/cli/commands/default.rb | 166.44 | 1.6643999999999999 | 3 | 0.5548 | 0 |
83
95
  | lib/skunk/cli/application.rb | 139.2 | 1.392 | 3 | 0.46399999999999997 | 0 |
@@ -94,14 +106,14 @@ New critique at file:////Users/etagwerker/Projects/fastruby/skunk/tmp/rubycritic
94
106
  | lib/skunk/cli/commands/help.rb | 0.0 | 0.0 | 2 | 0.0 | 0 |
95
107
  +-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
96
108
 
97
- StinkScore Total: 612.31
109
+ SkunkScore Total: 612.31
98
110
  Modules Analysed: 13
99
- StinkScore Average: 0.47100769230769230769230769231e2
100
- Worst StinkScore: 166.44 (lib/skunk/cli/commands/default.rb)
111
+ SkunkScore Average: 0.47100769230769230769230769231e2
112
+ Worst SkunkScore: 166.44 (lib/skunk/cli/commands/default.rb)
101
113
  ```
102
114
 
103
115
  The command will run `rubycritic` and it will try to load code coverage data
104
- from your `.resultset.json` file.
116
+ from your `coverage/.resultset.json` file.
105
117
 
106
118
  Skunk's report will be in the console. Use it wisely. :)
107
119
 
@@ -113,7 +125,7 @@ Simply run:
113
125
  skunk -b <target-branch-name>
114
126
  ```
115
127
 
116
- Then get a StinkScore average comparison:
128
+ Then get a SkunkScore average comparison:
117
129
 
118
130
  ```
119
131
  $ skunk -b master
@@ -147,28 +159,63 @@ running churn
147
159
  .................
148
160
  running simple_cov
149
161
  .................
150
- Base branch (master) average stink score: 290.53999999999996
151
- Feature branch (feature/compare) average stink score: 340.3005882352941
162
+ Base branch (master) average skunk score: 290.53999999999996
163
+ Feature branch (feature/compare) average skunk score: 340.3005882352941
152
164
  Score: 340.3
153
165
  ```
154
166
 
155
167
  This should give you an idea if you're moving in the right direction or not.
156
168
 
169
+ ### Sharing results
170
+
171
+ If you want to quickly share the results of your report, you can use an
172
+ environment variable:
173
+
174
+ ```
175
+ SHARE=true skunk app/
176
+ ...
177
+ SkunkScore Total: 126.99
178
+ Modules Analysed: 17
179
+ SkunkScore Average: 7.47
180
+ Worst SkunkScore: 41.92 (lib/skunk/cli/commands/status_sharer.rb)
181
+
182
+ Generated with Skunk v0.5.0
183
+ Shared at: https://skunk.fastruby.io/k
184
+ ```
185
+
186
+ Results will be posted by default to https://skunk.fastruby.io which is a free
187
+ and open source Ruby on Rails application sponsored by
188
+ [OmbuLabs](https://www.ombulabs.com) ([source code](https://github.com/fastruby/skunk.fyi)).
189
+
190
+ If you prefer to post results to your own server, you can do so:
191
+
192
+ ```
193
+ SHARE_URL=https://path.to.your.skunk-fyi-server.example.com skunk app/
194
+ ...
195
+ SkunkScore Total: 126.99
196
+ Modules Analysed: 17
197
+ SkunkScore Average: 7.47
198
+ Worst SkunkScore: 41.92 (lib/skunk/cli/commands/status_sharer.rb)
199
+
200
+ Generated with Skunk v0.5.0
201
+ Shared at: https://path.to.your.skunk-fyi-server.example.com/k
202
+ ```
203
+
157
204
  ## Known Issues
158
205
 
159
- The StinkScore should be calculated per method. This would provide a more accurate
160
- representation of the average StinkScore in a module.
206
+ The SkunkScore should be calculated per method. This would provide a more accurate
207
+ representation of the average SkunkScore in a module.
161
208
 
162
- I think that the StinkScore of a module should be the average of the StinkScores of
209
+ I think that the SkunkScore of a module should be the average of the SkunkScores of
163
210
  all of its methods.
164
211
 
165
- Right now the StinkScore is calculated using the totals for a module:
212
+ Right now the SkunkScore is calculated using the totals for a module:
166
213
 
167
214
  - Total Code Coverage Percentage per Module
168
215
  - Total Churn per Module
169
216
  - Total Cost per Module
170
217
 
171
- For more details, feel free to review and improve this method: [RubyCritic::AnalysedModule#stink_score]
218
+ For more details, feel free to review and improve this method: [RubyCritic::AnalysedModule#skunk_score]
172
219
 
173
220
  ## Development
174
221
 
@@ -179,3 +226,9 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
179
226
  ## Contributing
180
227
 
181
228
  Bug reports and pull requests are welcome on GitHub at https://github.com/fastruby/skunk/issues.
229
+
230
+ ## Sponsorship
231
+
232
+ ![FastRuby.io | Rails Upgrade Services](https://github.com/fastruby/skunk/raw/master/fastruby-logo.png)
233
+
234
+ `skunk` is maintained and funded by [FastRuby.io](https://fastruby.io). The names and logos for FastRuby.io are trademarks of The Lean Software Boutique LLC.
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
@@ -1,25 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rubycritic/cli/options"
4
+ require "rubycritic/cli/application"
5
+
3
6
  require "skunk"
4
7
  require "skunk/rubycritic/analysed_module"
5
8
  require "skunk/cli/options"
6
9
  require "skunk/cli/command_factory"
7
-
8
- require "rubycritic/cli/application"
10
+ require "skunk/cli/commands/status_sharer"
9
11
 
10
12
  module Skunk
11
13
  module Cli
12
14
  # Knows how to execute command line commands
15
+ # :reek:InstanceVariableAssumption
13
16
  class Application < RubyCritic::Cli::Application
17
+ COVERAGE_FILE = "coverage/.resultset.json"
18
+
19
+ def initialize(argv)
20
+ @options = Skunk::Cli::Options.new(argv)
21
+ end
22
+
23
+ # :reek:UncommunicativeVariableName
14
24
  def execute
15
- parsed_options = @options.parse.to_h
16
- 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
+
17
32
  print(reporter.status_message)
33
+ share_status_message = command.share(reporter)
34
+ print(share_status_message)
35
+
18
36
  reporter.status
19
- rescue OptionParser::InvalidOption => error
20
- warn "Error: #{error}"
37
+ rescue OptionParser::InvalidOption => e
38
+ warn "Error: #{e}"
21
39
  STATUS_ERROR
22
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
23
58
  end
24
59
  end
25
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,17 +2,19 @@
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/compare_score"
5
7
 
6
8
  # nodoc #
7
9
  module Skunk
8
10
  module Command
9
- # Knows how to compare two branches and their stink score average
11
+ # Knows how to compare two branches and their skunk score average
10
12
  class Compare < RubyCritic::Command::Compare
11
13
  # switch branch and analyse files but don't generate a report
12
14
  def analyse_branch(branch)
13
15
  ::RubyCritic::SourceControlSystem::Git.switch_branch(::RubyCritic::Config.send(branch))
14
16
  critic = critique(branch)
15
- ::RubyCritic::Config.send(:"#{branch}_score=", critic.stink_score_average)
17
+ ::RubyCritic::Config.send(:"#{branch}_score=", critic.skunk_score_average)
16
18
  ::RubyCritic::Config.root = branch_directory(branch)
17
19
  end
18
20
 
@@ -28,10 +30,14 @@ module Skunk
28
30
 
29
31
  # create a txt file with the branch score details
30
32
  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"
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
+
40
+ Skunk::Command::Output.create_directory(::RubyCritic::Config.compare_root_directory)
35
41
  File.open(build_details_path, "w") { |file| file.write(details) }
36
42
  puts details
37
43
  end
@@ -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
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "skunk/cli/commands/base"
4
+ require "rubycritic/commands/help"
4
5
 
5
6
  module Skunk
6
7
  module Cli
7
8
  module Command
8
- 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
9
20
  end
10
21
  end
11
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
@@ -10,23 +10,27 @@ 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
- <%= ttable %>\n
17
- StinkScore Total: <%= total_stink_score %>
18
+ <%= _ttable %>\n
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
 
29
- ttable = Terminal::Table.new(opts)
33
+ _ttable = Terminal::Table.new(opts)
30
34
 
31
35
  @status_message = TEMPLATE.result(binding)
32
36
  end
@@ -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