devtools 0.1.0 → 0.1.5

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/Gemfile +0 -2
  4. data/README.md +3 -3
  5. data/circle.yml +1 -1
  6. data/config/devtools.yml +2 -0
  7. data/config/flay.yml +2 -2
  8. data/config/flog.yml +1 -1
  9. data/config/mutant.yml +2 -0
  10. data/config/reek.yml +7 -12
  11. data/config/rubocop.yml +8 -0
  12. data/devtools.gemspec +21 -19
  13. data/lib/devtools/config.rb +76 -55
  14. data/lib/devtools/flay.rb +94 -0
  15. data/lib/devtools/project/initializer/rake.rb +9 -7
  16. data/lib/devtools/project/initializer/rspec.rb +28 -55
  17. data/lib/devtools/project/initializer.rb +2 -12
  18. data/lib/devtools/project.rb +28 -105
  19. data/lib/devtools/rake/flay.rb +126 -0
  20. data/lib/devtools/spec_helper.rb +1 -3
  21. data/lib/devtools.rb +34 -83
  22. data/shared/spec/shared/abstract_type_behavior.rb +0 -2
  23. data/shared/spec/shared/command_method_behavior.rb +0 -2
  24. data/shared/spec/shared/each_method_behaviour.rb +0 -2
  25. data/shared/spec/shared/hash_method_behavior.rb +0 -2
  26. data/shared/spec/shared/idempotent_method_behavior.rb +0 -2
  27. data/shared/spec/support/ice_nine_config.rb +0 -2
  28. data/spec/integration/devtools/rake/flay/verify_spec.rb +164 -0
  29. data/spec/spec_helper.rb +2 -8
  30. data/spec/unit/devtools/config/yardstick_spec.rb +17 -0
  31. data/spec/unit/devtools/config_spec.rb +78 -0
  32. data/spec/unit/devtools/flay/file_list/call_spec.rb +19 -0
  33. data/spec/unit/devtools/flay/scale/flay_report_spec.rb +17 -0
  34. data/spec/unit/devtools/flay/scale/measure_spec.rb +43 -0
  35. data/spec/unit/devtools/project/initializer/rake_spec.rb +21 -0
  36. data/spec/unit/devtools/project/initializer/rspec_spec.rb +52 -0
  37. data/spec/unit/devtools/project_spec.rb +34 -0
  38. data/spec/unit/devtools_spec.rb +14 -0
  39. data/tasks/metrics/ci.rake +2 -3
  40. data/tasks/metrics/coverage.rake +0 -2
  41. data/tasks/metrics/flay.rake +6 -33
  42. data/tasks/metrics/flog.rake +0 -2
  43. data/tasks/metrics/mutant.rake +31 -39
  44. data/tasks/metrics/reek.rake +5 -4
  45. data/tasks/metrics/rubocop.rake +0 -2
  46. data/tasks/metrics/yardstick.rake +0 -14
  47. data/tasks/spec.rake +0 -2
  48. data/tasks/yard.rake +0 -2
  49. metadata +99 -35
  50. data/.travis.yml +0 -15
  51. data/TODO +0 -0
  52. data/bin/devtools +0 -18
  53. data/default/config/devtools.yml +0 -2
  54. data/default/config/flay.yml +0 -3
  55. data/default/config/flog.yml +0 -2
  56. data/default/config/mutant.yml +0 -3
  57. data/default/config/reek.yml +0 -103
  58. data/default/config/rubocop.yml +0 -91
  59. data/default/config/yardstick.yml +0 -2
  60. data/lib/devtools/platform.rb +0 -118
  61. data/lib/devtools/site/initializer.rb +0 -57
  62. data/lib/devtools/site.rb +0 -41
@@ -1,87 +1,22 @@
1
- # encoding: utf-8
2
-
3
1
  module Devtools
4
2
 
5
3
  # The project devtools supports
6
4
  class Project
5
+ include Concord.new(:root)
7
6
 
