├── TODO ├── .ruby-gemset ├── config ├── flog.yml ├── yardstick.yml ├── flay.yml ├── mutant.yml ├── rubocop.yml └── reek.yml ├── default └── config │ ├── flog.yml │ ├── yardstick.yml │ ├── flay.yml │ ├── mutant.yml │ ├── devtools.yml │ ├── rubocop.yml │ └── reek.yml ├── .rspec ├── Rakefile ├── lib ├── devtools │ ├── spec_helper.rb │ ├── project │ │ ├── initializer │ │ │ ├── rake.rb │ │ │ └── rspec.rb │ │ └── initializer.rb │ ├── site.rb │ ├── site │ │ └── initializer.rb │ ├── platform.rb │ ├── project.rb │ └── config.rb └── devtools.rb ├── Gemfile ├── shared ├── spec │ ├── shared │ │ ├── command_method_behavior.rb │ │ ├── idempotent_method_behavior.rb │ │ ├── hash_method_behavior.rb │ │ ├── each_method_behaviour.rb │ │ └── abstract_type_behavior.rb │ └── support │ │ └── ice_nine_config.rb └── Gemfile ├── tasks ├── yard.rake ├── metrics │ ├── coverage.rake │ ├── ci.rake │ ├── rubocop.rake │ ├── reek.rake │ ├── mutant.rake │ ├── yardstick.rake │ ├── flog.rake │ └── flay.rake └── spec.rake ├── bin └── devtools ├── .gitignore ├── .travis.yml ├── devtools.gemspec ├── spec └── spec_helper.rb ├── LICENSE ├── Guardfile └── README.md /TODO: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | devtools 2 | -------------------------------------------------------------------------------- /config/flog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 24.2 3 | -------------------------------------------------------------------------------- /config/yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /default/config/flog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 0 3 | -------------------------------------------------------------------------------- /default/config/yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /config/flay.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 9 3 | total_score: 105 4 | -------------------------------------------------------------------------------- /config/mutant.yml: -------------------------------------------------------------------------------- 1 | name: devtools 2 | namespace: Devtools 3 | -------------------------------------------------------------------------------- /default/config/flay.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 0 3 | total_score: 0 4 | -------------------------------------------------------------------------------- /default/config/mutant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: your_lib 3 | namespace: YourLib 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --profile 4 | --warnings 5 | --order random 6 | -------------------------------------------------------------------------------- /default/config/devtools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | unit_test_timeout: 0.1 3 | fail_on_branch: 4 | - "master" 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'devtools' 2 | 3 | ENV['DEVTOOLS_SELF'] = '1' 4 | 5 | Devtools.init_rake_tasks 6 | -------------------------------------------------------------------------------- /lib/devtools/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'devtools' 4 | 5 | Devtools.init_spec_helper 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | eval_gemfile 'shared/Gemfile' 8 | -------------------------------------------------------------------------------- /shared/spec/shared/command_method_behavior.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples_for 'a command method' do 4 | it 'returns self' do 5 | should equal(object) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /tasks/yard.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require 'yard' 5 | 6 | YARD::Rake::YardocTask.new 7 | rescue LoadError 8 | task :yard do 9 | $stderr.puts 'In order to run yard, you must: gem install yard' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /shared/spec/shared/idempotent_method_behavior.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples_for 'an idempotent method' do 4 | it 'is idempotent' do 5 | first = subject 6 | __memoized.delete(:subject) 7 | should equal(first) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /shared/spec/support/ice_nine_config.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | if defined?(IceNine) 4 | module IceNine 5 | 6 | # Freezer namespace 7 | class Freezer 8 | 9 | # Rspec freezer 10 | class RSpec < NoFreeze; end 11 | 12 | end # Freezer 13 | end # IceNine 14 | end 15 | -------------------------------------------------------------------------------- /shared/spec/shared/hash_method_behavior.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples_for 'a hash method' do 4 | it_should_behave_like 'an idempotent method' 5 | 6 | specification = proc do 7 | should be_instance_of(Fixnum) 8 | end 9 | 10 | it 'is a fixnum' do 11 | instance_eval(&specification) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /tasks/metrics/coverage.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | desc 'Measure code coverage' 5 | task :coverage do 6 | begin 7 | original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' 8 | Rake::Task['spec:unit'].invoke 9 | ensure 10 | ENV['COVERAGE'] = original 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/devtools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | require 'bundler' 5 | 6 | Bundler.setup 7 | 8 | require 'devtools' 9 | 10 | command = ARGV.first 11 | 12 | case command 13 | when 'init' then Devtools.init 14 | when 'sync' then Devtools.sync 15 | when 'update' then Devtools.update 16 | else 17 | puts 'command not supported' 18 | end 19 | -------------------------------------------------------------------------------- /shared/spec/shared/each_method_behaviour.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples_for 'an #each method' do 4 | it_should_behave_like 'a command method' 5 | 6 | context 'with no block' do 7 | subject { object.each } 8 | 9 | it { should be_instance_of(to_enum.class) } 10 | 11 | it 'yields the expected values' do 12 | expect(subject.to_a).to eql(object.to_a) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /tasks/metrics/ci.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | desc 'Run all specs, metrics and mutant' 4 | task ci: %w[ spec ci:metrics metrics:mutant ] 5 | 6 | namespace :ci do 7 | tasks = %w[ 8 | metrics:coverage 9 | metrics:yardstick:verify 10 | metrics:rubocop 11 | metrics:flog 12 | metrics:flay 13 | metrics:reek 14 | ] 15 | 16 | desc 'Run metrics (except mutant)' 17 | task metrics: tasks 18 | end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.sw[op] 15 | 16 | ## Rubinius 17 | *.rbc 18 | .rbx 19 | 20 | ## PROJECT::GENERAL 21 | *.gem 22 | coverage 23 | profiling 24 | turbulence 25 | rdoc 26 | pkg 27 | tmp 28 | doc 29 | log 30 | .yardoc 31 | measurements 32 | 33 | ## BUNDLER 34 | .bundle 35 | Gemfile.lock 36 | 37 | ## PROJECT::SPECIFIC 38 | -------------------------------------------------------------------------------- /lib/devtools/project/initializer/rake.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | class Project 5 | class Initializer 6 | 7 | # Imports all devtools rake tasks into a project 8 | class Rake 9 | 10 | extend ::Rake::DSL 11 | 12 | def self.call 13 | FileList[RAKE_FILES_GLOB].each { |task| import(task) } 14 | end 15 | 16 | end # class Rake 17 | end # class Initializer 18 | end # class Project 19 | end # module Devtools 20 | -------------------------------------------------------------------------------- /shared/spec/shared/abstract_type_behavior.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | shared_examples_for 'an abstract type' do 4 | context 'called on a subclass' do 5 | let(:object) { Class.new(described_class) } 6 | 7 | it { should be_instance_of(object) } 8 | end 9 | 10 | context 'called on the class' do 11 | let(:object) { described_class } 12 | 13 | specify do 14 | expect { subject } 15 | .to raise_error(NotImplementedError, "#{object} is an abstract type") 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/devtools/project/initializer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | class Project 5 | 6 | # Base class for project initializers 7 | class Initializer 8 | 9 | attr_reader :project 10 | protected :project 11 | 12 | def initialize(project) 13 | @project = project 14 | end 15 | 16 | def call 17 | fail NotImplementedError, "#{self.class}##{__method__} must be implemented" 18 | end 19 | end # class Initializer 20 | end # class Project 21 | end # module Devtools 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without yard guard benchmarks 3 | script: "bundle exec rake ci" 4 | rvm: 5 | - 1.9.3 6 | - 2.0.0 7 | - ruby-head 8 | - rbx 9 | matrix: 10 | include: 11 | - rvm: jruby-19mode 12 | env: JRUBY_OPTS="$JRUBY_OPTS --debug" 13 | - rvm: jruby-head 14 | env: JRUBY_OPTS="$JRUBY_OPTS --debug" 15 | notifications: 16 | irc: 17 | channels: 18 | - irc.freenode.org#rom-rb 19 | on_success: never 20 | on_failure: change 21 | email: 22 | recipients: 23 | - dan.kubb@gmail.com 24 | - mbj@schirp-dso.com 25 | on_success: never 26 | on_failure: change 27 | -------------------------------------------------------------------------------- /tasks/metrics/rubocop.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | desc 'Check with code style guide' 5 | task :rubocop do 6 | enabled = begin 7 | require 'rubocop' 8 | rescue LoadError, NotImplementedError 9 | false 10 | end 11 | 12 | if enabled 13 | require 'rubocop' 14 | config = Devtools.project.rubocop 15 | begin 16 | Rubocop::CLI.new.run(%W[--config #{config.config_file.to_s}]) 17 | rescue Encoding::CompatibilityError => exception 18 | Devtools.notify exception.message 19 | end 20 | else 21 | $stderr.puts 'Rubocop is disabled' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /devtools.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = 'devtools' 5 | gem.version = '0.0.2' 6 | gem.authors = [ 'Markus Schirp' ] 7 | gem.email = [ 'mbj@schirp-dso.com' ] 8 | gem.description = 'A metagem for ROM-style development' 9 | gem.summary = gem.description 10 | gem.homepage = 'https://github.com/rom-rb/devtools' 11 | gem.license = 'MIT' 12 | 13 | gem.require_paths = %w[lib] 14 | gem.files = `git ls-files`.split($/) 15 | gem.executables = %w[devtools] 16 | gem.test_files = `git ls-files -- spec`.split($/) 17 | gem.extra_rdoc_files = %w[README.md TODO] 18 | end 19 | -------------------------------------------------------------------------------- /tasks/metrics/reek.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | begin 5 | require 'reek/rake/task' 6 | 7 | project = Devtools.project 8 | config = project.reek 9 | 10 | if config.enabled? 11 | Reek::Rake::Task.new do |reek| 12 | reek.reek_opts = '--quiet' 13 | reek.fail_on_error = Devtools.fail_on_current_branch? 14 | reek.config_files = config.config_file.to_s 15 | reek.source_files = '{app,lib}/**/*.rb' 16 | end 17 | else 18 | task :reek do 19 | $stderr.puts 'Reek is disabled' 20 | end 21 | end 22 | rescue LoadError 23 | task :reek do 24 | $stderr.puts 'In order to run reek, you must: gem install reek' 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Run all specs' 7 | task spec: %w[ spec:unit spec:integration ] 8 | 9 | namespace :spec do 10 | desc 'Run unit specs' 11 | RSpec::Core::RakeTask.new(:unit) do |unit| 12 | unit.pattern = 'spec/unit/**/*_spec.rb' 13 | end 14 | 15 | desc 'Run integration specs' 16 | RSpec::Core::RakeTask.new(:integration) do |integration| 17 | integration.pattern = 'spec/integration/**/*_spec.rb' 18 | end 19 | end 20 | rescue LoadError 21 | %w[ spec spec:unit spec:integration ].each do |name| 22 | task name do 23 | $stderr.puts "In order to run #{name}, do: gem install rspec" 24 | end 25 | end 26 | end 27 | 28 | task test: :spec 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'devtools/spec_helper' 4 | 5 | if ENV['COVERAGE'] == 'true' 6 | require 'simplecov' 7 | require 'coveralls' 8 | 9 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 10 | SimpleCov::Formatter::HTMLFormatter, 11 | Coveralls::SimpleCov::Formatter 12 | ] 13 | 14 | SimpleCov.start do 15 | command_name 'spec:unit' 16 | add_filter 'config' 17 | add_filter 'spec' 18 | minimum_coverage 100 19 | end 20 | end 21 | 22 | require 'devtools' 23 | 24 | # Require spec support files and shared behavior 25 | Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each do |file| 26 | require file.chomp('.rb') 27 | end 28 | 29 | RSpec.configure do |config| 30 | config.expect_with :rspec do |expect_with| 31 | expect_with.syntax = :expect 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ruby Object Mapper Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tasks/metrics/mutant.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | allowed_versions = %w(mri-1.9.3 mri-2.0.0 rbx-1.9.3) 5 | 6 | enabled = begin 7 | require 'mutant' 8 | rescue LoadError, NotImplementedError 9 | false 10 | end 11 | 12 | config = Devtools.project.mutant 13 | enabled &&= config.enabled? && allowed_versions.include?(Devtools.rvm) 14 | 15 | zombify = %w( 16 | adamantium equalizer ice_nine infecto anima concord abstract_type 17 | descendants_tracker parser rspec unparser mutant 18 | ).include?(config.name) 19 | 20 | if enabled && !ENV['DEVTOOLS_SELF'] 21 | desc 'Measure mutation coverage' 22 | task mutant: :coverage do 23 | namespace = 24 | if zombify 25 | Mutant::Zombifier.zombify 26 | Zombie::Mutant 27 | else 28 | Mutant 29 | end 30 | 31 | namespaces = Array(config.namespace).map { |n| "::#{n}*" } 32 | status = namespace::CLI.run(['--include', 'lib', '--require', config.name, *namespaces, config.strategy]) 33 | 34 | if status.nonzero? 35 | Devtools.notify 'Mutant task is not successful' 36 | end 37 | end 38 | else 39 | desc 'Measure mutation coverage' 40 | task mutant: :coverage do 41 | $stderr.puts 'Mutant is disabled' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | guard :bundler do 4 | watch('Gemfile') 5 | watch('Gemfile.lock') 6 | watch(%w{.+.gemspec\z}) 7 | end 8 | 9 | guard :rspec, cli: File.read('.rspec').split.push('--fail-fast').join(' '), keep_failed: false do 10 | # Run all specs if configuration is modified 11 | watch('.rspec') { 'spec' } 12 | watch('Guardfile') { 'spec' } 13 | watch('Gemfile.lock') { 'spec' } 14 | watch('spec/spec_helper.rb') { 'spec' } 15 | 16 | # Run all specs if supporting files are modified 17 | watch(%r{\Aspec/(?:fixtures|lib|support|shared)/.+\.rb\z}) { 'spec' } 18 | 19 | # Run unit specs if associated lib code is modified 20 | watch(%r{\Alib/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}*"] } 21 | watch(%r{\Alib/(.+)/support/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}/#{m[2]}*"] } 22 | watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { 'spec' } 23 | 24 | # Run a spec if it is modified 25 | watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z}) 26 | end 27 | 28 | guard :rubocop, cli: %w[--config config/rubocop.yml] do 29 | watch(%r{.+\.(?:rb|rake)\z}) 30 | watch(%r{\Aconfig/rubocop\.yml\z}) { |m| File.dirname(m[0]) } 31 | watch(%r{(?:.+/)?\.rubocop\.yml\z}) { |m| File.dirname(m[0]) } 32 | end 33 | -------------------------------------------------------------------------------- /lib/devtools/site.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | 5 | # Encapsulates a specific {Project} devtools is used for 6 | class Site 7 | 8 | attr_reader :root 9 | 10 | attr_reader :project 11 | 12 | def initialize(project) 13 | @project = project 14 | @root = project.root 15 | end 16 | 17 | # Initialize project and load shared specs 18 | # 19 | # Expects to be called from $application_root/spec/spec_helper.rb 20 | # 21 | # @return [self] 22 | # 23 | # @api private 24 | def init_spec_helper 25 | Project::Initializer::Rspec.call(project) 26 | self 27 | end 28 | 29 | # Initialize devtools using default config 30 | # 31 | # @return [undefined] 32 | # 33 | # @api private 34 | def init 35 | Initializer.call(self) 36 | puts 'Run bundle install to complete the devtools installation' 37 | self 38 | end 39 | 40 | # Sync gemfiles 41 | # 42 | # @return [undefined] 43 | # 44 | # @api private 45 | def sync 46 | target = root.join(GEMFILE_NAME) 47 | FileUtils.cp(SHARED_GEMFILE_PATH, target) 48 | puts "Successfully synced #{target}" 49 | self 50 | end 51 | 52 | # Sync gemfiles and run bundle update 53 | # 54 | # @return [undefined] 55 | # 56 | # @api private 57 | def update 58 | sync 59 | system(BUNDLE_UPDATE) 60 | self 61 | end 62 | 63 | end # class Site 64 | end # module Devtools 65 | -------------------------------------------------------------------------------- /tasks/metrics/yardstick.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | namespace :yardstick do 5 | begin 6 | require 'yardstick/rake/measurement' 7 | require 'yardstick/rake/verify' 8 | 9 | if Devtools.project.yardstick.enabled? 10 | # Enable the legacy parser for JRuby until ripper is fully supported 11 | if Devtools.jruby? 12 | # Remove when https://github.com/lsegal/yard/issues/681 is resolved 13 | # This code first requires ripper, then removes the constant so 14 | # that it does not trigger a bug in YARD where if it checks if Ripper 15 | # is available and assumes other constants are defined, when JRuby's 16 | # implementation does not yet. 17 | require 'ripper' 18 | Object.send(:remove_const, :Ripper) 19 | YARD::Parser::SourceParser.parser_type = :ruby18 20 | end 21 | 22 | options = Devtools.project.yardstick.options 23 | 24 | Yardstick::Rake::Measurement.new(:measure, options) 25 | 26 | Yardstick::Rake::Verify.new(:verify, options) 27 | else 28 | %w[ measure verify ].each do |name| 29 | task name.to_s do 30 | $stderr.puts 'Yardstick is disabled' 31 | end 32 | end 33 | end 34 | rescue LoadError 35 | %w[ measure verify ].each do |name| 36 | task name.to_s do 37 | $stderr.puts "In order to run #{name}, do: gem install yardstick" 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /default/config/rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Includes: 3 | - '**/*.rake' 4 | - 'Gemfile' 5 | - 'Gemfile.devtools' 6 | Excludes: 7 | - '**/vendor/**' 8 | - '**/benchmarks/**' 9 | 10 | # Avoid parameter lists longer than five parameters. 11 | ParameterLists: 12 | Max: 3 13 | CountKeywordArgs: true 14 | 15 | # Avoid more than `Max` levels of nesting. 16 | BlockNesting: 17 | Max: 3 18 | 19 | # Align with the style guide. 20 | CollectionMethods: 21 | PreferredMethods: 22 | collect: 'map' 23 | inject: 'reduce' 24 | find: 'detect' 25 | find_all: 'select' 26 | 27 | # Do not force public/protected/private keyword to be indented at the same 28 | # level as the def keyword. My personal preference is to outdent these keywords 29 | # because I think when scanning code it makes it easier to identify the 30 | # sections of code and visually separate them. When the keyword is at the same 31 | # level I think it sort of blends in with the def keywords and makes it harder 32 | # to scan the code and see where the sections are. 33 | AccessModifierIndentation: 34 | Enabled: false 35 | 36 | # Limit line length 37 | LineLength: 38 | Max: 79 39 | 40 | # Disable documentation checking until a class needs to be documented once 41 | Documentation: 42 | Enabled: false 43 | 44 | # Do not favor modifier if/unless usage when you have a single-line body 45 | IfUnlessModifier: 46 | Enabled: false 47 | 48 | # Allow case equality operator (in limited use within the specs) 49 | CaseEquality: 50 | Enabled: false 51 | 52 | # Constants do not always have to use SCREAMING_SNAKE_CASE 53 | ConstantName: 54 | Enabled: false 55 | 56 | # Not all trivial readers/writers can be defined with attr_* methods 57 | TrivialAccessors: 58 | Enabled: false 59 | 60 | # Allow empty lines around body 61 | EmptyLinesAroundBody: 62 | Enabled: false 63 | -------------------------------------------------------------------------------- /tasks/metrics/flog.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | begin 5 | require 'flog' 6 | require 'flog_cli' 7 | 8 | project = Devtools.project 9 | config = project.flog 10 | 11 | compatible_scores = %w(mri-1.9.3) 12 | 13 | if ! compatible_scores.include?(Devtools.rvm) 14 | task :flog do 15 | $stderr.puts "Flog is disabled under #{Devtools.rvm}" 16 | end 17 | elsif config.enabled? 18 | # Original code by Marty Andrews: 19 | # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html 20 | desc 'Measure code complexity' 21 | task :flog do 22 | threshold = config.threshold.to_f.round(1) 23 | flog = Flog.new 24 | flog.flog(*FlogCLI.expand_dirs_to_files(project.lib_dir)) 25 | 26 | totals = flog.totals.select { |name, score| name[-5, 5] != '#none' } 27 | .map { |name, score| [name, score.round(1)] } 28 | .sort_by { |name, score| score } 29 | 30 | if totals.any? 31 | max = totals.last[1] 32 | unless max >= threshold 33 | Devtools.notify "Adjust flog score down to #{max}" 34 | end 35 | end 36 | 37 | bad_methods = totals.select { |name, score| score > threshold } 38 | if bad_methods.any? 39 | bad_methods.reverse_each do |name, score| 40 | printf "%8.1f: %s\n", score, name 41 | end 42 | 43 | Devtools.notify( 44 | "#{bad_methods.size} methods have a flog complexity > #{threshold}" 45 | ) 46 | end 47 | end 48 | else 49 | task :flog do 50 | $stderr.puts 'Flog is disabled' 51 | end 52 | end 53 | rescue LoadError 54 | task :flog do 55 | $stderr.puts 'In order to run flog, you must: gem install flog' 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/devtools/site/initializer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | class Site 5 | 6 | # Supports initializing new projects with a Gemfile and Rakefile 7 | class Initializer 8 | 9 | def self.call(root) 10 | new(root).call 11 | end 12 | 13 | attr_reader :site 14 | 15 | attr_reader :root 16 | 17 | attr_reader :config_dir 18 | 19 | def initialize(site) 20 | @site = site 21 | @root = site.root 22 | config_dir = @root.join(DEFAULT_CONFIG_DIR_NAME).tap(&:mkpath) 23 | @config_dir = config_dir.parent 24 | end 25 | 26 | # Init devtools using default config 27 | # 28 | # @return [undefined] 29 | # 30 | # @api public 31 | def call 32 | FileUtils.cp_r(DEFAULT_CONFIG_PATH, config_dir) 33 | 34 | site.sync 35 | init_gemfile 36 | init_rakefile 37 | 38 | self 39 | end 40 | 41 | private 42 | 43 | # Initialize the Gemfile 44 | # 45 | # @return [undefined] 46 | # 47 | # @api private 48 | def init_gemfile 49 | gemfile = root.join(DEFAULT_GEMFILE_NAME) 50 | unless gemfile.file? && gemfile.read.include?(EVAL_GEMFILE) 51 | gemfile.open('a') do |file| 52 | file << ANNOTATION_WRAPPER % EVAL_GEMFILE 53 | end 54 | end 55 | end 56 | 57 | # Initialize the Rakefile 58 | # 59 | # @return [undefined] 60 | # 61 | # @api private 62 | def init_rakefile 63 | rakefile = root.join(RAKE_FILE_NAME) 64 | unless rakefile.file? && rakefile.read.include?(INIT_RAKE_TASKS) 65 | rakefile.open('a') do |file| 66 | file << ANNOTATION_WRAPPER % [REQUIRE, INIT_RAKE_TASKS].join("\n") 67 | end 68 | end 69 | end 70 | 71 | end # class Initializer 72 | end # class Site 73 | end # module Devtools 74 | -------------------------------------------------------------------------------- /shared/Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | group :development do 4 | gem 'rake', '~> 10.1.0' 5 | gem 'rspec', '~> 2.14.1' 6 | gem 'yard', '~> 0.8.7' 7 | 8 | platform :rbx do 9 | gem 'rubysl-singleton', '~> 2.0.0' 10 | end 11 | end 12 | 13 | group :yard do 14 | gem 'kramdown', '~> 1.2.0' 15 | end 16 | 17 | group :guard do 18 | gem 'guard', '~> 2.2.4' 19 | gem 'guard-bundler', '~> 2.0.0' 20 | gem 'guard-rspec', '~> 4.0.4' 21 | gem 'guard-rubocop', '~> 1.0.0' 22 | 23 | # file system change event handling 24 | gem 'listen', '~> 2.2.0' 25 | gem 'rb-fchange', '~> 0.0.6', require: false 26 | gem 'rb-fsevent', '~> 0.9.3', require: false 27 | gem 'rb-inotify', '~> 0.9.0', require: false 28 | 29 | # notification handling 30 | gem 'libnotify', '~> 0.8.0', require: false 31 | gem 'rb-notifu', '~> 0.0.4', require: false 32 | gem 'terminal-notifier-guard', '~> 1.5.3', require: false 33 | end 34 | 35 | group :metrics do 36 | gem 'coveralls', '~> 0.7.0' 37 | gem 'flay', '~> 2.4.0' 38 | gem 'flog', '~> 4.2.0' 39 | gem 'reek', '~> 1.3.2' 40 | gem 'rubocop', '~> 0.15.0' 41 | gem 'simplecov', '~> 0.8.2' 42 | gem 'yardstick', '~> 0.9.7', git: 'https://github.com/dkubb/yardstick.git' 43 | 44 | platforms :ruby_19, :ruby_20 do 45 | gem 'mutant', '~> 0.3.0.rc3', git: 'https://github.com/mbj/mutant.git' 46 | gem 'unparser', '~> 0.1.5', git: 'https://github.com/mbj/unparser.git' 47 | gem 'yard-spellcheck', '~> 0.1.5' 48 | end 49 | 50 | platform :rbx do 51 | gem 'json', '~> 1.8.1' 52 | gem 'racc', '~> 1.4.10' 53 | gem 'rubysl-logger', '~> 2.0.0' 54 | gem 'rubysl-open-uri', '~> 2.0.0' 55 | gem 'rubysl-prettyprint', '~> 2.0.2' 56 | end 57 | end 58 | 59 | group :benchmarks do 60 | gem 'rbench', '~> 0.2.3' 61 | end 62 | 63 | platform :jruby do 64 | group :jruby do 65 | gem 'jruby-openssl', '~> 0.8.5' 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /config/rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Includes: 3 | - '**/*.rake' 4 | - 'Gemfile' 5 | - 'Gemfile.devtools' 6 | Excludes: 7 | - '**/vendor/**' 8 | - '**/benchmarks/**' 9 | 10 | # Avoid parameter lists longer than five parameters. 11 | ParameterLists: 12 | Max: 3 13 | CountKeywordArgs: true 14 | 15 | MethodLength: 16 | CountComments: false 17 | Max: 15 18 | 19 | # Avoid more than `Max` levels of nesting. 20 | BlockNesting: 21 | Max: 3 22 | 23 | # Align with the style guide. 24 | CollectionMethods: 25 | PreferredMethods: 26 | collect: 'map' 27 | inject: 'reduce' 28 | find: 'detect' 29 | find_all: 'select' 30 | 31 | # Do not force public/protected/private keyword to be indented at the same 32 | # level as the def keyword. My personal preference is to outdent these keywords 33 | # because I think when scanning code it makes it easier to identify the 34 | # sections of code and visually separate them. When the keyword is at the same 35 | # level I think it sort of blends in with the def keywords and makes it harder 36 | # to scan the code and see where the sections are. 37 | AccessModifierIndentation: 38 | Enabled: false 39 | 40 | # Limit line length 41 | LineLength: 42 | Max: 113 # TODO: lower to 79 once the rubocop branch in shared/Gemfile is removed 43 | 44 | # Disable documentation checking until a class needs to be documented once 45 | Documentation: 46 | Enabled: false 47 | 48 | # Do not favor modifier if/unless usage when you have a single-line body 49 | IfUnlessModifier: 50 | Enabled: false 51 | 52 | # Allow case equality operator (in limited use within the specs) 53 | CaseEquality: 54 | Enabled: false 55 | 56 | # Constants do not always have to use SCREAMING_SNAKE_CASE 57 | ConstantName: 58 | Enabled: false 59 | 60 | # Not all trivial readers/writers can be defined with attr_* methods 61 | TrivialAccessors: 62 | Enabled: false 63 | 64 | # Do not prefer do/end over {} for multiline blocks 65 | Blocks: 66 | Enabled: false 67 | 68 | # Allow empty lines around body 69 | EmptyLinesAroundBody: 70 | Enabled: false 71 | -------------------------------------------------------------------------------- /tasks/metrics/flay.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | namespace :metrics do 4 | begin 5 | require 'flay' 6 | 7 | project = Devtools.project 8 | config = project.flay 9 | 10 | compatible_scores = %w(mri-1.9.3 mri-2.0.0) 11 | 12 | if ! compatible_scores.include?(Devtools.rvm) 13 | task :flay do 14 | $stderr.puts "Flay is disabled under #{Devtools.rvm}" 15 | end 16 | elsif config.enabled? 17 | # Original code by Marty Andrews: 18 | # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html 19 | desc 'Measure code duplication' 20 | task :flay do 21 | threshold = config.threshold 22 | total_score = config.total_score 23 | files = Flay.expand_dirs_to_files(project.lib_dir).sort 24 | 25 | # Run flay first to ensure the max mass matches the threshold 26 | flay = Flay.new(fuzzy: false, verbose: false, mass: 0) 27 | flay.process(*files) 28 | flay.analyze 29 | 30 | masses = flay.masses.map do |hash, mass| 31 | Rational(mass, flay.hashes[hash].size) 32 | end 33 | 34 | max = (masses.max || 0).to_i 35 | unless max >= threshold 36 | Devtools.notify "Adjust flay threshold down to #{max}" 37 | end 38 | 39 | total = masses.inject(:+).to_i 40 | unless total == total_score 41 | Devtools.notify "Flay total is now #{total}, but expected #{total_score}" 42 | end 43 | 44 | # Run flay a second time with the threshold set 45 | flay = Flay.new(fuzzy: false, verbose: false, mass: threshold.succ) 46 | flay.process(*files) 47 | flay.analyze 48 | 49 | mass_size = flay.masses.size 50 | 51 | if mass_size.nonzero? 52 | flay.report 53 | Devtools.notify "#{mass_size} chunks have a duplicate mass > #{threshold}" 54 | end 55 | end 56 | else 57 | task :flay do 58 | $stderr.puts 'Flay is disabled' 59 | end 60 | end 61 | rescue LoadError 62 | task :flay do 63 | $stderr.puts 'In order to run flay, you must: gem install flay' 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /default/config/reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Attribute: 3 | enabled: true 4 | exclude: [] 5 | BooleanParameter: 6 | enabled: true 7 | exclude: [] 8 | ClassVariable: 9 | enabled: true 10 | exclude: [] 11 | ControlParameter: 12 | enabled: true 13 | exclude: [] 14 | DataClump: 15 | enabled: true 16 | exclude: [] 17 | max_copies: 2 18 | min_clump_size: 2 19 | DuplicateMethodCall: 20 | enabled: true 21 | exclude: [] 22 | max_calls: 1 23 | allow_calls: [] 24 | FeatureEnvy: 25 | enabled: true 26 | exclude: [] 27 | IrresponsibleModule: 28 | enabled: true 29 | exclude: [] 30 | LongParameterList: 31 | enabled: true 32 | exclude: [] 33 | max_params: 2 34 | overrides: 35 | initialize: 36 | max_params: 3 37 | LongYieldList: 38 | enabled: true 39 | exclude: [] 40 | max_params: 2 41 | NestedIterators: 42 | enabled: true 43 | exclude: [] 44 | max_allowed_nesting: 1 45 | ignore_iterators: [] 46 | NilCheck: 47 | enabled: true 48 | exclude: [] 49 | RepeatedConditional: 50 | enabled: true 51 | exclude: [] 52 | max_ifs: 1 53 | TooManyInstanceVariables: 54 | enabled: true 55 | exclude: [] 56 | max_instance_variables: 3 57 | TooManyMethods: 58 | enabled: true 59 | exclude: [] 60 | max_methods: 10 61 | TooManyStatements: 62 | enabled: true 63 | exclude: 64 | - each 65 | max_statements: 2 66 | UncommunicativeMethodName: 67 | enabled: true 68 | exclude: [] 69 | reject: 70 | - !ruby/regexp /^[a-z]$/ 71 | - !ruby/regexp /[0-9]$/ 72 | - !ruby/regexp /[A-Z]/ 73 | accept: [] 74 | UncommunicativeModuleName: 75 | enabled: true 76 | exclude: [] 77 | reject: 78 | - !ruby/regexp /^.$/ 79 | - !ruby/regexp /[0-9]$/ 80 | accept: [] 81 | UncommunicativeParameterName: 82 | enabled: true 83 | exclude: [] 84 | reject: 85 | - !ruby/regexp /^.$/ 86 | - !ruby/regexp /[0-9]$/ 87 | - !ruby/regexp /[A-Z]/ 88 | accept: [] 89 | UncommunicativeVariableName: 90 | enabled: true 91 | exclude: [] 92 | reject: 93 | - !ruby/regexp /^.$/ 94 | - !ruby/regexp /[0-9]$/ 95 | - !ruby/regexp /[A-Z]/ 96 | accept: [] 97 | UnusedParameters: 98 | enabled: true 99 | exclude: [] 100 | UtilityFunction: 101 | enabled: true 102 | exclude: [] 103 | max_helper_calls: 0 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devtools 2 | 3 | [![Build Status](https://secure.travis-ci.org/rom-rb/devtools.png?branch=master)](http://travis-ci.org/rom-rb/devtools) 4 | [![Dependency Status](https://gemnasium.com/rom-rb/devtools.png)](https://gemnasium.com/rom-rb/devtools) 5 | [![Code Climate](https://codeclimate.com/github/datamapper/devtools.png)](https://codeclimate.com/github/datamapper/devtools) 6 | 7 | 8 | Metagem to assist [ROM](https://github.com/rom-rb)-style development. 9 | Used to centralize metric setup and development gem dependencies. 10 | 11 | ## Installation 12 | 13 | The installation looks stupid because Gemfiles are not nestable (A Gemfile cannot 14 | include another Gemfile from a remote repository). Because of this we use an 15 | updatable local copy of the shared parts. 16 | 17 | Add the git source to your Gemfile's development section: 18 | 19 | ```ruby 20 | group :development, :test do 21 | gem 'devtools', git: 'https://github.com/rom-rb/devtools.git' 22 | end 23 | ``` 24 | 25 | To initialize devtools in a project run the following command: 26 | 27 | ```ruby 28 | bundle install 29 | bundle exec devtools init 30 | ``` 31 | 32 | This will *change your Gemfile and Rakefile* and add config files. Make sure to 33 | review the diff and don't freak out :wink: 34 | 35 | ## Updating 36 | 37 | Later on if you want to update to the latest devtools just run: 38 | 39 | ``` 40 | bundle update devtools 41 | bundle exec devtools sync 42 | bundle install 43 | ``` 44 | 45 | ## RSpec support 46 | 47 | If you're using RSpec and want to have access to our common setup just adjust 48 | `spec/spec_helper.rb` to include 49 | 50 | ```ruby 51 | require 'devtools/spec_helper' 52 | ``` 53 | 54 | ## Credits 55 | 56 | The whole [ROM](https://github.com/rom-rb) team that created and maintained all 57 | these tasks before they were centralized here. 58 | 59 | ## Contributing 60 | 61 | * Fork the project. 62 | * Make your feature addition or bug fix. 63 | * Add tests for it. This is important so I don't break it in a 64 | future version unintentionally. 65 | * Commit, do not mess with Rakefile or version 66 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 67 | * Send me a pull request. Bonus points for topic branches. 68 | 69 | ## License 70 | 71 | See `LICENSE` file. 72 | -------------------------------------------------------------------------------- /config/reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Attribute: 3 | enabled: false 4 | exclude: [] 5 | BooleanParameter: 6 | enabled: true 7 | exclude: [] 8 | ClassVariable: 9 | enabled: true 10 | exclude: [] 11 | ControlParameter: 12 | enabled: true 13 | exclude: [] 14 | DataClump: 15 | enabled: true 16 | exclude: [] 17 | max_copies: 0 18 | min_clump_size: 1 19 | DuplicateMethodCall: 20 | enabled: true 21 | exclude: [] 22 | max_calls: 1 23 | allow_calls: [] 24 | FeatureEnvy: 25 | enabled: true 26 | exclude: 27 | - Devtools::Site::Initializer#init_gemfile 28 | - Devtools::Site::Initializer#init_rakefile 29 | IrresponsibleModule: 30 | enabled: true 31 | exclude: [] 32 | LongParameterList: 33 | enabled: true 34 | exclude: [] 35 | max_params: 2 36 | overrides: {} 37 | LongYieldList: 38 | enabled: true 39 | exclude: [] 40 | max_params: 0 41 | NestedIterators: 42 | enabled: true 43 | exclude: [] 44 | max_allowed_nesting: 1 45 | ignore_iterators: [] 46 | NilCheck: 47 | enabled: true 48 | exclude: [] 49 | RepeatedConditional: 50 | enabled: true 51 | exclude: [] 52 | max_ifs: 1 53 | TooManyInstanceVariables: 54 | enabled: true 55 | exclude: 56 | - Devtools::Project 57 | max_instance_variables: 14 # no adamantium ;) 58 | TooManyMethods: 59 | enabled: true 60 | exclude: [] 61 | max_methods: 15 62 | TooManyStatements: 63 | enabled: true 64 | exclude: 65 | - Devtools::Project#initialize # needs refactoring, it is way too large 66 | max_statements: 5 67 | UncommunicativeMethodName: 68 | enabled: true 69 | exclude: [] 70 | reject: 71 | - !ruby/regexp /^[a-z]$/ 72 | - !ruby/regexp /[0-9]$/ 73 | - !ruby/regexp /[A-Z]/ 74 | accept: [] 75 | UncommunicativeModuleName: 76 | enabled: true 77 | exclude: [] 78 | reject: 79 | - !ruby/regexp /^.$/ 80 | - !ruby/regexp /[0-9]$/ 81 | accept: [] 82 | UncommunicativeParameterName: 83 | enabled: true 84 | exclude: [] 85 | reject: 86 | - !ruby/regexp /^.$/ 87 | - !ruby/regexp /[0-9]$/ 88 | - !ruby/regexp /[A-Z]/ 89 | accept: [] 90 | UncommunicativeVariableName: 91 | enabled: true 92 | exclude: [] 93 | reject: 94 | - !ruby/regexp /^.$/ 95 | - !ruby/regexp /[0-9]$/ 96 | - !ruby/regexp /[A-Z]/ 97 | accept: [] 98 | UnusedParameters: 99 | enabled: true 100 | exclude: [] 101 | UtilityFunction: 102 | enabled: true 103 | exclude: 104 | - Devtools::Platform#ruby18? 105 | - Devtools::Platform#ruby19? 106 | - Devtools::Platform#ruby20? 107 | max_helper_calls: 0 108 | -------------------------------------------------------------------------------- /lib/devtools/platform.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | 5 | # Provides methods to determine the ruby platform 6 | module Platform 7 | 8 | DEFAULT_RVM_NAME = 'mri'.freeze 9 | 10 | # Return Ruby engine string 11 | # 12 | # @return [String] 13 | # 14 | # @api private 15 | def ruby_engine 16 | @ruby_engine ||= (defined?(RUBY_ENGINE) && RUBY_ENGINE || 'ruby').freeze 17 | end 18 | 19 | # Return RVM name 20 | # 21 | # @return [String] 22 | # 23 | # @api private 24 | def rvm_name 25 | @rvm_name ||= begin 26 | engine = ruby_engine 27 | engine == 'ruby' ? DEFAULT_RVM_NAME : engine 28 | end 29 | end 30 | 31 | # Return RVM string 32 | # 33 | # @return [String] 34 | # 35 | # @api private 36 | def rvm 37 | @rvm ||= "#{rvm_name}-#{RUBY_VERSION}".freeze 38 | end 39 | 40 | # Test for being executed under JRuby 41 | # 42 | # @return [true] 43 | # if running under JRuby 44 | # 45 | # @return [false] 46 | # otherwise 47 | # 48 | # @api private 49 | def jruby? 50 | ruby_engine == 'jruby' 51 | end 52 | 53 | # Test for being executed under rbx 54 | # 55 | # @return [true] 56 | # if running under rbx 57 | # 58 | # @return [false] 59 | # otherwise 60 | # 61 | # @api private 62 | def rbx? 63 | ruby_engine == 'rbx' 64 | end 65 | 66 | # Test for being executed under rubies with a JIT 67 | # 68 | # @return [true] 69 | # if running under JRuby or rbx 70 | # 71 | # @return [false] 72 | # otherwise 73 | # 74 | # @api private 75 | def jit? 76 | jruby? || rbx? 77 | end 78 | 79 | # Test for 1.8 mode 80 | # 81 | # @return [true] 82 | # if running under 1.8.x 83 | # 84 | # @return [false] 85 | # otherwise 86 | # 87 | # @api private 88 | def ruby18? 89 | RUBY_VERSION.start_with?('1.8.') 90 | end 91 | 92 | # Test for 1.9 mode 93 | # 94 | # @return [true] 95 | # if running under 1.9.x 96 | # 97 | # @return [false] 98 | # otherwise 99 | # 100 | # @api private 101 | def ruby19? 102 | RUBY_VERSION.start_with?('1.9.') 103 | end 104 | 105 | # Test for 2.0 mode 106 | # 107 | # @return [true] 108 | # if running under 2.0.x 109 | # 110 | # @return [false] 111 | # otherwise 112 | # 113 | # @api private 114 | def ruby20? 115 | RUBY_VERSION.start_with?('2.0.') 116 | end 117 | end # module Platform 118 | end # module Devtools 119 | -------------------------------------------------------------------------------- /lib/devtools/project/initializer/rspec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | class Project 5 | class Initializer 6 | 7 | # Requires all shared specs in a project's spec_helper 8 | # Also installs a configurable unit test timeout 9 | class Rspec < self 10 | 11 | def self.require_files(directory) 12 | Devtools.require_files(directory, SHARED_SPEC_PATTERN) 13 | end 14 | 15 | # Initialize RSpec for +project+ 16 | # 17 | # @param [Project] project 18 | # the project to initialize 19 | # 20 | # @return [Rspec] 21 | # 22 | # @api private 23 | def self.call(project) 24 | new(project).call 25 | end 26 | 27 | # The spec root 28 | # 29 | # @return [Pathname] 30 | # 31 | # @api private 32 | attr_reader :spec_root 33 | private :spec_root 34 | 35 | # The unit test timeout 36 | # 37 | # @return [Numeric] 38 | # 39 | # @api private 40 | attr_reader :unit_test_timeout 41 | private :unit_test_timeout 42 | 43 | # Initialize a new instance 44 | # 45 | # @param [Project] project 46 | # the project to initialize 47 | # 48 | # @param [Numeric] unit_test_timeout 49 | # the maximum time a single unit test can take 50 | # 51 | # @return [undefined] 52 | # 53 | # @api private 54 | def initialize(project) 55 | super 56 | @spec_root = project.spec_root 57 | @unit_test_timeout = project.unit_test_timeout 58 | end 59 | 60 | # Setup RSpec for {#project} 61 | # 62 | # @return [self] 63 | # 64 | # @api private 65 | def call 66 | require 'rspec' 67 | require_shared_spec_files 68 | enable_unit_test_timeout unless Devtools.jit? 69 | self 70 | end 71 | 72 | private 73 | 74 | # Timeout unit tests that take longer than 1/10th of a second 75 | # 76 | # @param [Numeric] timeout 77 | # 78 | # @return [undefined] 79 | # 80 | # @raise [Timeout::Error] 81 | # raised when the times are outside the timeout 82 | # 83 | # @api private 84 | # 85 | def enable_unit_test_timeout 86 | timeout = unit_test_timeout # support the closure 87 | RSpec.configuration.around file_path: UNIT_TEST_PATH_REGEXP do |example| 88 | Timeout.timeout(timeout, &example) 89 | end 90 | end 91 | 92 | def require_shared_spec_files 93 | require_files(SHARED_SPEC_PATH) 94 | require_files(spec_root) 95 | end 96 | 97 | def require_files(directory) 98 | self.class.require_files(directory) 99 | end 100 | 101 | end # class Rspec 102 | end # class Initializer 103 | end # class Project 104 | end # module Devtools 105 | -------------------------------------------------------------------------------- /lib/devtools/project.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | 5 | # The project devtools supports 6 | class Project 7 | 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 21 | 22 | # The flog configuration 23 | # 24 | # @return [Config::Flog] 25 | # 26 | # @api private 27 | attr_reader :flog 28 | 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 shared gemfile path 66 | # 67 | # @return [Pathname] 68 | # 69 | # @api private 70 | attr_reader :shared_gemfile_path 71 | 72 | # The default config path 73 | # 74 | # @return [Pathname] 75 | # 76 | # @api private 77 | attr_reader :default_config_path 78 | 79 | # The lib directory 80 | # 81 | # @return [Pathname] 82 | # 83 | # @api private 84 | attr_reader :lib_dir 85 | 86 | # The Ruby file pattern 87 | # 88 | # @return [Pathname] 89 | # 90 | # @api private 91 | attr_reader :file_pattern 92 | 93 | # The spec root 94 | # 95 | # @return [Pathname] 96 | # 97 | # @api private 98 | attr_reader :spec_root 99 | 100 | # Return config directory 101 | # 102 | # @return [Pathname] 103 | # 104 | # @api private 105 | attr_reader :config_dir 106 | 107 | # The unit test timeout 108 | # 109 | # @return [Numeric] 110 | # 111 | # @api private 112 | attr_reader :unit_test_timeout 113 | 114 | # Initialize object 115 | # 116 | # @param [Pathname] root 117 | # 118 | # @return [undefined] 119 | # 120 | # @api private 121 | # 122 | def initialize(root) 123 | @root = root 124 | 125 | @shared_gemfile_path = @root.join(GEMFILE_NAME).freeze 126 | @default_config_path = @root.join(DEFAULT_CONFIG_DIR_NAME).freeze 127 | @lib_dir = @root.join(LIB_DIRECTORY_NAME).freeze 128 | @spec_root = @root.join(SPEC_DIRECTORY_NAME).freeze 129 | @file_pattern = @lib_dir.join(RB_FILE_PATTERN).freeze 130 | @config_dir = @default_config_path 131 | 132 | @reek = Config::Reek.new(self) 133 | @rubocop = Config::Rubocop.new(self) 134 | @flog = Config::Flog.new(self) 135 | @yardstick = Config::Yardstick.new(self) 136 | @flay = Config::Flay.new(self) 137 | @mutant = Config::Mutant.new(self) 138 | @devtools = Config::Devtools.new(self) 139 | 140 | @unit_test_timeout = @devtools.unit_test_timeout 141 | end 142 | 143 | end # class Project 144 | end # module Devtools 145 | -------------------------------------------------------------------------------- /lib/devtools/config.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Devtools 4 | MASTER_BRANCH = 'master'.freeze 5 | 6 | # Abstract base class of tool configuration 7 | class Config 8 | # Represent no configuration 9 | DEFAULT_CONFIG = {}.freeze 10 | 11 | # Declare an attribute 12 | # 13 | # @param [Symbol] name 14 | # 15 | # @yieldreturn [Object] 16 | # the default value to use 17 | # 18 | # @api private 19 | # 20 | # @return [self] 21 | # 22 | def self.attribute(name, *default) 23 | define_method(name) do 24 | raw.fetch(name.to_s, *default) 25 | end 26 | end 27 | private_class_method :attribute 28 | 29 | # Return project 30 | # 31 | # @return [Project] 32 | # 33 | # @api private 34 | # 35 | attr_reader :project 36 | 37 | # Initialize object 38 | # 39 | # @return [Project] 40 | # 41 | # @api private 42 | # 43 | def initialize(project) 44 | @project = project 45 | end 46 | 47 | # Return config path 48 | # 49 | # @return [String] 50 | # 51 | # @api private 52 | # 53 | def config_file 54 | @config_file ||= project.config_dir.join(self.class::FILE).freeze 55 | end 56 | 57 | # Test if the task is enabled 58 | # 59 | # If there is no config file, and no sensible defaults, then the rake task 60 | # should become disabled. 61 | # 62 | # @return [Boolean] 63 | # 64 | # @api private 65 | # 66 | def enabled? 67 | !raw.equal?(DEFAULT_CONFIG) 68 | end 69 | 70 | private 71 | 72 | # Return raw data 73 | # 74 | # @return [Hash] 75 | # 76 | # @api private 77 | # 78 | def raw 79 | @raw ||= yaml_config || DEFAULT_CONFIG 80 | end 81 | 82 | # Return the raw config data from a yaml file 83 | # 84 | # @return [Hash] 85 | # returned if the yaml file is found 86 | # @return [nil] 87 | # returned if the yaml file is not found 88 | # 89 | # @api private 90 | # 91 | def yaml_config 92 | config_file = self.config_file 93 | YAML.load_file(config_file).freeze if config_file.file? 94 | end 95 | 96 | # Rubocop configuration 97 | class Rubocop < self 98 | FILE = 'rubocop.yml'.freeze 99 | end 100 | 101 | # Reek configuration 102 | class Reek < self 103 | FILE = 'reek.yml'.freeze 104 | end 105 | 106 | # Flay configuration 107 | class Flay < self 108 | FILE = 'flay.yml'.freeze 109 | 110 | attribute :total_score 111 | attribute :threshold 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 | # 129 | # @api private 130 | def options 131 | OPTIONS.each_with_object({}) { |name, hash| 132 | hash[name] = raw.fetch(name, nil) 133 | } 134 | end 135 | end 136 | 137 | # Flog configuration 138 | class Flog < self 139 | FILE = 'flog.yml'.freeze 140 | 141 | attribute :total_score 142 | attribute :threshold 143 | end 144 | 145 | # Mutant configuration 146 | class Mutant < self 147 | FILE = 'mutant.yml'.freeze 148 | DEFAULT_NAME = ''.freeze 149 | DEFAULT_STRATEGY = '--rspec'.freeze 150 | 151 | attribute :name, DEFAULT_NAME 152 | attribute :namespace 153 | attribute :strategy, DEFAULT_STRATEGY 154 | end 155 | 156 | # Devtools configuration 157 | class Devtools < self 158 | FILE = 'devtools.yml'.freeze 159 | DEFAULT_UNIT_TEST_TIMEOUT = 0.1 # 100ms 160 | DEFAULT_BRANCHES_TO_FAIL_ON = [MASTER_BRANCH] 161 | 162 | attribute :unit_test_timeout, DEFAULT_UNIT_TEST_TIMEOUT 163 | attribute :fail_on_branch, DEFAULT_BRANCHES_TO_FAIL_ON 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/devtools.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'pathname' 4 | require 'rake' 5 | require 'timeout' 6 | require 'yaml' 7 | require 'fileutils' 8 | 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' 17 | 18 | # Provides access to metric tools 19 | module Devtools 20 | 21 | extend Platform 22 | 23 | ROOT = Pathname.new(__FILE__).parent.parent.freeze 24 | PROJECT_ROOT = Pathname.pwd.freeze 25 | SHARED_PATH = ROOT.join('shared').freeze 26 | SHARED_SPEC_PATH = SHARED_PATH.join('spec').freeze 27 | SHARED_GEMFILE_PATH = SHARED_PATH.join('Gemfile').freeze 28 | DEFAULT_CONFIG_PATH = ROOT.join('default/config').freeze 29 | RAKE_FILES_GLOB = ROOT.join('tasks/**/*.rake').to_s.freeze 30 | LIB_DIRECTORY_NAME = 'lib'.freeze 31 | SPEC_DIRECTORY_NAME = 'spec'.freeze 32 | RB_FILE_PATTERN = '**/*.rb'.freeze 33 | RAKE_FILE_NAME = 'Rakefile'.freeze 34 | DEFAULT_GEMFILE_NAME = 'Gemfile'.freeze 35 | GEMFILE_NAME = 'Gemfile.devtools'.freeze 36 | EVAL_GEMFILE = "eval_gemfile '#{GEMFILE_NAME}'".freeze 37 | BUNDLE_UPDATE = 'bundle update'.freeze 38 | REQUIRE = "require 'devtools'".freeze 39 | INIT_RAKE_TASKS = 'Devtools.init_rake_tasks'.freeze 40 | SHARED_SPEC_PATTERN = '{shared,support}/**/*.rb'.freeze 41 | UNIT_TEST_PATH_REGEXP = %r{\bspec/unit/}.freeze 42 | DEFAULT_CONFIG_DIR_NAME = 'config'.freeze 43 | ANNOTATION_WRAPPER = "\n# Added by devtools\n%s".freeze 44 | 45 | # Provides devtools for a project 46 | SITE = Site.new(Project.new(PROJECT_ROOT)) 47 | 48 | # Initialize project and load tasks 49 | # 50 | # Should *only* be called from your $application_root/Rakefile 51 | # 52 | # @return [self] 53 | # 54 | # @api public 55 | def self.init_rake_tasks 56 | Project::Initializer::Rake.call 57 | self 58 | end 59 | 60 | # Initialize project and load shared specs 61 | # 62 | # Expects to be called from $application_root/spec/spec_helper.rb 63 | # 64 | # @return [self] 65 | # 66 | # @api public 67 | def self.init_spec_helper 68 | SITE.init_spec_helper 69 | self 70 | end 71 | 72 | # Initialize devtools using default config 73 | # 74 | # @return [undefined] 75 | # 76 | # @api public 77 | def self.init 78 | SITE.init 79 | self 80 | end 81 | 82 | # Sync Gemfile.devtools 83 | # 84 | # @return [undefined] 85 | # 86 | # @api public 87 | def self.sync 88 | SITE.sync 89 | end 90 | 91 | # Sync Gemfile.devtools and run bundle update 92 | # 93 | # @return [undefined] 94 | # 95 | # @api public 96 | def self.update 97 | SITE.update 98 | end 99 | 100 | # Return project 101 | # 102 | # @return [Project] 103 | # 104 | # @api private 105 | def self.project 106 | SITE.project 107 | end 108 | 109 | # Require shared examples 110 | # 111 | # @param [Pathname] dir 112 | # the directory containing the files to require 113 | # 114 | # @param [String] pattern 115 | # the file pattern to match inside directory 116 | # 117 | # @return [self] 118 | # 119 | # @api private 120 | def self.require_files(dir, pattern) 121 | Dir[dir.join(pattern)].each { |file| require file } 122 | self 123 | end 124 | 125 | # Notify or abort depending on the branch 126 | # 127 | # @param [String] msg 128 | # 129 | # @return [undefined] 130 | # 131 | # @api private 132 | def self.notify(msg) 133 | fail_on_current_branch? ? abort(msg) : puts(msg) 134 | end 135 | 136 | # Test if the build should fail because of metrics on this branch 137 | # 138 | # @return [Boolean] 139 | # 140 | # @api private 141 | def self.fail_on_current_branch? 142 | fail_on_branch.include?(current_branch) 143 | end 144 | 145 | # Return the branches the build should fail on because of metrics 146 | # 147 | # @return [Array[String]] 148 | # 149 | # @api private 150 | def self.fail_on_branch 151 | project.devtools.fail_on_branch 152 | end 153 | 154 | # Return current git branch 155 | # 156 | # @return [String] 157 | # 158 | # @api private 159 | def self.current_branch 160 | ENV['TRAVIS_BRANCH'] || `git rev-parse --abbrev-ref HEAD`.rstrip 161 | end 162 | private_class_method :current_branch 163 | 164 | end # module Devtools 165 | --------------------------------------------------------------------------------