central-devtools 0.8.3

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/.rspec +6 -0
  4. data/.rubocop.yml +5 -0
  5. data/.ruby-gemset +1 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +191 -0
  8. data/README.md +36 -0
  9. data/Rakefile +8 -0
  10. data/central-devtools.gemspec +44 -0
  11. data/config/container.yml +7 -0
  12. data/config/devtools.yml +2 -0
  13. data/config/flay.yml +3 -0
  14. data/config/flog.yml +2 -0
  15. data/config/mutant.yml +4 -0
  16. data/config/reek.yml +102 -0
  17. data/config/rubocop.yml +117 -0
  18. data/config/yardstick.yml +2 -0
  19. data/lib/central/devtools/config.rb +199 -0
  20. data/lib/central/devtools/flay.rb +91 -0
  21. data/lib/central/devtools/project/initializer/rake.rb +23 -0
  22. data/lib/central/devtools/project/initializer/rspec.rb +70 -0
  23. data/lib/central/devtools/project/initializer.rb +13 -0
  24. data/lib/central/devtools/project.rb +80 -0
  25. data/lib/central/devtools/rake/flay.rb +121 -0
  26. data/lib/central/devtools/spec_helper.rb +5 -0
  27. data/lib/central/devtools/version.rb +7 -0
  28. data/lib/central/devtools.rb +90 -0
  29. data/shared/spec/shared/abstract_type_behavior.rb +16 -0
  30. data/shared/spec/shared/command_method_behavior.rb +5 -0
  31. data/shared/spec/shared/each_method_behaviour.rb +13 -0
  32. data/shared/spec/shared/hash_method_behavior.rb +7 -0
  33. data/shared/spec/shared/idempotent_method_behavior.rb +10 -0
  34. data/shared/spec/support/ice_nine_config.rb +9 -0
  35. data/shared/tasks/docker.rake +80 -0
  36. data/shared/tasks/metrics/ci.rake +19 -0
  37. data/shared/tasks/metrics/coverage.rake +14 -0
  38. data/shared/tasks/metrics/flay.rake +21 -0
  39. data/shared/tasks/metrics/flog.rake +38 -0
  40. data/shared/tasks/metrics/mutant.rake +44 -0
  41. data/shared/tasks/metrics/reek.rake +10 -0
  42. data/shared/tasks/metrics/rubocop.rake +15 -0
  43. data/shared/tasks/metrics/yardstick.rake +15 -0
  44. data/shared/tasks/spec.rake +36 -0
  45. data/shared/tasks/yard.rake +11 -0
  46. data/spec/integration/central/devtools/rake/flay/verify_spec.rb +166 -0
  47. data/spec/spec_helper.rb +27 -0
  48. data/spec/unit/central/devtools/config/yardstick_spec.rb +19 -0
  49. data/spec/unit/central/devtools/config_spec.rb +75 -0
  50. data/spec/unit/central/devtools/flay/file_list/call_spec.rb +21 -0
  51. data/spec/unit/central/devtools/flay/scale/flay_report_spec.rb +19 -0
  52. data/spec/unit/central/devtools/flay/scale/measure_spec.rb +45 -0
  53. data/spec/unit/central/devtools/project/initializer/rake_spec.rb +23 -0
  54. data/spec/unit/central/devtools/project/initializer/rspec_spec.rb +55 -0
  55. data/spec/unit/central/devtools/project_spec.rb +37 -0
  56. data/spec/unit/central/devtools_spec.rb +16 -0
  57. metadata +393 -0