8
- # The reek configuration
9
- #
10
- # @return [Config::Reek]
11
- #
12
- # @api private
13
- attr_reader :reek
14
-
15
- # The rubocop configuration
16
- #
17
- # @return [Config::Rubocop]
18
- #
19
- # @api private
20
- attr_reader :rubocop
7
+ CONFIGS = {
8
+ devtools: Config::Devtools,
9
+ flay: Config::Flay,
10
+ flog: Config::Flog,
11
+ reek: Config::Reek,
12
+ mutant: Config::Mutant,
13
+ rubocop: Config::Rubocop,
14
+ yardstick: Config::Yardstick
15
+ }.freeze
21
16
 
22
- # The flog configuration
23
- #
24
- # @return [Config::Flog]
25
- #
26
- # @api private
27
- attr_reader :flog
17
+ private_constant(*constants(false))
28
18
 
29
- # The yardstick configuration
30
- #
31
- # @return [Config::Yardstick]
32
- #
33
- # @api private
34
- attr_reader :yardstick
35
-
36
- # The flay configuration
37
- #
38
- # @return [Config::Flay]
39
- #
40
- # @api private
41
- attr_reader :flay
42
-
43
- # The mutant configuration
44
- #
45
- # @return [Config::Mutant]
46
- #
47
- # @api private
48
- attr_reader :mutant
49
-
50
- # The devtools configuration
51
- #
52
- # @return [Config::Devtools]
53
- #
54
- # @api private
55
- attr_reader :devtools
56
-
57
- # Return project root
58
- #
59
- # @return [Pathname]
60
- #
61
- # @api private
62
- #
63
- attr_reader :root
64
-
65
- # The default config path
66
- #
67
- # @return [Pathname]
68
- #
69
- # @api private
70
- attr_reader :default_config_path
71
-
72
- # The lib directory
73
- #
74
- # @return [Pathname]
75
- #
76
- # @api private
77
- attr_reader :lib_dir
78
-
79
- # The Ruby file pattern
80
- #
81
- # @return [Pathname]
82
- #
83
- # @api private
84
- attr_reader :file_pattern
19
+ attr_reader(*CONFIGS.keys)
85
20
 
86
21
  # The spec root
87
22
  #
@@ -90,20 +25,6 @@ module Devtools
90
25
  # @api private
91
26
  attr_reader :spec_root
92
27
 
93
- # Return config directory
94
- #
95
- # @return [Pathname]
96
- #
97
- # @api private
98
- attr_reader :config_dir
99
-
100
- # The unit test timeout
101
- #
102
- # @return [Numeric]
103
- #
104
- # @api private
105
- attr_reader :unit_test_timeout
106
-
107
28
  # Initialize object
108
29
  #
109
30
  # @param [Pathname] root
@@ -113,12 +34,20 @@ module Devtools
113
34
  # @api private
114
35
  #
115
36
  def initialize(root)
116
- @root = root
37
+ super(root)
117
38
 
118
39
  initialize_environment
119
40
  initialize_configs
41
+ end
120
42
 
121
- @unit_test_timeout = @devtools.unit_test_timeout
43
+ # Init rspec
44
+ #
45
+ # @return [self]
46
+ #
47
+ # @api private
48
+ def init_rspec
49
+ Initializer::Rspec.call(self)
50
+ self
122
51
  end
123
52
 
124
53
  private
@@ -130,11 +59,7 @@ module Devtools
130
59
  # @api private
131
60
  #
132
61
  def initialize_environment
133
- @default_config_path = @root.join(DEFAULT_CONFIG_DIR_NAME).freeze
134
- @lib_dir = @root.join(LIB_DIRECTORY_NAME).freeze
135
- @spec_root = @root.join(SPEC_DIRECTORY_NAME).freeze
136
- @file_pattern = @lib_dir.join(RB_FILE_PATTERN).freeze
137
- @config_dir = @default_config_path
62
+ @spec_root = root.join(SPEC_DIRECTORY_NAME)
138
63
  end
139
64
 
140
65
  # Initialize configs
@@ -144,13 +69,11 @@ module Devtools
144
69
  # @api private
145
70
  #
146
71
  def initialize_configs