@@ -0,0 +1,199 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ # Abstract base class of tool configuration
6
+ class Config
7
+ include Adamantium::Flat, AbstractType, Concord.new(:config_dir)
8
+
9
+ # Represent no configuration
10
+ DEFAULT_CONFIG = {}.freeze
11
+
12
+ # Simple named type check representation
13
+ class TypeCheck
14
+ # Type check against expected class
15
+ include Concord.new(:name, :allowed_classes)
16
+
17
+ FORMAT_ERROR = '%<name>s: Got instance of %<got>s expected %<allowed>s'.freeze
18
+ CLASS_DELIM = ','.freeze
19
+
20
+ # Check value for instance of expected class
21
+ #
22
+ # @param [Object] value
23
+ # @return [Object]
24
+ def call(value)
25
+ klass = value.class
26
+
27
+ unless allowed_classes.any?(&klass.method(:equal?))
28
+ fail TypeError, FORMAT_ERROR % {
29
+ name: name,
30
+ got: klass,
31
+ allowed: allowed_classes.join(CLASS_DELIM)
32
+ }
33
+ end
34
+
35
+ value
36
+ end
37
+ end
38
+
39
+ private_constant(*constants(false))
40
+
41
+ # Error raised on type errors
42
+ TypeError = Class.new(RuntimeError)
43
+
44
+ # Declare an attribute
45
+ #
46
+ # @param [Symbol] name
47
+ # @param [Array<Class>] classes
48
+ # @return [self]
49
+ # @api private
50
+ def self.attribute(name, classes, **options)
51
+ default = [options.fetch(:default)] if options.key?(:default)
52
+ type_check = TypeCheck.new(name, classes)
53
+ key = name.to_s
54
+
55
+ define_method(name) do
56
+ type_check.call(raw.fetch(key, *default))
57
+ end
58
+ end
59
+ private_class_method :attribute
60
+
61
+ # Return config path
62
+ #
63
+ # @return [String]
64
+ # @api private
65
+ def config_file
66
+ config_dir.join(self.class::FILE)
67
+ end
68
+ memoize :config_file
69
+
70
+ private
71
+
72
+ # Return raw data
73
+ #
74
+ # @return [Hash]
75
+ # @api private
76
+ def raw
77
+ yaml_config || DEFAULT_CONFIG
78
+ end
79
+ memoize :raw
80
+
81
+ # Return the raw config data from a yaml file
82
+ #
83
+ # @return [Hash]
84
+ # returned if the yaml file is found
85
+ # @return [nil]
86
+ # returned if the yaml file is not found
87
+ # @api private
88
+ def yaml_config
89
+ IceNine.deep_freeze(YAML.load_file(config_file)) if config_file.file?
90
+ end
91
+
92
+ # Rubocop configuration
93
+ class Rubocop < self
94
+ FILE = 'rubocop.yml'.freeze
95
+ end
96
+
97
+ # Reek configuration
98
+ class Reek < self
99
+ FILE = 'reek.yml'.freeze
100
+ end
101
+
102
+ # Flay configuration
103
+ class Flay < self
104
+ FILE = 'flay.yml'.freeze
105
+ DEFAULT_LIB_DIRS = %w(lib).freeze
106
+ DEFAULT_EXCLUDES = %w().freeze
107
+
108
+ attribute :total_score, Fixnum
109
+ attribute :threshold, Fixnum
110
+ attribute :lib_dirs, Array, default: DEFAULT_LIB_DIRS
111
+ attribute :excludes, Array, default: DEFAULT_EXCLUDES
112
+ end
113
+
114
+ # Yardstick configuration
115
+ class Yardstick < self
116
+ FILE = 'yardstick.yml'.freeze
117
+ OPTIONS = %w(
118
+ threshold
119
+ rules
120
+ verbose
121
+ path
122
+ require_exact_threshold
123
+ ).freeze
124
+
125
+ # Options hash that Yardstick understands
126
+ #
127
+ # @return [Hash]
128
+ # @api private
129
+ def options
130
+ OPTIONS.each_with_object({}) do |name, hash|
131
+ hash[name] = raw.fetch(name, nil)
132
+ end
133
+ end
134
+ end
135
+
136
+ # Flog configuration
137
+ class Flog < self
138
+ FILE = 'flog.yml'.freeze
139
+ DEFAULT_LIB_DIRS = %w(lib).freeze
140
+
141
+ attribute :total_score, Float
142
+ attribute :threshold, Float
143
+ attribute :lib_dirs, Array, default: DEFAULT_LIB_DIRS
144
+ end
145
+
146
+ # Mutant configuration
147
+ class Mutant < self
148
+ FILE = 'mutant.yml'.freeze
149
+ DEFAULT_NAME = ''.freeze
150
+ DEFAULT_STRATEGY = 'rspec'.freeze
151
+ DEFAULT_COVERAGE = '1/1'.freeze
152
+
153
+ attribute :name, String, default: DEFAULT_NAME
154
+ attribute :strategy, String, default: DEFAULT_STRATEGY
155
+ attribute :zombify, [TrueClass, FalseClass], default: false
156
+ attribute :since, [String, NilClass], default: nil
157
+ attribute :ignore_subjects, Array, default: []
158
+ attribute :expect_coverage, String, default: DEFAULT_COVERAGE
159
+ attribute :namespace, String
160
+ end
161
+
162
+ # Devtools configuration
163
+ class Devtools < self
164
+ FILE = 'devtools.yml'.freeze
165
+ DEFAULT_UNIT_TEST_TIMEOUT = 0.1
166
+
167
+ attribute :unit_test_timeout, Float, default: DEFAULT_UNIT_TEST_TIMEOUT
168
+ end
169
+
170
+ # Container configuration
171
+ class Container < self
172
+ FILE = 'container.yml'.freeze
173
+ OPTIONS = %w(
174
+ prefix
175
+ component
176
+ build_tag
177
+ from_image
178
+ from_version
179
+ ).freeze
180
+
181
+ # Options hash for the container
182
+ #
183
+ # @return [Hash]
184
+ # @api private
185
+ def options
186
+ OPTIONS.each_with_object({}) do |name, hash|
187
+ hash[name] = raw.fetch(name, nil)
188
+ end
189
+ end
190
+
191
+ attribute :prefix, String, default: nil
192
+ attribute :component, String, default: nil
193
+ attribute :build_tag, String, default: nil
194
+ attribute :from_image, String, default: nil
195
+ attribute :from_version, String, default: nil
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ module Flay
6
+ # Measure flay mass relative to size of duplicated sexps
7
+ class Scale
8
+ include Adamantium
9
+ include Anima.new(:minimum_mass, :files)
10
+ include Procto.call(:measure)
11
+
12
+ # Measure duplication mass
13
+ #
14
+ # @return [Array<Rational>]
15
+ # @api private
16
+ def measure
17
+ flay.masses.map do |hash, mass|
18
+ Rational(mass, flay.hashes.fetch(hash).size)
19
+ end
20
+ end
21
+
22
+ # Report flay output
23
+ #
24
+ # @return [undefined]
25
+ # @api private
26
+ def flay_report
27
+ flay.report
28
+ end
29
+
30
+ private
31
+
32
+ # Memoized flay instance
33
+ #
34
+ # @return [Flay]
35
+ # @api private
36
+ def flay
37
+ ::Flay.new(mass: minimum_mass).tap do |flay|
38
+ flay.process(*files)
39
+ flay.analyze
40
+ end
41
+ end
42
+ memoize :flay, freezer: :noop
43
+ end
44
+
45
+ # Expand include and exclude file settings for flay
46
+ class FileList
47
+ include Procto.call, Concord.new(:includes, :excludes)
48
+
49
+ # Expand includes and filter by excludes
50
+ #
51
+ # @return [Set<Pathname>]
52
+ # @api private
53
+ def call
54
+ include_set - exclude_set
55
+ end
56
+
57
+ private
58
+
59
+ # Set of excluded files
60
+ #
61
+ # @return [Set<Pathname>]
62
+ # @api private
63
+ def exclude_set
64
+ excludes.flat_map(&Pathname.method(:glob))
65
+ end
66
+
67
+ # Set of included files
68
+ #
69
+ # Expanded using flay's file expander which takes into
70
+ # account flay's plugin support
71
+ #
72
+ # @return [Set<Pathname>]
73
+ # @api private
74
+ def include_set
75
+ Set.new(flay_includes.map(&method(:Pathname)))
76
+ end
77
+
78
+ # Expand includes using flay
79
+ #
80
+ # Expanded using flay's file expander which takes into
81
+ # account flay's plugin support
82
+ #
83
+ # @return [Array<String>]
84
+ # @api private
85
+ def flay_includes
86
+ ::Flay.expand_dirs_to_files(includes)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ class Project
6
+ class Initializer
7
+ # Imports all devtools rake tasks into a project
8
+ class Rake < self
9
+ include AbstractType
10
+
11
+ # Initialize rake tasks
12
+ #
13
+ # @return [undefined]
14
+ # @api rpivate
15
+ def self.call
16
+ FileList.glob(RAKE_FILES_GLOB).each(&::Rake.application.method(:add_import))
17
+ self
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ class Project
6
+ class Initializer
7
+ # Requires all shared specs in a project's spec_helper
8
+ # Also installs a configurable unit test timeout
9
+ class Rspec < self
10
+ include Concord.new(:project)
11
+
12
+ # Call initializer for project
13
+ #
14
+ # @param [Project] project
15
+ # @return [self]
16
+ # @api private
17
+ def self.call(project)
18
+ new(project).__send__(:call)
19
+ self
20
+ end
21
+
22
+ private
23
+
24
+ # Setup RSpec for project
25
+ #
26
+ # @return [self]
27
+ # @api private
28
+ def call
29
+ require_shared_spec_files
30
+ enable_unit_test_timeout
31
+ end
32
+
33
+ # Timeout unit tests that take longer than configured amount of time
34
+ #
35
+ # @param [Numeric] timeout
36
+ # @return [undefined]
37
+ # @raise [Timeout::Error]
38
+ # raised when the times are outside the timeout
39
+ # @api private
40
+ def enable_unit_test_timeout
41
+ timeout = project.devtools.unit_test_timeout
42
+ RSpec.configuration.around(file_path: UNIT_TEST_PATH_REGEXP) do |example|
43
+ Timeout.timeout(timeout, &example)
44
+ end
45
+ end
46
+
47
+ # Trigger the require of shared spec files
48
+ #
49
+ # @return [undefined]
50
+ # @api private
51
+ def require_shared_spec_files
52
+ require_files(SHARED_SPEC_PATH)
53
+ require_files(project.spec_root)
54
+ end
55
+
56
+ # Require files with pattern
57
+ #
58
+ # @param [Pathname] dir
59
+ # the directory containing the files to require
60
+ #
61
+ # @return [self]
62
+ # @api private
63
+ def require_files(dir)
64
+ Dir.glob(dir.join(SHARED_SPEC_PATTERN)).each(&Kernel.method(:require))
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ class Project
6
+ # Base class for project initializers
7
+ class Initializer
8
+ include AbstractType
9
+ abstract_singleton_method :call
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ class Project
6
+ include Concord.new(:root)
7
+
8
+ CONFIGS = {
9
+ container: Config::Container,
10
+ devtools: Config::Devtools,
11
+ flay: Config::Flay,
12
+ flog: Config::Flog,
13
+ reek: Config::Reek,
14
+ mutant: Config::Mutant,
15
+ rubocop: Config::Rubocop,
16
+ yardstick: Config::Yardstick
17
+ }.freeze
18
+
19
+ private_constant(*constants(false))
20
+
21
+ attr_reader(*CONFIGS.keys)
22
+
23
+ # The spec root
24
+ #
25
+ # @return [Pathname]
26
+ #
27
+ # @api private
28
+ attr_reader :spec_root
29
+
30
+ # Initialize object
31
+ #
32
+ # @param [Pathname] root
33
+ #
34
+ # @return [undefined]
35
+ #
36
+ # @api private
37
+ #
38
+ def initialize(root)
39
+ super(root)
40
+
41
+ initialize_environment
42
+ initialize_configs
43
+ end
44
+
45
+ # Init rspec
46
+ #
47
+ # @return [self]
48
+ #
49
+ # @api private
50
+ def init_rspec
51
+ Initializer::Rspec.call(self)
52
+ self
53
+ end
54
+
55
+ private
56
+
57
+ # Initialize environment
58
+ #
59
+ # @return [undefined]
60
+ #
61
+ # @api private
62
+ def initialize_environment
63
+ @spec_root = root.join(SPEC_DIRECTORY_NAME)
64
+ end
65
+
66
+ # Initialize configs
67
+ #
68
+ # @return [undefined]
69
+ #
70
+ # @api private
71
+ def initialize_configs
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
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,121 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ module Rake
6
+ # Flay metric runner
7
+ class Flay
8
+ include Anima.new(:threshold, :total_score, :lib_dirs, :excludes),
9
+ Procto.call(:verify),
10
+ Adamantium
11
+
12
+ BELOW_THRESHOLD = 'Adjust flay threshold down to %d'.freeze
13
+ TOTAL_MISMATCH = 'Flay total is now %d, but expected %d'.freeze
14
+ ABOVE_THRESHOLD = '%d chunks have a duplicate mass > %d'.freeze
15
+
16
+ # Verify code specified by `files` does not violate flay expectations
17
+ #
18
+ # @raise [SystemExit] if a violation is found
19
+ # @return [undefined] otherwise
20
+ #
21
+ # rubocop:disable MethodLength
22
+ #
23
+ # @api private
24
+ def verify
25
+ # Run flay first to ensure the max mass matches the threshold
26
+ Central::Devtools.notify_metric_violation(
27
+ BELOW_THRESHOLD % largest_mass
28
+ ) if below_threshold?
29
+
30
+ Central::Devtools.notify_metric_violation(
31
+ TOTAL_MISMATCH % [total_mass, total_score]
32
+ ) if total_mismatch?
33
+
34
+ # Run flay a second time with the threshold set
35
+ return unless above_threshold?
36
+
37
+ restricted_flay_scale.flay_report
38
+ Central::Devtools.notify_metric_violation(
39
+ ABOVE_THRESHOLD % [restricted_mass_size, threshold]
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ # List of files flay will analyze
46
+ #
47
+ # @return [Set<Pathname>]
48
+ # @api private
49
+ def files
50
+ Central::Devtools::Flay::FileList.call(lib_dirs, excludes)
51
+ end
52
+
53
+ # Is there mass duplication which exceeds specified `threshold`
54
+ #
55
+ # @return [Boolean]
56
+ # @api private
57
+ def above_threshold?
58
+ restricted_mass_size.nonzero?
59
+ end
60
+
61
+ # Is the specified `threshold` greater than the largest flay mass
62
+ #
63
+ # @return [Boolean]
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
+ # @api private
73
+ def total_mismatch?
74
+ !total_mass.equal?(total_score)
75
+ end
76
+
77
+ # Size of mass measured by `Flay::Scale` and filtered by `threshold`
78
+ #
79
+ # @return [Integer]
80
+ # @api private
81
+ def restricted_mass_size
82
+ restricted_flay_scale.measure.size
83
+ end
84
+
85
+ # Sum of all flay mass
86
+ #
87
+ # @return [Integer]
88
+ # @api private
89
+ def total_mass
90
+ flay_masses.reduce(:+).to_i
91
+ end
92
+
93
+ # Largest flay mass found
94
+ #
95
+ # @return [Integer]
96
+ # @api private
97
+ def largest_mass
98
+ flay_masses.max.to_i
99
+ end
100
+
101
+ # Flay scale which only measures mass above `threshold`
102
+ #
103
+ # @return [Flay::Scale]
104
+ # @api private
105
+ def restricted_flay_scale
106
+ Central::Devtools::Flay::Scale.new(minimum_mass: threshold.succ, files: files)
107
+ end
108
+ memoize :restricted_flay_scale
109
+
110
+ # All flay masses found in `files`
111
+ #
112
+ # @return [Array<Rational>]
113
+ # @api private
114
+ def flay_masses
115
+ Central::Devtools::Flay::Scale.call(minimum_mass: 0, files: files)
116
+ end
117
+ memoize :flay_masses
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'central/devtools'
4
+
5
+ Central::Devtools::PROJECT.init_rspec
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ module Central
4
+ module Devtools
5
+ VERSION = '0.8.3'.freeze
6
+ end
7
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: UTF-8
2
+
3
+ # Stdlib infrastructure
4
+ require 'pathname'
5
+ require 'rake'
6
+ require 'timeout'
7
+ require 'yaml'
8
+ require 'fileutils'
9
+
10
+ # Non stdlib infrastructure
11
+ require 'procto'
12
+ require 'anima'
13
+ require 'concord'
14
+ require 'adamantium'
15
+
16
+ # Wrapped tools
17
+ require 'flay'
18
+ require 'rspec'
19
+ require 'rspec/its'
20
+ require 'simplecov'
21
+
22
+ module Central
23
+ # The devtools namespace
24
+ module Devtools
25
+ ROOT = Pathname.new(__FILE__).parent.parent.parent.freeze
26
+ PROJECT_ROOT = Pathname.pwd.freeze
27
+ SHARED_PATH = ROOT.join('shared').freeze
28
+ SHARED_SPEC_PATH = SHARED_PATH.join('spec').freeze
29
+ DEFAULT_CONFIG_PATH = ROOT.join('default/config').freeze
30
+ RAKE_FILES_GLOB = ROOT.join('shared/tasks/**/*.rake').to_s.freeze
31
+ LIB_DIRECTORY_NAME = 'lib'.freeze
32
+ SPEC_DIRECTORY_NAME = 'spec'.freeze
33
+ RAKE_FILE_NAME = 'Rakefile'.freeze
34
+ SHARED_SPEC_PATTERN = '{shared,support}/**/*.rb'.freeze
35
+ UNIT_TEST_PATH_REGEXP = %r{\bspec/unit/}
36
+ DEFAULT_CONFIG_DIR_NAME = 'config'.freeze
37
+
38
+ private_constant(*constants(false))
39
+
40
+ # Abort with message when a metric violation occurs.
41
+ #
42
+ # @param [String] message
43
+ # @return [undefined]
44
+ # @api private
45
+ def self.notify_metric_violation(message)
46
+ abort(message)
47
+ end
48
+
49
+ # Initialize project and load tasks. This should *only* be called from an
50
+ # $application_root/Rakefile.
51
+ #
52
+ # @return [self]
53
+ # @api public
54
+ def self.init
55
+ Project::Initializer::Rake.call
56
+ self
57
+ end
58
+
59
+ # Return devtools root path.
60
+ #
61
+ # @return [Pathname]
62
+ # @api private
63
+ def self.root
64
+ ROOT
65
+ end
66
+
67
+ # Return project.
68
+ #
69
+ # @return [Project]
70
+ # @api private
71
+ def self.project
72
+ PROJECT
73
+ end
74
+ end
75
+ end
76
+
77
+ require_relative 'devtools/config'
78
+ require_relative 'devtools/project'
79
+ require_relative 'devtools/project/initializer'
80
+ require_relative 'devtools/project/initializer/rake'
81
+ require_relative 'devtools/project/initializer/rspec'
82
+ require_relative 'devtools/flay'
83
+ require_relative 'devtools/rake/flay'
84
+
85
+ module Central
86
+ # Self initialization
87
+ module Devtools
88
+ PROJECT = Project.new(PROJECT_ROOT)
89
+ end
90
+ end