147
- @reek = Config::Reek.new(self)
148
- @rubocop = Config::Rubocop.new(self)
149
- @flog = Config::Flog.new(self)
150
- @yardstick = Config::Yardstick.new(self)
151
- @flay = Config::Flay.new(self)
152
- @mutant = Config::Mutant.new(self)
153
- @devtools = Config::Devtools.new(self)
72
+ config_dir = root.join(DEFAULT_CONFIG_DIR_NAME)
73
+
74
+ CONFIGS.each do |name, klass|
75
+ instance_variable_set(:"@#{name}", klass.new(config_dir))
76
+ end
154
77
  end
155
78
 
156
79
  end # class Project
@@ -0,0 +1,126 @@
1
+ module Devtools
2
+ module Rake
3
+ # Flay metric runner
4
+ class Flay
5
+ include Anima.new(:threshold, :total_score, :lib_dirs, :excludes),
6
+ Procto.call(:verify),
7
+ Adamantium
8
+
9
+ BELOW_THRESHOLD = 'Adjust flay threshold down to %d'.freeze
10
+ TOTAL_MISMATCH = 'Flay total is now %d, but expected %d'.freeze
11
+ ABOVE_THRESHOLD = '%d chunks have a duplicate mass > %d'.freeze
12
+
13
+ # Verify code specified by `files` does not violate flay expectations
14
+ #
15
+ # @raise [SystemExit] if a violation is found
16
+ # @return [undefined] otherwise
17
+ #
18
+ # rubocop:disable MethodLength
19
+ #
20
+ # @api private
21
+ def verify
22
+ # Run flay first to ensure the max mass matches the threshold
23
+ Devtools.notify_metric_violation(
24
+ BELOW_THRESHOLD % largest_mass
25
+ ) if below_threshold?
26
+
27
+ Devtools.notify_metric_violation(
28
+ TOTAL_MISMATCH % [total_mass, total_score]
29
+ ) if total_mismatch?
30
+
31
+ # Run flay a second time with the threshold set
32
+ return unless above_threshold?
33
+
34
+ restricted_flay_scale.flay_report
35
+ Devtools.notify_metric_violation(
36
+ ABOVE_THRESHOLD % [restricted_mass_size, threshold]
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ # List of files flay will analyze
43
+ #
44
+ # @return [Set<Pathname>]
45
+ #
46
+ # @api private
47
+ def files
48
+ Devtools::Flay::FileList.call(lib_dirs, excludes)
49
+ end
50
+
51
+ # Is there mass duplication which exceeds specified `threshold`
52
+ #
53
+ # @return [Boolean]
54
+ #
55
+ # @api private
56
+ def above_threshold?
57
+ restricted_mass_size.nonzero?
58
+ end
59
+
60
+ # Is the specified `threshold` greater than the largest flay mass
61
+ #
62
+ # @return [Boolean]
63
+ #
64
+ # @api private
65
+ def below_threshold?
66
+ threshold > largest_mass
67
+ end
68
+
69
+ # Is the expected mass total different from the actual mass total
70
+ #
71
+ # @return [Boolean]
72
+ #
73
+ # @api private
74
+ def total_mismatch?
75
+ !total_mass.equal?(total_score)
76
+ end
77
+
78
+ # Size of mass measured by `Flay::Scale` and filtered by `threshold`
79
+ #
80
+ # @return [Integer]
81
+ #
82
+ # @api private
83
+ def restricted_mass_size
84
+ restricted_flay_scale.measure.size
85
+ end
86
+
87
+ # Sum of all flay mass
88
+ #
89
+ # @return [Integer]
90
+ #
91
+ # @api private
92
+ def total_mass
93
+ flay_masses.reduce(:+).to_i
94
+ end
95
+
96
+ # Largest flay mass found
97
+ #
98
+ # @return [Integer]
99
+ #
100
+ # @api private
101
+ def largest_mass
102
+ flay_masses.max.to_i
103
+ end
104
+
105
+ # Flay scale which only measures mass above `threshold`
106
+ #
107
+ # @return [Flay::Scale]
108
+ #
109
+ # @api private
110
+ def restricted_flay_scale
111
+ Devtools::Flay::Scale.new(minimum_mass: threshold.succ, files: files)
112
+ end
113
+ memoize :restricted_flay_scale
114
+
115
+ # All flay masses found in `files`
116
+ #
117
+ # @return [Array<Rational>]
118
+ #
119
+ # @api private
120
+ def flay_masses
121
+ Devtools::Flay::Scale.call(minimum_mass: 0, files: files)
122
+ end
123
+ memoize :flay_masses
124
+ end
125
+ end
126
+ end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  require 'devtools'
4
2
 
5
- Devtools.init_spec_helper
3
+ Devtools::PROJECT.init_rspec
data/lib/devtools.rb CHANGED
@@ -1,24 +1,24 @@
1
- # encoding: utf-8
2
-
1
+ # Stdlib infrastructure
3
2
  require 'pathname'
4
3
  require 'rake'
5
4
  require 'timeout'
6
5
  require 'yaml'
7
6
  require 'fileutils'
8
7
 
9
- require 'devtools/platform'
10
- require 'devtools/site'
11
- require 'devtools/site/initializer'
12
- require 'devtools/project'
13
- require 'devtools/config'
14
- require 'devtools/project/initializer'
15
- require 'devtools/project/initializer/rake'
16
- require 'devtools/project/initializer/rspec'
8
+ # Non stdlib infrastructure
9
+ require 'procto'
10
+ require 'anima'
11
+ require 'concord'
12
+ require 'adamantium'
17
13
 
18
- # Provides access to metric tools
19
- module Devtools
14
+ # Wrapped tools
15
+ require 'flay'
16
+ require 'rspec'
17
+ require 'rspec/its'
18
+ require 'simplecov'
20
19
 
21
- extend Platform
20
+ # Main devtools namespace population
21
+ module Devtools
22
22
 
23
23
  ROOT = Pathname.new(__FILE__).parent.parent.freeze
24
24
  PROJECT_ROOT = Pathname.pwd.freeze
@@ -28,17 +28,12 @@ module Devtools
28
28
  RAKE_FILES_GLOB = ROOT.join('tasks/**/*.rake').to_s.freeze
29
29
  LIB_DIRECTORY_NAME = 'lib'.freeze
30
30
  SPEC_DIRECTORY_NAME = 'spec'.freeze
31
- RB_FILE_PATTERN = '**/*.rb'.freeze
32
31
  RAKE_FILE_NAME = 'Rakefile'.freeze
33
- REQUIRE = "require 'devtools'".freeze
34
- INIT_RAKE_TASKS = 'Devtools.init_rake_tasks'.freeze
35
32
  SHARED_SPEC_PATTERN = '{shared,support}/**/*.rb'.freeze
36
33
  UNIT_TEST_PATH_REGEXP = %r{\bspec/unit/}.freeze
37
34
  DEFAULT_CONFIG_DIR_NAME = 'config'.freeze
38
- ANNOTATION_WRAPPER = "\n# Added by devtools\n%s\n".freeze
39
35
 
40
- # Provides devtools for a project
41
- SITE = Site.new(Project.new(PROJECT_ROOT))
36
+ private_constant(*constants(false))
42
37
 
43
38
  # React to metric violation
44
39
  #
@@ -63,26 +58,13 @@ module Devtools
63
58
  self
64
59
  end
65
60
 
66
- # Initialize project and load shared specs
67
- #
68
- # Expects to be called from $application_root/spec/spec_helper.rb
61
+ # Return devtools root path
69
62
  #
70
- # @return [self]
63
+ # @return [Pathname]
71
64
  #
72
- # @api public
73
- def self.init_spec_helper
74
- SITE.init_spec_helper
75
- self
76
- end
77
-
78
- # Initialize devtools using default config
79
- #
80
- # @return [undefined]
81
- #
82
- # @api public
83
- def self.init
84
- SITE.init
85
- self
65
+ # @api private
66
+ def self.root
67
+ ROOT
86
68
  end
87
69
 
88
70
  # Return project
@@ -91,53 +73,22 @@ module Devtools
91
73
  #
92
74
  # @api private
93
75
  def self.project
94
- SITE.project
76
+ PROJECT
95
77
  end
96
78
 
97
- # Require shared examples
98
- #
99
- # @param [Pathname] dir
100
- # the directory containing the files to require
101
- #
102
- # @param [String] pattern
103
- # the file pattern to match inside directory
104
- #
105
- # @return [self]
106
- #
107
- # @api private
108
- def self.require_files(dir, pattern)
109
- Dir[dir.join(pattern)].each { |file| require file }
110
- self
111
- end
112
-
113
- # Detect ci
114
- #
115
- # @return [Boolean]
116
- #
117
- # @api private
118
- #
119
- def self.ci?
120
- ENV.key?('CI')
121
- end
122
-
123
- # Detect circle ci
124
- #
125
- # @return [Boolean]
126
- #
127
- # @api private
128
- #
129
- def self.circle_ci?
130
- ENV.key?('CIRCLECI')
131
- end
79
+ end # module Devtools
132
80
 
133
- # Detect travis ci
134
- #
135
- # @return [Boolean]
136
- #
137
- # @api private
138
- #
139
- def self.travis?
140
- ENV.key?('TRAVIS')
141
- end
81
+ # Devtools implementation
82
+ require 'devtools/config'
83
+ require 'devtools/project'
84
+ require 'devtools/project/initializer'
85
+ require 'devtools/project/initializer/rake'
86
+ require 'devtools/project/initializer/rspec'
87
+ require 'devtools/flay'
88
+ require 'devtools/rake/flay'
142
89
 
143
- end # module Devtools
90
+ # Devtools self initialization
91
+ module Devtools
92
+ # The project devtools is active for
93
+ PROJECT = Project.new(PROJECT_ROOT)
94
+ end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  shared_examples_for 'an abstract type' do
4
2
  context 'called on a subclass' do
5
3
  let(:object) { Class.new(described_class) }
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  shared_examples_for 'a command method' do
4
2
  it 'returns self' do
5
3
  should equal(object)
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  shared_examples_for 'an #each method' do
4
2
  it_should_behave_like 'a command method'
5
3
 
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  shared_examples_for 'a hash method' do
4
2
  it_should_behave_like 'an idempotent method'
5
3
 
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  shared_examples_for 'an idempotent method' do
4
2
  it 'is idempotent' do
5
3
  first = subject
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  if defined?(IceNine)
4
2
  module IceNine
5
3
 
@@ -0,0 +1,164 @@
1
+ describe Devtools::Rake::Flay, '#verify' do
2
+ let(:tempfile) { Tempfile.new(%w[file .rb], Dir.mktmpdir) }
3
+ let(:file) { Pathname(tempfile.path) }
4
+ let(:directories) { [file.dirname.to_s] }
5
+
6
+ let(:ruby) do
7
+ <<-ERUBY
8
+ def foo; end
9
+ def bar; end
10
+ ERUBY
11
+ end
12
+
13
+ around(:each) do |example|
14
+ begin
15
+ # silence other flay output
16
+ $stdout = $stderr = StringIO.new
17
+
18
+ tempfile.write(ruby)
19
+ tempfile.close
20
+
21
+ example.run
22
+ ensure
23
+ $stdout = STDOUT
24
+ $stderr = STDERR
25
+
26
+ file.unlink
27
+ end
28
+ end
29
+
30
+ context 'reporting' do
31
+ let(:options) do
32
+ { threshold: 3, total_score: 3, lib_dirs: directories, excludes: [] }.freeze
33
+ end
34
+
35
+ let(:instance) { described_class.new(options) }
36
+
37
+ it 'measures total mass' do
38
+ allow(::Flay).to receive(:new).and_call_original
39
+
40
+ instance.verify
41
+
42
+ expect(::Flay).to have_received(:new).with(hash_including(mass: 0))
43
+ end
44
+
45
+ it 'does not report the files it is processing' do
46
+ expect { instance.verify }.to_not output(/Processing #{file}/).to_stderr
47
+ end
48
+ end
49
+
50
+ context 'when theshold is too low' do
51
+ let(:instance) do
52
+ described_class.new(threshold: 0, total_score: 0, lib_dirs: directories, excludes: [])
53
+ end
54
+
55
+ specify do
56
+ expect { instance.verify }
57
+ .to raise_error(SystemExit)
58
+ .with_message('Flay total is now 3, but expected 0')
59
+ end
60
+ end
61
+
62
+ context 'when threshold is too high' do
63
+ let(:instance) do
64
+ described_class.new(threshold: 1000, total_score: 0, lib_dirs: directories, excludes: [])
65
+ end
66
+
67
+ specify do
68
+ expect { instance.verify }
69
+ .to raise_error(SystemExit)
70
+ .with_message('Adjust flay threshold down to 3')
71
+ end
72
+ end
73
+
74
+ context 'when total is too high' do
75
+ let(:instance) do
76
+ described_class.new(threshold: 3, total_score: 50, lib_dirs: directories, excludes: [])
77
+ end
78
+
79
+ specify do
80
+ expect { instance.verify }
81
+ .to raise_error(SystemExit)
82
+ .with_message('Flay total is now 3, but expected 50')
83
+ end
84
+ end
85
+
86
+ context 'when duplicate mass is greater than 0' do
87
+ let(:ruby) do
88
+ <<-ERUBY
89
+ def foo
90
+ :hi if baz?
91
+ end
92
+
93
+ def bar
94
+ :hi if baz?
95
+ end
96
+ ERUBY
97
+ end
98
+
99
+ let(:report) do
100
+ <<-REPORT
101
+ Total score (lower is better) = 10
102
+
103
+ Similar code found in :defn (mass = 10)
104
+ #{file}:1
105
+ #{file}:5
106
+ REPORT
107
+ end
108
+
109
+ let(:instance) do
110
+ described_class.new(threshold: 3, total_score: 5, lib_dirs: directories, excludes: [])
111
+ end
112
+
113
+ specify do
114
+ expect { instance.verify }
115
+ .to raise_error(SystemExit)
116
+ .with_message('1 chunks have a duplicate mass > 3')
117
+ end
118
+
119
+ specify do
120
+ expect { instance.verify }
121
+ .to raise_error(SystemExit)
122
+ .and output(report).to_stdout
123
+ end
124
+ end
125
+
126
+ context 'when multiple duplicate masses' do
127
+ let(:ruby) do
128
+ <<-ERUBY
129
+ def foo; end
130
+ def bar; end
131
+
132
+ class Foo
133
+ def initialize
134
+ @a = 1
135
+ end
136
+ end
137
+ class Bar
138
+ def initialize
139
+ @a = 1
140
+ end
141
+ end
142
+ ERUBY
143
+ end
144
+
145
+ let(:instance) do
146
+ described_class.new(threshold: 5, total_score: 8, lib_dirs: directories, excludes: [])
147
+ end
148
+
149
+ it 'sums masses for total' do
150
+ expect { instance.verify }.to_not raise_error
151
+ end
152
+ end
153
+
154
+ context 'when no duplication masses' do
155
+ let(:ruby) { '' }
156
+ let(:instance) do
157
+ described_class.new(threshold: 0, total_score: 0, lib_dirs: directories, excludes: [])
158
+ end
159
+
160
+ specify do
161
+ expect { instance.verify }.to_not raise_error
162
+ end
163
+ end
164
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,14 +1,10 @@
1
- # encoding: utf-8
2
-
3
1
  require 'devtools/spec_helper'
2
+ require 'tempfile'
3
+ require 'tmpdir'
4
4
 
5
5
  if ENV['COVERAGE'] == 'true'
6
- require 'simplecov'
7
- require 'coveralls'
8
-
9
6
  SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
10
7
  SimpleCov::Formatter::HTMLFormatter,
11
- Coveralls::SimpleCov::Formatter
12
8
  ]
13
9
 
14
10
  SimpleCov.start do
@@ -22,8 +18,6 @@ if ENV['COVERAGE'] == 'true'
22
18
  end
23
19
  end
24
20
 
25
- require 'devtools'
26
-
27
21
  RSpec.configure do |config|
28
22
  config.expect_with :rspec do |expect_with|
29
23
  expect_with.syntax = :expect