From 443fdc93f1360c74eecff52378bb7d0c860f640b Mon Sep 17 00:00:00 2001 From: Alex Kotov Date: Sun, 17 Jan 2021 04:07:52 +0500 Subject: [PATCH] Move dependency "devtools" to the repo --- Gemfile | 2 + Gemfile.lock | 80 +++---- devtools/.circleci/config.yml | 41 ++++ devtools/.gitignore | 37 ++++ devtools/.rspec | 6 + devtools/.rubocop.yml | 4 + devtools/Gemfile | 3 + devtools/LICENSE | 20 ++ devtools/README.md | 47 +++++ devtools/Rakefile | 5 + devtools/config/devtools.yml | 2 + devtools/config/flay.yml | 3 + devtools/config/flog.yml | 2 + devtools/config/mutant.yml | 4 + devtools/config/reek.yml | 106 ++++++++++ devtools/config/rubocop.yml | 151 ++++++++++++++ devtools/config/yardstick.yml | 2 + devtools/devtools.gemspec | 36 ++++ devtools/lib/devtools.rb | 96 +++++++++ devtools/lib/devtools/config.rb | 180 ++++++++++++++++ devtools/lib/devtools/flay.rb | 98 +++++++++ devtools/lib/devtools/project.rb | 80 +++++++ devtools/lib/devtools/project/initializer.rb | 11 + .../lib/devtools/project/initializer/rake.rb | 23 ++ .../lib/devtools/project/initializer/rspec.rb | 80 +++++++ devtools/lib/devtools/rake/flay.rb | 155 ++++++++++++++ devtools/lib/devtools/spec_helper.rb | 3 + .../spec/shared/abstract_type_behavior.rb | 16 ++ .../spec/shared/command_method_behavior.rb | 5 + .../spec/shared/each_method_behaviour.rb | 13 ++ .../spec/shared/idempotent_method_behavior.rb | 11 + .../shared/spec/support/ice_nine_config.rb | 12 ++ .../devtools/rake/flay/verify_spec.rb | 196 ++++++++++++++++++ devtools/spec/spec_helper.rb | 24 +++ .../unit/devtools/config/yardstick_spec.rb | 17 ++ devtools/spec/unit/devtools/config_spec.rb | 78 +++++++ .../unit/devtools/flay/file_list/call_spec.rb | 19 ++ .../devtools/flay/scale/flay_report_spec.rb | 17 ++ .../unit/devtools/flay/scale/measure_spec.rb | 43 ++++ .../devtools/project/initializer/rake_spec.rb | 21 ++ .../project/initializer/rspec_spec.rb | 52 +++++ devtools/spec/unit/devtools/project_spec.rb | 35 ++++ devtools/spec/unit/devtools_spec.rb | 14 ++ devtools/tasks/metrics/ci.rake | 17 ++ devtools/tasks/metrics/coverage.rake | 13 ++ devtools/tasks/metrics/flay.rake | 21 ++ devtools/tasks/metrics/flog.rake | 43 ++++ devtools/tasks/metrics/mutant.rake | 43 ++++ devtools/tasks/metrics/reek.rake | 8 + devtools/tasks/metrics/rubocop.rake | 13 ++ devtools/tasks/metrics/yardstick.rake | 12 ++ devtools/tasks/spec.rake | 34 +++ devtools/tasks/yard.rake | 9 + mutant.gemspec | 1 - 54 files changed, 2025 insertions(+), 39 deletions(-) create mode 100644 devtools/.circleci/config.yml create mode 100644 devtools/.gitignore create mode 100644 devtools/.rspec create mode 100644 devtools/.rubocop.yml create mode 100644 devtools/Gemfile create mode 100644 devtools/LICENSE create mode 100644 devtools/README.md create mode 100644 devtools/Rakefile create mode 100644 devtools/config/devtools.yml create mode 100644 devtools/config/flay.yml create mode 100644 devtools/config/flog.yml create mode 100644 devtools/config/mutant.yml create mode 100644 devtools/config/reek.yml create mode 100644 devtools/config/rubocop.yml create mode 100644 devtools/config/yardstick.yml create mode 100644 devtools/devtools.gemspec create mode 100644 devtools/lib/devtools.rb create mode 100644 devtools/lib/devtools/config.rb create mode 100644 devtools/lib/devtools/flay.rb create mode 100644 devtools/lib/devtools/project.rb create mode 100644 devtools/lib/devtools/project/initializer.rb create mode 100644 devtools/lib/devtools/project/initializer/rake.rb create mode 100644 devtools/lib/devtools/project/initializer/rspec.rb create mode 100644 devtools/lib/devtools/rake/flay.rb create mode 100644 devtools/lib/devtools/spec_helper.rb create mode 100644 devtools/shared/spec/shared/abstract_type_behavior.rb create mode 100644 devtools/shared/spec/shared/command_method_behavior.rb create mode 100644 devtools/shared/spec/shared/each_method_behaviour.rb create mode 100644 devtools/shared/spec/shared/idempotent_method_behavior.rb create mode 100644 devtools/shared/spec/support/ice_nine_config.rb create mode 100644 devtools/spec/integration/devtools/rake/flay/verify_spec.rb create mode 100644 devtools/spec/spec_helper.rb create mode 100644 devtools/spec/unit/devtools/config/yardstick_spec.rb create mode 100644 devtools/spec/unit/devtools/config_spec.rb create mode 100644 devtools/spec/unit/devtools/flay/file_list/call_spec.rb create mode 100644 devtools/spec/unit/devtools/flay/scale/flay_report_spec.rb create mode 100644 devtools/spec/unit/devtools/flay/scale/measure_spec.rb create mode 100644 devtools/spec/unit/devtools/project/initializer/rake_spec.rb create mode 100644 devtools/spec/unit/devtools/project/initializer/rspec_spec.rb create mode 100644 devtools/spec/unit/devtools/project_spec.rb create mode 100644 devtools/spec/unit/devtools_spec.rb create mode 100644 devtools/tasks/metrics/ci.rake create mode 100644 devtools/tasks/metrics/coverage.rake create mode 100644 devtools/tasks/metrics/flay.rake create mode 100644 devtools/tasks/metrics/flog.rake create mode 100644 devtools/tasks/metrics/mutant.rake create mode 100644 devtools/tasks/metrics/reek.rake create mode 100644 devtools/tasks/metrics/rubocop.rake create mode 100644 devtools/tasks/metrics/yardstick.rake create mode 100644 devtools/tasks/spec.rake create mode 100644 devtools/tasks/yard.rake diff --git a/Gemfile b/Gemfile index 66fa43b0..87cc4a8d 100644 --- a/Gemfile +++ b/Gemfile @@ -4,4 +4,6 @@ source 'https://rubygems.org' gemspec name: 'mutant' +gem 'devtools', path: 'devtools' + eval_gemfile File.expand_path('Gemfile.shared', __dir__) diff --git a/Gemfile.lock b/Gemfile.lock index 9b48544d..0c8affed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,6 +20,29 @@ PATH mutant (~> 0.8.24) rspec-core (>= 3.4.0, < 4.0.0) +PATH + remote: devtools + specs: + devtools (0.1.22) + abstract_type (~> 0.0.7) + adamantium (~> 0.2.0) + anima (~> 0.3.0) + concord (~> 0.1.5) + flay (~> 2.12.0) + flog (~> 4.6.2) + mutant (~> 0.8.19) + mutant-rspec (~> 0.8.19) + procto (~> 0.0.3) + rake (~> 12.3.0) + reek (~> 5.2.0) + rspec (~> 3.8.0) + rspec-core (~> 3.8.0) + rspec-its (~> 1.2.0) + rubocop (~> 0.60.0) + simplecov (~> 0.16.1) + yard (~> 0.9.16) + yardstick (~> 0.9.9) + GEM remote: https://rubygems.org/ specs: @@ -45,41 +68,22 @@ GEM equalizer (~> 0.0.9) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devtools (0.1.22) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - anima (~> 0.3.0) - concord (~> 0.1.5) - flay (~> 2.12.0) - flog (~> 4.6.2) - mutant (~> 0.8.19) - mutant-rspec (~> 0.8.19) - procto (~> 0.0.3) - rake (~> 12.3.0) - reek (~> 5.2.0) - rspec (~> 3.8.0) - rspec-core (~> 3.8.0) - rspec-its (~> 1.2.0) - rubocop (~> 0.60.0) - simplecov (~> 0.16.1) - yard (~> 0.9.16) - yardstick (~> 0.9.9) diff-lcs (1.3) - docile (1.3.1) + docile (1.3.5) equalizer (0.0.11) erubis (2.7.0) - flay (2.12.0) + flay (2.12.1) erubis (~> 2.7.0) path_expander (~> 1.0) ruby_parser (~> 3.0) sexp_processor (~> 4.0) - flog (4.6.2) + flog (4.6.4) path_expander (~> 1.0) ruby_parser (~> 3.1, > 3.1.0) sexp_processor (~> 4.8) ice_nine (0.11.2) - jaro_winkler (1.5.1) - json (2.1.0) + jaro_winkler (1.5.4) + json (2.5.1) kwalify (0.7.2) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) @@ -95,11 +99,11 @@ GEM parallel (1.12.1) parser (2.5.3.0) ast (~> 2.4.0) - path_expander (1.0.3) - powerpack (0.1.2) + path_expander (1.1.0) + powerpack (0.1.3) procto (0.0.3) rainbow (3.0.0) - rake (12.3.1) + rake (12.3.3) reek (5.2.0) codeclimate-engine-rb (~> 0.4.0) kwalify (~> 0.7.0) @@ -110,18 +114,18 @@ GEM rspec-core (~> 3.8.0) rspec-expectations (~> 3.8.0) rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) + rspec-core (3.8.2) rspec-support (~> 3.8.0) - rspec-expectations (3.8.2) + rspec-expectations (3.8.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.8.0) rspec-its (1.2.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.8.0) + rspec-mocks (3.8.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.8.0) - rspec-support (3.8.0) + rspec-support (3.8.3) rubocop (0.60.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) @@ -130,17 +134,17 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.4.0) - ruby-progressbar (1.10.0) - ruby_parser (3.11.0) + ruby-progressbar (1.11.0) + ruby_parser (3.15.1) sexp_processor (~> 4.9) - sexp_processor (4.11.0) + sexp_processor (4.15.2) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) thread_safe (0.3.6) - unicode-display_width (1.4.0) + unicode-display_width (1.4.1) unparser (0.4.2) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) @@ -154,7 +158,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - yard (0.9.16) + yard (0.9.26) yardstick (0.9.9) yard (~> 0.8, >= 0.8.7.2) @@ -162,9 +166,9 @@ PLATFORMS ruby DEPENDENCIES - devtools (~> 0.1.22) + devtools! mutant! parallel (~> 1.3) BUNDLED WITH - 1.17.1 + 1.17.3 diff --git a/devtools/.circleci/config.yml b/devtools/.circleci/config.yml new file mode 100644 index 00000000..d3eb9477 --- /dev/null +++ b/devtools/.circleci/config.yml @@ -0,0 +1,41 @@ +defaults: &defaults + working_directory: ~/mutant + docker: + - image: circleci/ruby:2.5.3 +version: 2 +jobs: + unit_specs: + <<: *defaults + steps: + - checkout + - run: bundle install + - run: bundle exec rspec spec/unit + integration_specs: + <<: *defaults + steps: + - checkout + - run: bundle install + - run: bundle exec rspec spec/integration + metrics: + <<: *defaults + steps: + - checkout + - run: bundle install + - run: bundle exec rake metrics:rubocop + - run: bundle exec rake metrics:reek + - run: bundle exec rake metrics:flay + - run: bundle exec rake metrics:flog + mutant: + <<: *defaults + steps: + - checkout + - run: bundle install + - run: bundle exec rake metrics:mutant +workflows: + version: 2 + test: + jobs: + - unit_specs + - integration_specs + - metrics + - mutant diff --git a/devtools/.gitignore b/devtools/.gitignore new file mode 100644 index 00000000..1412f271 --- /dev/null +++ b/devtools/.gitignore @@ -0,0 +1,37 @@ +## MAC OS +.DS_Store + +## TEXTMATE +*.tmproj +tmtags + +## EMACS +*~ +\#* +.\#* + +## VIM +*.sw[op] + +## Rubinius +*.rbc +.rbx + +## PROJECT::GENERAL +*.gem +coverage +profiling +turbulence +rdoc +pkg +tmp +doc +log +.yardoc +measurements + +## BUNDLER +.bundle +Gemfile.lock + +## PROJECT::SPECIFIC diff --git a/devtools/.rspec b/devtools/.rspec new file mode 100644 index 00000000..5c7a944f --- /dev/null +++ b/devtools/.rspec @@ -0,0 +1,6 @@ +--color +--format progress +--profile +--warnings +--order random +--require spec_helper diff --git a/devtools/.rubocop.yml b/devtools/.rubocop.yml new file mode 100644 index 00000000..9d668b63 --- /dev/null +++ b/devtools/.rubocop.yml @@ -0,0 +1,4 @@ +AllCops: + DisplayCopNames: true + Include: + - 'Gemfile' diff --git a/devtools/Gemfile b/devtools/Gemfile new file mode 100644 index 00000000..fa75df15 --- /dev/null +++ b/devtools/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/devtools/LICENSE b/devtools/LICENSE new file mode 100644 index 00000000..aac7589d --- /dev/null +++ b/devtools/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Markus Schirp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/devtools/README.md b/devtools/README.md new file mode 100644 index 00000000..760f3e99 --- /dev/null +++ b/devtools/README.md @@ -0,0 +1,47 @@ +# devtools + +[![Build Status](https://img.shields.io/circleci/project/mbj/devtools.svg)](https://circleci.com/gh/mbj/devtools/tree/master) +[![Dependency Status](https://gemnasium.com/mbj/devtools.png)](https://gemnasium.com/mbj/devtools) +[![Code Climate](https://codeclimate.com/github/datamapper/devtools.png)](https://codeclimate.com/github/datamapper/devtools) + + +Metagem to assist development. +Used to centralize metric setup and development gem dependencies. + +## Installation + +Add the gem to your Gemfile's development section. + +```ruby +group :development, :test do + gem 'devtools', '~> 0.1.x' +end +``` + +## RSpec support + +If you're using RSpec and want to have access to our common setup just adjust +`spec/spec_helper.rb` to include + +```ruby +require 'devtools/spec_helper' +``` + +## Credits + +The whole [ROM](https://github.com/rom-rb) team that created and maintained all +these tasks before they were centralized here. + +## Contributing + +* Fork the project. +* Make your feature addition or bug fix. +* Add tests for it. This is important so I don't break it in a + future version unintentionally. +* Commit, do not mess with Rakefile or version + (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) +* Send me a pull request. Bonus points for topic branches. + +## License + +See `LICENSE` file. diff --git a/devtools/Rakefile b/devtools/Rakefile new file mode 100644 index 00000000..4ba3cbc4 --- /dev/null +++ b/devtools/Rakefile @@ -0,0 +1,5 @@ +require 'devtools' + +ENV['DEVTOOLS_SELF'] = '1' + +Devtools.init_rake_tasks diff --git a/devtools/config/devtools.yml b/devtools/config/devtools.yml new file mode 100644 index 00000000..f85cd4a3 --- /dev/null +++ b/devtools/config/devtools.yml @@ -0,0 +1,2 @@ +--- +unit_test_timeout: 0.2 diff --git a/devtools/config/flay.yml b/devtools/config/flay.yml new file mode 100644 index 00000000..e32ac18d --- /dev/null +++ b/devtools/config/flay.yml @@ -0,0 +1,3 @@ +--- +threshold: 8 +total_score: 122 diff --git a/devtools/config/flog.yml b/devtools/config/flog.yml new file mode 100644 index 00000000..f03ce2b0 --- /dev/null +++ b/devtools/config/flog.yml @@ -0,0 +1,2 @@ +--- +threshold: 13.5 diff --git a/devtools/config/mutant.yml b/devtools/config/mutant.yml new file mode 100644 index 00000000..4ef17fa2 --- /dev/null +++ b/devtools/config/mutant.yml @@ -0,0 +1,4 @@ +name: devtools +namespace: Devtools +ignore_subjects: +- "Devtools::Flay::Scale#flay" diff --git a/devtools/config/reek.yml b/devtools/config/reek.yml new file mode 100644 index 00000000..54309929 --- /dev/null +++ b/devtools/config/reek.yml @@ -0,0 +1,106 @@ +--- +detectors: + Attribute: + enabled: false + exclude: [] + BooleanParameter: + enabled: true + exclude: [] + ClassVariable: + enabled: true + exclude: [] + ControlParameter: + enabled: true + exclude: [] + DataClump: + enabled: true + exclude: [] + max_copies: 0 + min_clump_size: 2 + DuplicateMethodCall: + enabled: true + exclude: [] + max_calls: 1 + allow_calls: [] + FeatureEnvy: + enabled: true + exclude: [] + IrresponsibleModule: + enabled: true + exclude: [] + LongParameterList: + enabled: true + exclude: + - Devtools::Config#self.attribute + max_params: 2 + overrides: {} + LongYieldList: + enabled: true + exclude: [] + max_params: 0 + NestedIterators: + enabled: true + exclude: [] + max_allowed_nesting: 1 + ignore_iterators: [] + NilCheck: + enabled: true + exclude: [] + RepeatedConditional: + enabled: true + exclude: [] + max_ifs: 1 + TooManyConstants: + enabled: true + exclude: + - Devtools + TooManyInstanceVariables: + enabled: true + exclude: [] + max_instance_variables: 2 + TooManyMethods: + enabled: true + exclude: [] + max_methods: 15 + TooManyStatements: + enabled: true + exclude: [] + max_statements: 5 + UncommunicativeMethodName: + enabled: true + exclude: [] + reject: + - '/^[a-z]$/' + - '/[0-9]$/' + - '/[A-Z]/' + accept: [] + UncommunicativeModuleName: + enabled: true + exclude: [] + reject: + - '/^.$/' + - '/[0-9]$/' + accept: [] + UncommunicativeParameterName: + enabled: true + exclude: [] + reject: + - '/^.$/' + - '/[0-9]$/' + - '/[A-Z]/' + accept: [] + UncommunicativeVariableName: + enabled: true + exclude: [] + reject: + - '/^.$/' + - '/[0-9]$/' + - '/[A-Z]/' + accept: [] + UnusedParameters: + enabled: true + exclude: [] + UtilityFunction: + enabled: true + exclude: + - Devtools::Project::Initializer::Rspec#require_files # intentional for deduplication diff --git a/devtools/config/rubocop.yml b/devtools/config/rubocop.yml new file mode 100644 index 00000000..2f2e75cb --- /dev/null +++ b/devtools/config/rubocop.yml @@ -0,0 +1,151 @@ +inherit_from: ../.rubocop.yml + +AllCops: + TargetRubyVersion: 2.5.0 + +Metrics/BlockLength: + Exclude: + # Ignore RSpec DSL + - spec/**/* + # Ignore gemspec DSL + - '*.gemspec' + +Naming/FileName: + Exclude: + - Rakefile + +# Avoid parameter lists longer than five parameters. +ParameterLists: + Max: 3 + CountKeywordArgs: true + +# Avoid more than `Max` levels of nesting. +BlockNesting: + Max: 3 + +# Align with the style guide. +CollectionMethods: + PreferredMethods: + collect: 'map' + inject: 'reduce' + find: 'detect' + find_all: 'select' + +# Do not force public/protected/private keyword to be indented at the same +# level as the def keyword. My personal preference is to outdent these keywords +# because I think when scanning code it makes it easier to identify the +# sections of code and visually separate them. When the keyword is at the same +# level I think it sort of blends in with the def keywords and makes it harder +# to scan the code and see where the sections are. +AccessModifierIndentation: + Enabled: false + +# Limit line length +LineLength: + Max: 106 + +# Disable documentation checking until a class needs to be documented once +Documentation: + Enabled: false + +# Do not always use &&/|| instead of and/or. +AndOr: + Enabled: false + +# Do not favor modifier if/unless usage when you have a single-line body +IfUnlessModifier: + Enabled: false + +# Allow case equality operator (in limited use within the specs) +CaseEquality: + Enabled: false + +# Constants do not always have to use SCREAMING_SNAKE_CASE +ConstantName: + Enabled: false + +# Not all trivial readers/writers can be defined with attr_* methods +TrivialAccessors: + Enabled: false + +# Allow empty lines around class body +EmptyLinesAroundClassBody: + Enabled: false + +# Allow empty lines around module body +EmptyLinesAroundModuleBody: + Enabled: false + +# Allow empty lines around block body +EmptyLinesAroundBlockBody: + Enabled: false + +# Allow multiple line operations to not require indentation +MultilineOperationIndentation: + Enabled: false + +# Prefer String#% over Kernel#sprintf +FormatString: + Enabled: false + +# Use square brackets for literal Array objects +PercentLiteralDelimiters: + PreferredDelimiters: + '%': '{}' + '%i': '[]' + '%q': () + '%Q': () + '%r': '{}' + '%s': () + '%w': '[]' + '%W': '[]' + '%x': () + +# Align if/else blocks with the variable assignment +EndAlignment: + EnforcedStyleAlignWith: variable + +# Do not always align parameters when it is easier to read +AlignParameters: + Exclude: + - spec/**/*_spec.rb + +# Prefer #kind_of? over #is_a? +ClassCheck: + EnforcedStyle: kind_of? + +# Do not prefer double quotes to be used when %q or %Q is more appropriate +UnneededPercentQ: + Enabled: false + +# Allow a maximum ABC score +Metrics/AbcSize: + Max: 20.1 + +# Do not prefer lambda.call(...) over lambda.(...) +LambdaCall: + Enabled: false + +# Allow additional spaces +ExtraSpacing: + Enabled: false + +# All objects can still be mutated if their eigenclass is patched +RedundantFreeze: + Enabled: false + +# Prefer using `fail` when raising and `raise` when reraising +SignalException: + EnforcedStyle: semantic + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/CommentedKeyword: + Enabled: false + +Style/MixinGrouping: + Enabled: false + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented diff --git a/devtools/config/yardstick.yml b/devtools/config/yardstick.yml new file mode 100644 index 00000000..a6b63e85 --- /dev/null +++ b/devtools/config/yardstick.yml @@ -0,0 +1,2 @@ +--- +threshold: 100 diff --git a/devtools/devtools.gemspec b/devtools/devtools.gemspec new file mode 100644 index 00000000..b737b772 --- /dev/null +++ b/devtools/devtools.gemspec @@ -0,0 +1,36 @@ +Gem::Specification.new do |gem| + gem.name = 'devtools' + gem.version = '0.1.22' + gem.authors = ['Markus Schirp'] + gem.email = ['mbj@schirp-dso.com'] + gem.description = 'A metagem wrapping development tools' + gem.summary = gem.description + gem.homepage = 'https://github.com/rom-rb/devtools' + gem.license = 'MIT' + + gem.require_paths = %w[lib] + gem.files = `git ls-files`.split("\n") + gem.executables = %w[] + gem.test_files = `git ls-files -- spec`.split("\n") + gem.extra_rdoc_files = %w[README.md] + gem.required_ruby_version = '>= 2.5' + + gem.add_runtime_dependency 'abstract_type', '~> 0.0.7' + gem.add_runtime_dependency 'adamantium', '~> 0.2.0' + gem.add_runtime_dependency 'anima', '~> 0.3.0' + gem.add_runtime_dependency 'concord', '~> 0.1.5' + gem.add_runtime_dependency 'flay', '~> 2.12.0' + gem.add_runtime_dependency 'flog', '~> 4.6.2' + gem.add_runtime_dependency 'mutant', '~> 0.8.19' + gem.add_runtime_dependency 'mutant-rspec', '~> 0.8.19' + gem.add_runtime_dependency 'procto', '~> 0.0.3' + gem.add_runtime_dependency 'rake', '~> 12.3.0' + gem.add_runtime_dependency 'reek', '~> 5.2.0' + gem.add_runtime_dependency 'rspec', '~> 3.8.0' + gem.add_runtime_dependency 'rspec-core', '~> 3.8.0' + gem.add_runtime_dependency 'rspec-its', '~> 1.2.0' + gem.add_runtime_dependency 'rubocop', '~> 0.60.0' + gem.add_runtime_dependency 'simplecov', '~> 0.16.1' + gem.add_runtime_dependency 'yard', '~> 0.9.16' + gem.add_runtime_dependency 'yardstick', '~> 0.9.9' +end diff --git a/devtools/lib/devtools.rb b/devtools/lib/devtools.rb new file mode 100644 index 00000000..2684a375 --- /dev/null +++ b/devtools/lib/devtools.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# Stdlib infrastructure +require 'pathname' +require 'rake' +require 'timeout' +require 'yaml' +require 'fileutils' + +# Non stdlib infrastructure +require 'abstract_type' +require 'procto' +require 'anima' +require 'concord' +require 'adamantium' + +# Wrapped tools +require 'flay' +require 'rspec' +require 'rspec/its' +require 'simplecov' + +# Main devtools namespace population +module Devtools + ROOT = Pathname.new(__FILE__).parent.parent.freeze + PROJECT_ROOT = Pathname.pwd.freeze + SHARED_PATH = ROOT.join('shared').freeze + SHARED_SPEC_PATH = SHARED_PATH.join('spec').freeze + DEFAULT_CONFIG_PATH = ROOT.join('default/config').freeze + RAKE_FILES_GLOB = ROOT.join('tasks/**/*.rake').to_s.freeze + LIB_DIRECTORY_NAME = 'lib' + SPEC_DIRECTORY_NAME = 'spec' + RAKE_FILE_NAME = 'Rakefile' + SHARED_SPEC_PATTERN = '{shared,support}/**/*.rb' + UNIT_TEST_PATH_REGEXP = %r{\bspec/unit/} + DEFAULT_CONFIG_DIR_NAME = 'config' + + private_constant(*constants(false)) + + # React to metric violation + # + # @param [String] msg + # + # @return [undefined] + # + # @api private + def self.notify_metric_violation(msg) + abort(msg) + end + + # Initialize project and load tasks + # + # Should *only* be called from your $application_root/Rakefile + # + # @return [self] + # + # @api public + def self.init_rake_tasks + Project::Initializer::Rake.call + self + end + + # Return devtools root path + # + # @return [Pathname] + # + # @api private + def self.root + ROOT + end + + # Return project + # + # @return [Project] + # + # @api private + def self.project + PROJECT + end + +end # module Devtools + +# Devtools implementation +require 'devtools/config' +require 'devtools/project' +require 'devtools/project/initializer' +require 'devtools/project/initializer/rake' +require 'devtools/project/initializer/rspec' +require 'devtools/flay' +require 'devtools/rake/flay' + +# Devtools self initialization +module Devtools + # The project devtools is active for + PROJECT = Project.new(PROJECT_ROOT) +end diff --git a/devtools/lib/devtools/config.rb b/devtools/lib/devtools/config.rb new file mode 100644 index 00000000..ca65805b --- /dev/null +++ b/devtools/lib/devtools/config.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +module Devtools + # Abstract base class of tool configuration + class Config + include Adamantium::Flat, AbstractType, Concord.new(:config_dir) + + # Represent no configuration + DEFAULT_CONFIG = {}.freeze + + # Simple named type check representation + class TypeCheck + # Type check against expected class + include Concord.new(:name, :allowed_classes) + + ERROR_FORMAT = '%s: Got instance of %s expected %s' + CLASS_DELIM = ',' + + # Check value for instance of expected class + # + # @param [Object] value + # + # @return [Object] + def call(value) + klass = value.class + format_values = { + name: name, + got: klass, + allowed: allowed_classes.join(CLASS_DELIM) + } + + unless allowed_classes.any?(&klass.method(:equal?)) + fail TypeError, format(ERROR_FORMAT, format_values) + end + + value + end + end # TypeCheck + + private_constant(*constants(false)) + + # Error raised on type errors + TypeError = Class.new(RuntimeError) + + # Declare an attribute + # + # @param [Symbol] name + # @param [Array] classes + # + # @api private + # + # @return [self] + # + def self.attribute(name, classes, **options) + default = [options.fetch(:default)] if options.key?(:default) + type_check = TypeCheck.new(name, classes) + key = name.to_s + + define_method(name) do + type_check.call(raw.fetch(key, *default)) + end + end + private_class_method :attribute + + # Return config path + # + # @return [String] + # + # @api private + # + def config_file + config_dir.join(self.class::FILE) + end + memoize :config_file + + private + + # Return raw data + # + # @return [Hash] + # + # @api private + # + def raw + yaml_config || DEFAULT_CONFIG + end + memoize :raw + + # Return the raw config data from a yaml file + # + # @return [Hash] + # returned if the yaml file is found + # @return [nil] + # returned if the yaml file is not found + # + # @api private + # + def yaml_config + IceNine.deep_freeze(YAML.load_file(config_file)) if config_file.file? + end + + # Rubocop configuration + class Rubocop < self + FILE = 'rubocop.yml' + end # Rubocop + + # Reek configuration + class Reek < self + FILE = 'reek.yml' + end # Reek + + # Flay configuration + # + class Flay < self + FILE = 'flay.yml' + DEFAULT_LIB_DIRS = %w[lib].freeze + DEFAULT_EXCLUDES = %w[].freeze + + attribute :total_score, [0.class] + attribute :threshold, [0.class] + attribute :lib_dirs, [Array], default: DEFAULT_LIB_DIRS + attribute :excludes, [Array], default: DEFAULT_EXCLUDES + end # Flay + + # Yardstick configuration + class Yardstick < self + FILE = 'yardstick.yml' + OPTIONS = %w[ + threshold + rules + verbose + path + require_exact_threshold + ].freeze + + # Options hash that Yardstick understands + # + # @return [Hash] + # + # @api private + def options + OPTIONS.each_with_object({}) do |name, hash| + hash[name] = raw.fetch(name, nil) + end + end + end # Yardstick + + # Flog configuration + class Flog < self + FILE = 'flog.yml' + DEFAULT_LIB_DIRS = %w[lib].freeze + + attribute :total_score, [Float] + attribute :threshold, [Float] + attribute :lib_dirs, [Array], default: DEFAULT_LIB_DIRS + end # Flog + + # Mutant configuration + class Mutant < self + FILE = 'mutant.yml' + DEFAULT_NAME = '' + DEFAULT_STRATEGY = 'rspec' + + attribute :name, [String], default: DEFAULT_NAME + attribute :strategy, [String], default: DEFAULT_STRATEGY + attribute :zombify, [TrueClass, FalseClass], default: false + attribute :since, [String, NilClass], default: nil + attribute :ignore_subjects, [Array], default: [] + attribute :namespace, [String] + end # Mutant + + # Devtools configuration + class Devtools < self + FILE = 'devtools.yml' + DEFAULT_UNIT_TEST_TIMEOUT = 0.1 # 100ms + + attribute :unit_test_timeout, [Float], default: DEFAULT_UNIT_TEST_TIMEOUT + end # Devtools + end # Config +end # Devtools diff --git a/devtools/lib/devtools/flay.rb b/devtools/lib/devtools/flay.rb new file mode 100644 index 00000000..fda8aa8b --- /dev/null +++ b/devtools/lib/devtools/flay.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Devtools + module Flay + # Measure flay mass relative to size of duplicated sexps + class Scale + include Adamantium + include Anima.new(:minimum_mass, :files) + include Procto.call(:measure) + + # Measure duplication mass + # + # @return [Array] + # + # @api private + def measure + flay.masses.map do |hash, mass| + Rational(mass, flay.hashes.fetch(hash).size) + end + end + + # Report flay output + # + # @return [undefined] + # + # @api private + def flay_report + flay.report + end + + private + + # Memoized flay instance + # + # @return [Flay] + # + # @api private + def flay + ::Flay.new(mass: minimum_mass).tap do |flay| + flay.process(*files) + flay.analyze + end + end + memoize :flay, freezer: :noop + end + + # Expand include and exclude file settings for flay + class FileList + include Procto.call, Concord.new(:includes, :excludes) + + GLOB = '**/*.{rb,erb}' + + # Expand includes and filter by excludes + # + # @return [Set] + # + # @api private + def call + include_set - exclude_set + end + + private + + # Set of excluded files + # + # @return [Set] + # + # @api private + def exclude_set + excludes.flat_map(&Pathname.method(:glob)) + end + + # Set of included files + # + # Expanded using flay's file expander which takes into + # account flay's plugin support + # + # @return [Set] + # + # @api private + def include_set + Set.new(flay_includes.map(&method(:Pathname))) + end + + # Expand includes using flay + # + # Expanded using flay's file expander which takes into + # account flay's plugin support + # + # @return [Array] + # + # @api private + def flay_includes + PathExpander.new(includes.dup, GLOB).process + end + end + end +end diff --git a/devtools/lib/devtools/project.rb b/devtools/lib/devtools/project.rb new file mode 100644 index 00000000..379498fb --- /dev/null +++ b/devtools/lib/devtools/project.rb @@ -0,0 +1,80 @@ +module Devtools + + # The project devtools supports + class Project + include Concord.new(:root) + + CONFIGS = { + devtools: Config::Devtools, + flay: Config::Flay, + flog: Config::Flog, + reek: Config::Reek, + mutant: Config::Mutant, + rubocop: Config::Rubocop, + yardstick: Config::Yardstick + }.freeze + + private_constant(*constants(false)) + + attr_reader(*CONFIGS.keys) + + # The spec root + # + # @return [Pathname] + # + # @api private + attr_reader :spec_root + + # Initialize object + # + # @param [Pathname] root + # + # @return [undefined] + # + # @api private + # + def initialize(root) + super(root) + + initialize_environment + initialize_configs + end + + # Init rspec + # + # @return [self] + # + # @api private + def init_rspec + Initializer::Rspec.call(self) + self + end + + private + + # Initialize environment + # + # @return [undefined] + # + # @api private + # + def initialize_environment + @spec_root = root.join(SPEC_DIRECTORY_NAME) + end + + # Initialize configs + # + # @return [undefined] + # + # @api private + # + def initialize_configs + config_dir = root.join(DEFAULT_CONFIG_DIR_NAME) + + CONFIGS.each do |name, klass| + instance_variable_set(:"@#{name}", klass.new(config_dir)) + end + end + + end # class Project +end # module Devtools diff --git a/devtools/lib/devtools/project/initializer.rb b/devtools/lib/devtools/project/initializer.rb new file mode 100644 index 00000000..bac3abdd --- /dev/null +++ b/devtools/lib/devtools/project/initializer.rb @@ -0,0 +1,11 @@ +module Devtools + class Project + + # Base class for project initializers + class Initializer + include AbstractType + + abstract_singleton_method :call + end # class Initializer + end # class Project +end # module Devtools diff --git a/devtools/lib/devtools/project/initializer/rake.rb b/devtools/lib/devtools/project/initializer/rake.rb new file mode 100644 index 00000000..27e551d6 --- /dev/null +++ b/devtools/lib/devtools/project/initializer/rake.rb @@ -0,0 +1,23 @@ +module Devtools + class Project + class Initializer + # Imports all devtools rake tasks into a project + class Rake < self + include AbstractType + + # Initialize rake tasks + # + # @return [undefined] + # + # @api rpivate + def self.call + FileList + .glob(RAKE_FILES_GLOB) + .each(&::Rake.application.method(:add_import)) + self + end + + end # class Rake + end # class Initializer + end # class Project +end # module Devtools diff --git a/devtools/lib/devtools/project/initializer/rspec.rb b/devtools/lib/devtools/project/initializer/rspec.rb new file mode 100644 index 00000000..520050f0 --- /dev/null +++ b/devtools/lib/devtools/project/initializer/rspec.rb @@ -0,0 +1,80 @@ +module Devtools + class Project + class Initializer + + # Requires all shared specs in a project's spec_helper + # Also installs a configurable unit test timeout + class Rspec < self + include Concord.new(:project) + + # Call initializer for project + # + # @param [Project] project + # + # @return [self] + # + # @api private + def self.call(project) + new(project).__send__(:call) + self + end + + private + + # Setup RSpec for project + # + # @return [self] + # + # @api private + def call + require_shared_spec_files + enable_unit_test_timeout + end + + # Timeout unit tests that take longer than configured amount of time + # + # @param [Numeric] timeout + # + # @return [undefined] + # + # @raise [Timeout::Error] + # raised when the times are outside the timeout + # + # @api private + # + def enable_unit_test_timeout + timeout = project.devtools.unit_test_timeout + RSpec + .configuration + .around(file_path: UNIT_TEST_PATH_REGEXP) do |example| + Timeout.timeout(timeout, &example) + end + end + + # Trigger the require of shared spec files + # + # @return [undefined] + # + # @api private + # + def require_shared_spec_files + require_files(SHARED_SPEC_PATH) + require_files(project.spec_root) + end + + # Require files with pattern + # + # @param [Pathname] dir + # the directory containing the files to require + # + # @return [self] + # + # @api private + def require_files(dir) + Dir.glob(dir.join(SHARED_SPEC_PATTERN)).each(&Kernel.method(:require)) + end + + end # class Rspec + end # class Initializer + end # class Project +end # module Devtools diff --git a/devtools/lib/devtools/rake/flay.rb b/devtools/lib/devtools/rake/flay.rb new file mode 100644 index 00000000..64eec0ba --- /dev/null +++ b/devtools/lib/devtools/rake/flay.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module Devtools + module Rake + # Flay metric runner + class Flay + include Anima.new(:threshold, :total_score, :lib_dirs, :excludes), + Procto.call(:verify), + Adamantium + + BELOW_THRESHOLD = 'Adjust flay threshold down to %d' + TOTAL_MISMATCH = 'Flay total is now %d, but expected %d' + ABOVE_THRESHOLD = '%d chunks have a duplicate mass > %d' + + # Verify code specified by `files` does not violate flay expectations + # + # @raise [SystemExit] if a violation is found + # @return [undefined] otherwise + # + # + # @api private + def verify + # Run flay first to ensure the max mass matches the threshold + below_threshold_message if below_threshold? + + total_mismatch_message if total_mismatch? + + # Run flay a second time with the threshold set + return unless above_threshold? + + restricted_flay_scale.flay_report + above_threshold_message + end + + private + + # List of files flay will analyze + # + # @return [Set] + # + # @api private + def files + Devtools::Flay::FileList.call(lib_dirs, excludes) + end + + # Is there mass duplication which exceeds specified `threshold` + # + # @return [Boolean] + # + # @api private + def above_threshold? + restricted_mass_size.nonzero? + end + + # Is the specified `threshold` greater than the largest flay mass + # + # @return [Boolean] + # + # @api private + def below_threshold? + threshold > largest_mass + end + + # Is the expected mass total different from the actual mass total + # + # @return [Boolean] + # + # @api private + def total_mismatch? + !total_mass.equal?(total_score) + end + + # Above threshold message + # + # @return [String] + # + # @api private + def above_threshold_message + format_values = { mass: restricted_mass_size, threshold: threshold } + Devtools.notify_metric_violation( + format(ABOVE_THRESHOLD, format_values) + ) + end + + # Below threshold message + # + # @return [String] + # + # @api private + def below_threshold_message + Devtools.notify_metric_violation( + format(BELOW_THRESHOLD, mass: largest_mass) + ) + end + + # Total mismatch message + # + # @return [String] + # + # @api private + def total_mismatch_message + Devtools.notify_metric_violation( + format(TOTAL_MISMATCH, mass: total_mass, expected: total_score) + ) + end + + # Size of mass measured by `Flay::Scale` and filtered by `threshold` + # + # @return [Integer] + # + # @api private + def restricted_mass_size + restricted_flay_scale.measure.size + end + + # Sum of all flay mass + # + # @return [Integer] + # + # @api private + def total_mass + flay_masses.reduce(:+).to_i + end + + # Largest flay mass found + # + # @return [Integer] + # + # @api private + def largest_mass + flay_masses.max.to_i + end + + # Flay scale which only measures mass above `threshold` + # + # @return [Flay::Scale] + # + # @api private + def restricted_flay_scale + Devtools::Flay::Scale.new(minimum_mass: threshold.succ, files: files) + end + memoize :restricted_flay_scale + + # All flay masses found in `files` + # + # @return [Array] + # + # @api private + def flay_masses + Devtools::Flay::Scale.call(minimum_mass: 0, files: files) + end + memoize :flay_masses + end + end +end diff --git a/devtools/lib/devtools/spec_helper.rb b/devtools/lib/devtools/spec_helper.rb new file mode 100644 index 00000000..87d0a3ae --- /dev/null +++ b/devtools/lib/devtools/spec_helper.rb @@ -0,0 +1,3 @@ +require 'devtools' + +Devtools::PROJECT.init_rspec diff --git a/devtools/shared/spec/shared/abstract_type_behavior.rb b/devtools/shared/spec/shared/abstract_type_behavior.rb new file mode 100644 index 00000000..cbcf22d8 --- /dev/null +++ b/devtools/shared/spec/shared/abstract_type_behavior.rb @@ -0,0 +1,16 @@ +shared_examples_for 'an abstract type' do + context 'called on a subclass' do + let(:object) { Class.new(described_class) } + + it { should be_instance_of(object) } + end + + context 'called on the class' do + let(:object) { described_class } + + specify do + expect { subject } + .to raise_error(NotImplementedError, "#{object} is an abstract type") + end + end +end diff --git a/devtools/shared/spec/shared/command_method_behavior.rb b/devtools/shared/spec/shared/command_method_behavior.rb new file mode 100644 index 00000000..9bba54c6 --- /dev/null +++ b/devtools/shared/spec/shared/command_method_behavior.rb @@ -0,0 +1,5 @@ +shared_examples_for 'a command method' do + it 'returns self' do + should equal(object) + end +end diff --git a/devtools/shared/spec/shared/each_method_behaviour.rb b/devtools/shared/spec/shared/each_method_behaviour.rb new file mode 100644 index 00000000..a7141a7d --- /dev/null +++ b/devtools/shared/spec/shared/each_method_behaviour.rb @@ -0,0 +1,13 @@ +shared_examples_for 'an #each method' do + it_should_behave_like 'a command method' + + context 'with no block' do + subject { object.each } + + it { should be_instance_of(to_enum.class) } + + it 'yields the expected values' do + expect(subject.to_a).to eql(object.to_a) + end + end +end diff --git a/devtools/shared/spec/shared/idempotent_method_behavior.rb b/devtools/shared/spec/shared/idempotent_method_behavior.rb new file mode 100644 index 00000000..be0755ee --- /dev/null +++ b/devtools/shared/spec/shared/idempotent_method_behavior.rb @@ -0,0 +1,11 @@ +shared_examples_for 'an idempotent method' do + it 'is idempotent' do + first = subject + error = 'RSpec not configured for threadsafety' + fail error unless RSpec.configuration.threadsafe? + mutex = __memoized.instance_variable_get(:@mutex) + memoized = __memoized.instance_variable_get(:@memoized) + mutex.synchronize { memoized.delete(:subject) } + should equal(first) + end +end diff --git a/devtools/shared/spec/support/ice_nine_config.rb b/devtools/shared/spec/support/ice_nine_config.rb new file mode 100644 index 00000000..5545e8d2 --- /dev/null +++ b/devtools/shared/spec/support/ice_nine_config.rb @@ -0,0 +1,12 @@ +if defined?(IceNine) + module IceNine + + # Freezer namespace + class Freezer + + # Rspec freezer + class RSpec < NoFreeze; end + + end # Freezer + end # IceNine +end diff --git a/devtools/spec/integration/devtools/rake/flay/verify_spec.rb b/devtools/spec/integration/devtools/rake/flay/verify_spec.rb new file mode 100644 index 00000000..463a7c14 --- /dev/null +++ b/devtools/spec/integration/devtools/rake/flay/verify_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +describe Devtools::Rake::Flay, '#verify' do + let(:tempfile) { Tempfile.new(%w[file .rb], Dir.mktmpdir) } + let(:file) { Pathname(tempfile.path) } + let(:directories) { [file.dirname.to_s] } + + let(:ruby) do + <<-ERUBY + def foo; end + def bar; end + ERUBY + end + + around(:each) do |example| + begin + # silence other flay output + $stdout = $stderr = StringIO.new + + tempfile.write(ruby) + tempfile.close + + example.run + ensure + $stdout = STDOUT + $stderr = STDERR + + file.unlink + end + end + + context 'reporting' do + let(:options) do + { threshold: 3, total_score: 3, lib_dirs: directories, excludes: [] } + end + + let(:instance) { described_class.new(options) } + + it 'measures total mass' do + allow(::Flay).to receive(:new).and_call_original + + instance.verify + + expect(::Flay).to have_received(:new).with(hash_including(mass: 0)) + end + + it 'does not report the files it is processing' do + expect { instance.verify }.to_not output(/Processing #{file}/).to_stderr + end + end + + context 'when theshold is too low' do + let(:instance) do + described_class.new( + threshold: 0, + total_score: 0, + lib_dirs: directories, + excludes: [] + ) + end + + specify do + expect { instance.verify } + .to raise_error(SystemExit) + .with_message('Flay total is now 3, but expected 0') + end + end + + context 'when threshold is too high' do + let(:instance) do + described_class.new( + threshold: 1000, + total_score: 0, + lib_dirs: directories, + excludes: [] + ) + end + + specify do + expect { instance.verify } + .to raise_error(SystemExit) + .with_message('Adjust flay threshold down to 3') + end + end + + context 'when total is too high' do + let(:instance) do + described_class.new( + threshold: 3, + total_score: 50, + lib_dirs: directories, + excludes: [] + ) + end + + specify do + expect { instance.verify } + .to raise_error(SystemExit) + .with_message('Flay total is now 3, but expected 50') + end + end + + context 'when duplicate mass is greater than 0' do + let(:ruby) do + <<-ERUBY + def foo + :hi if baz? + end + + def bar + :hi if baz? + end + ERUBY + end + + let(:report) do + <<~REPORT + Total score (lower is better) = 10 + + 1) Similar code found in :defn (mass = 10) + #{file}:1 + #{file}:5 + REPORT + end + + let(:instance) do + described_class.new( + threshold: 3, + total_score: 5, + lib_dirs: directories, + excludes: [] + ) + end + + specify do + expect { instance.verify } + .to raise_error(SystemExit) + .with_message('1 chunks have a duplicate mass > 3') + end + + specify do + expect { instance.verify } + .to raise_error(SystemExit) + .and output(report).to_stdout + end + end + + context 'when multiple duplicate masses' do + let(:ruby) do + <<-ERUBY + def foo; end + def bar; end + + class Foo + def initialize + @a = 1 + end + end + class Bar + def initialize + @a = 1 + end + end + ERUBY + end + + let(:instance) do + described_class.new( + threshold: 5, + total_score: 8, + lib_dirs: directories, + excludes: [] + ) + end + + it 'sums masses for total' do + expect { instance.verify }.to_not raise_error + end + end + + context 'when no duplication masses' do + let(:ruby) { '' } + let(:instance) do + described_class.new( + threshold: 0, + total_score: 0, + lib_dirs: directories, + excludes: [] + ) + end + + specify do + expect { instance.verify }.to_not raise_error + end + end +end diff --git a/devtools/spec/spec_helper.rb b/devtools/spec/spec_helper.rb new file mode 100644 index 00000000..3166cef1 --- /dev/null +++ b/devtools/spec/spec_helper.rb @@ -0,0 +1,24 @@ +require 'devtools/spec_helper' +require 'tempfile' +require 'tmpdir' + +if ENV['COVERAGE'] == 'true' + formatter = [SimpleCov::Formatter::HTMLFormatter] + SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(formatter) + + SimpleCov.start do + command_name 'spec:unit' + + add_filter 'config' + add_filter 'spec' + add_filter 'vendor' + + minimum_coverage 100 + end +end + +RSpec.configure do |config| + config.expect_with :rspec do |expect_with| + expect_with.syntax = :expect + end +end diff --git a/devtools/spec/unit/devtools/config/yardstick_spec.rb b/devtools/spec/unit/devtools/config/yardstick_spec.rb new file mode 100644 index 00000000..79135704 --- /dev/null +++ b/devtools/spec/unit/devtools/config/yardstick_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe Devtools::Config::Yardstick do + let(:object) { described_class.new(Devtools.root.join('config')) } + + describe '#options' do + subject { object.options } + + specify do + should eql( + 'threshold' => 100, + 'rules' => nil, + 'verbose' => nil, + 'path' => nil, + 'require_exact_threshold' => nil + ) + end + end +end diff --git a/devtools/spec/unit/devtools/config_spec.rb b/devtools/spec/unit/devtools/config_spec.rb new file mode 100644 index 00000000..2f3f61b3 --- /dev/null +++ b/devtools/spec/unit/devtools/config_spec.rb @@ -0,0 +1,78 @@ +RSpec.describe Devtools::Config do + + describe '.attribute' do + let(:raw) do + { + 'a' => 'bar', + 'c' => [] + } + + end + + let(:config_path) { instance_double(Pathname) } + + let(:class_under_test) do + expect(config_path).to receive(:file?) + .and_return(file?) + expect(config_path).to receive(:frozen?) + .and_return(true) + expect(config_path).to receive(:join) + .with('bar.yml') + .and_return(config_path) + + Class.new(described_class) do + attribute :a, [String] + attribute :b, [Array], default: [] + attribute :c, [TrueClass, FalseClass] + + const_set(:FILE, 'bar.yml') + end + end + + subject do + class_under_test.new(config_path) + end + + context 'on present config' do + let(:class_under_test) do + # Setup message expectation in a lasy way, not in a before + # block to make sure the around hook setting timeouts from the + # code under test is not affected. + expect(YAML).to receive(:load_file) + .with(config_path) + .and_return(raw) + + expect(IceNine).to receive(:deep_freeze) + .with(raw) + .and_return(raw) + + super() + end + + let(:file?) { true } + + it 'allows to receive existing keys' do + expect(subject.a).to eql('bar') + end + + it 'allows to receive absent keys with defaults' do + expect(subject.b).to eql([]) + end + + it 'executes checks when configured' do + expect { subject.c }.to raise_error( + Devtools::Config::TypeError, + 'c: Got instance of Array expected TrueClass,FalseClass' + ) + end + end + + context 'on absent config' do + let(:file?) { false } + + it 'defaults to absent keys' do + expect(subject.b).to eql([]) + end + end + end +end diff --git a/devtools/spec/unit/devtools/flay/file_list/call_spec.rb b/devtools/spec/unit/devtools/flay/file_list/call_spec.rb new file mode 100644 index 00000000..d0995651 --- /dev/null +++ b/devtools/spec/unit/devtools/flay/file_list/call_spec.rb @@ -0,0 +1,19 @@ +describe Devtools::Flay::FileList, '.call' do + subject(:output) { described_class.call([tmpdir.to_s].freeze, [exclude]) } + + let(:tmpdir) { Dir.mktmpdir } + let(:one) { Pathname(tmpdir).join('1.rb') } + let(:two) { Pathname(tmpdir).join('2.erb') } + let(:three) { Pathname(tmpdir).join('3.rb') } + let(:exclude) { Pathname(tmpdir).join('3*').to_s } + + around(:each) do |example| + [one, two, three].map(&FileUtils.method(:touch)) + + example.run + + FileUtils.rm_rf(tmpdir) + end + + it { should eql(Set.new([one, two])) } +end diff --git a/devtools/spec/unit/devtools/flay/scale/flay_report_spec.rb b/devtools/spec/unit/devtools/flay/scale/flay_report_spec.rb new file mode 100644 index 00000000..82f68493 --- /dev/null +++ b/devtools/spec/unit/devtools/flay/scale/flay_report_spec.rb @@ -0,0 +1,17 @@ +describe Devtools::Flay::Scale, '#flay_report' do + subject(:instance) { described_class.new(minimum_mass: 0, files: []) } + + let(:flay) do + instance_double(::Flay, process: nil, analyze: nil, masses: {}) + end + + before do + allow(::Flay).to receive(:new).with(mass: 0).and_return(flay) + end + + specify do + allow(flay).to receive(:report) + instance.flay_report + expect(flay).to have_received(:report) + end +end diff --git a/devtools/spec/unit/devtools/flay/scale/measure_spec.rb b/devtools/spec/unit/devtools/flay/scale/measure_spec.rb new file mode 100644 index 00000000..7c82c6c6 --- /dev/null +++ b/devtools/spec/unit/devtools/flay/scale/measure_spec.rb @@ -0,0 +1,43 @@ +describe Devtools::Flay::Scale, '#measure' do + subject(:measure) { instance.measure } + + let(:minimum_mass) { 0 } + let(:files) { [instance_double(File)] } + let(:flay_masses) { { 0 => 5, 1 => 10 } } + + let(:instance) do + described_class.new(minimum_mass: minimum_mass, files: files) + end + + let(:flay_hashes) do + { + 0 => instance_double(Array, size: 3), + 1 => instance_double(Array, size: 11) + } + end + + let(:flay) do + instance_double( + ::Flay, + analyze: nil, + masses: flay_masses, + hashes: flay_hashes + ) + end + + before do + allow(::Flay).to receive(:new).with(mass: minimum_mass).and_return(flay) + allow(flay).to receive(:process).with(*files) + end + + it { should eql([Rational(5, 3), Rational(10, 11)]) } + + context 'when minimum mass is not 0' do + let(:minimum_mass) { 1 } + + specify do + measure + expect(::Flay).to have_received(:new).with(mass: 1) + end + end +end diff --git a/devtools/spec/unit/devtools/project/initializer/rake_spec.rb b/devtools/spec/unit/devtools/project/initializer/rake_spec.rb new file mode 100644 index 00000000..ab364d4a --- /dev/null +++ b/devtools/spec/unit/devtools/project/initializer/rake_spec.rb @@ -0,0 +1,21 @@ +describe Devtools::Project::Initializer::Rake do + describe '.call' do + subject do + described_class.call + end + + it 'performs expected rake initialization' do + path_a = instance_double(Pathname) + path_b = instance_double(Pathname) + + expect(FileList).to receive(:glob) + .with(Devtools.root.join('tasks/**/*.rake').to_s) + .and_return([path_a, path_b]) + + expect(Rake.application).to receive(:add_import).with(path_a) + expect(Rake.application).to receive(:add_import).with(path_b) + + expect(subject).to be(described_class) + end + end +end diff --git a/devtools/spec/unit/devtools/project/initializer/rspec_spec.rb b/devtools/spec/unit/devtools/project/initializer/rspec_spec.rb new file mode 100644 index 00000000..1ba1f885 --- /dev/null +++ b/devtools/spec/unit/devtools/project/initializer/rspec_spec.rb @@ -0,0 +1,52 @@ +describe Devtools::Project::Initializer::Rspec do + let(:spec_root) { Devtools.root.join('spec') } + let(:unit_test_timeout) { instance_double(Float) } + + let(:project) do + instance_double( + Devtools::Project, + spec_root: spec_root, + devtools: instance_double( + Devtools::Config::Devtools, + unit_test_timeout: unit_test_timeout + ) + ) + end + + describe '.call' do + subject do + described_class.call(project) + end + + it 'performs expected rspec initialization' do + called = false + example = -> { called = true } + + expect(Dir).to receive(:glob) + .with(Devtools.root.join('shared/spec/{shared,support}/**/*.rb')) + .and_return(%w[shared-a shared-b]) + + expect(Kernel).to receive(:require).with('shared-a') + expect(Kernel).to receive(:require).with('shared-b') + + expect(Dir).to receive(:glob) + .with(Devtools.root.join('spec/{shared,support}/**/*.rb')) + .and_return(%w[support-a support-b]) + + expect(Kernel).to receive(:require).with('support-a') + expect(Kernel).to receive(:require).with('support-b') + + expect(Timeout).to receive(:timeout).with(unit_test_timeout) do |&block| + block.call + end + + expect(RSpec.configuration).to receive(:around) + .with(file_path: %r{\bspec/unit/}) + .and_yield(example) + + expect(subject).to be(described_class) + + expect(called).to be(true) + end + end +end diff --git a/devtools/spec/unit/devtools/project_spec.rb b/devtools/spec/unit/devtools/project_spec.rb new file mode 100644 index 00000000..c584abf2 --- /dev/null +++ b/devtools/spec/unit/devtools/project_spec.rb @@ -0,0 +1,35 @@ +RSpec.describe Devtools::Project do + let(:object) { described_class.new(Devtools.root) } + + describe '#init_rspec' do + subject { object.init_rspec } + + it 'calls the rspec initializer' do + expect(Devtools::Project::Initializer::Rspec) + .to receive(:call).with(Devtools.project) + expect(subject).to be(object) + end + end + + { + devtools: Devtools::Config::Devtools, + flay: Devtools::Config::Flay, + flog: Devtools::Config::Flog, + reek: Devtools::Config::Reek, + mutant: Devtools::Config::Mutant, + rubocop: Devtools::Config::Rubocop, + yardstick: Devtools::Config::Yardstick + }.each do |name, klass| + describe "##{name}" do + subject { object.send(name) } + + specify { should eql(klass.new(Devtools.root.join('config'))) } + end + end + + describe '#spec_root' do + subject { object.spec_root } + + specify { should eql(Devtools.root.join('spec')) } + end +end diff --git a/devtools/spec/unit/devtools_spec.rb b/devtools/spec/unit/devtools_spec.rb new file mode 100644 index 00000000..0766ed87 --- /dev/null +++ b/devtools/spec/unit/devtools_spec.rb @@ -0,0 +1,14 @@ +describe Devtools do + describe '.project' do + specify do + expect(Devtools.project).to equal(Devtools::PROJECT) + end + end + + describe '.init_rake_tasks' do + specify do + expect(Devtools::Project::Initializer::Rake).to receive(:call) + expect(Devtools.init_rake_tasks).to be(Devtools) + end + end +end diff --git a/devtools/tasks/metrics/ci.rake b/devtools/tasks/metrics/ci.rake new file mode 100644 index 00000000..1e46fe61 --- /dev/null +++ b/devtools/tasks/metrics/ci.rake @@ -0,0 +1,17 @@ +desc 'Run all specs, metrics and mutant' +task ci: %w[ci:metrics metrics:mutant] + +namespace :ci do + tasks = %w[ + metrics:coverage + metrics:yardstick:verify + metrics:rubocop + metrics:flog + metrics:flay + metrics:reek + spec:integration + ] + + desc 'Run metrics (except mutant)' + task metrics: tasks +end diff --git a/devtools/tasks/metrics/coverage.rake b/devtools/tasks/metrics/coverage.rake new file mode 100644 index 00000000..a36c39c0 --- /dev/null +++ b/devtools/tasks/metrics/coverage.rake @@ -0,0 +1,13 @@ +namespace :metrics do + desc 'Measure code coverage' + task :coverage do + begin + # rubocop:disable Style/ParallelAssignment + original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' + # rubocop:enable Style/ParallelAssignment + Rake::Task['spec:unit'].execute + ensure + ENV['COVERAGE'] = original + end + end +end diff --git a/devtools/tasks/metrics/flay.rake b/devtools/tasks/metrics/flay.rake new file mode 100644 index 00000000..75e0e89f --- /dev/null +++ b/devtools/tasks/metrics/flay.rake @@ -0,0 +1,21 @@ +namespace :metrics do + require 'flay' + + project = Devtools.project + config = project.flay + + # Original code by Marty Andrews: + # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html + desc 'Measure code duplication' + task :flay do + threshold = config.threshold + total_score = config.total_score + + Devtools::Rake::Flay.call( + threshold: threshold, + total_score: total_score, + lib_dirs: config.lib_dirs, + excludes: config.excludes + ) + end +end diff --git a/devtools/tasks/metrics/flog.rake b/devtools/tasks/metrics/flog.rake new file mode 100644 index 00000000..0da4bb78 --- /dev/null +++ b/devtools/tasks/metrics/flog.rake @@ -0,0 +1,43 @@ +# rubocop:disable Metrics/BlockLength +namespace :metrics do + require 'flog' + require 'flog_cli' + + project = Devtools.project + config = project.flog + + # Original code by Marty Andrews: + # http://blog.martyandrews.net/2009/05/enforcing-ruby-code-quality.html + desc 'Measure code complexity' + task :flog do + threshold = config.threshold.to_f.round(1) + flog = Flog.new + flog.flog(*PathExpander.new(config.lib_dirs.dup, '**/*.rb').process) + + totals = flog + .totals + .reject { |name, _score| name.end_with?('#none') } + .map { |name, score| [name, score.round(1)] } + .sort_by { |_name, score| score } + + if totals.any? + max = totals.last[1] + unless max >= threshold + Devtools.notify_metric_violation "Adjust flog score down to #{max}" + end + end + + bad_methods = totals.select { |_name, score| score > threshold } + + if bad_methods.any? + bad_methods.reverse_each do |name, score| + printf "%8.1f: %s\n", score, name + end + + Devtools.notify_metric_violation( + "#{bad_methods.size} methods have a flog complexity > #{threshold}" + ) + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/devtools/tasks/metrics/mutant.rake b/devtools/tasks/metrics/mutant.rake new file mode 100644 index 00000000..26e9848a --- /dev/null +++ b/devtools/tasks/metrics/mutant.rake @@ -0,0 +1,43 @@ +# rubocop:disable Metrics/BlockLength +namespace :metrics do + config = Devtools.project.mutant + + desc 'Measure mutation coverage' + task mutant: :coverage do + require 'mutant' + + namespace = + if config.zombify + Mutant.zombify + Zombie::Mutant + else + Mutant + end + + namespaces = Array(config.namespace).map { |n| "#{n}*" } + + ignore_subjects = config.ignore_subjects.flat_map do |matcher| + %W[--ignore #{matcher}] + end + + jobs = ENV.key?('CIRCLECI') ? %w[--jobs 4] : [] + + since = + if config.since + %W[--since #{config.since}] + else + [] + end + + arguments = %W[ + --include lib + --require #{config.name} + --use #{config.strategy} + ].concat(ignore_subjects).concat(namespaces).concat(since).concat(jobs) + + unless namespace::CLI.run(arguments) + Devtools.notify_metric_violation('Mutant task is not successful') + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/devtools/tasks/metrics/reek.rake b/devtools/tasks/metrics/reek.rake new file mode 100644 index 00000000..240dc67e --- /dev/null +++ b/devtools/tasks/metrics/reek.rake @@ -0,0 +1,8 @@ +namespace :metrics do + require 'reek/rake/task' + + Reek::Rake::Task.new do |reek| + reek.source_files = '{app,lib}/**/*.rb' + reek.config_file = 'config/reek.yml' + end +end diff --git a/devtools/tasks/metrics/rubocop.rake b/devtools/tasks/metrics/rubocop.rake new file mode 100644 index 00000000..af657e42 --- /dev/null +++ b/devtools/tasks/metrics/rubocop.rake @@ -0,0 +1,13 @@ +namespace :metrics do + desc 'Check with code style guide' + task :rubocop do + require 'rubocop' + config = Devtools.project.rubocop + begin + exit_status = RuboCop::CLI.new.run(%W[--config #{config.config_file}]) + fail 'Rubocop not successful' unless exit_status.zero? + rescue Encoding::CompatibilityError => exception + Devtools.notify_metric_violation exception.message + end + end +end diff --git a/devtools/tasks/metrics/yardstick.rake b/devtools/tasks/metrics/yardstick.rake new file mode 100644 index 00000000..71bd404a --- /dev/null +++ b/devtools/tasks/metrics/yardstick.rake @@ -0,0 +1,12 @@ +namespace :metrics do + namespace :yardstick do + require 'yardstick/rake/measurement' + require 'yardstick/rake/verify' + + options = Devtools.project.yardstick.options + + Yardstick::Rake::Measurement.new(:measure, options) + + Yardstick::Rake::Verify.new(:verify, options) + end +end diff --git a/devtools/tasks/spec.rake b/devtools/tasks/spec.rake new file mode 100644 index 00000000..a003afdc --- /dev/null +++ b/devtools/tasks/spec.rake @@ -0,0 +1,34 @@ +begin + require 'rspec/core/rake_task' + + # Remove existing same-named tasks + %w[spec spec:unit spec:integration].each do |task| + klass = Rake::Task + klass[task].clear if klass.task_defined?(task) + end + + desc 'Run all specs' + RSpec::Core::RakeTask.new(:spec) do |task| + task.pattern = 'spec/{unit,integration}/**/*_spec.rb' + end + + namespace :spec do + desc 'Run unit specs' + RSpec::Core::RakeTask.new(:unit) do |task| + task.pattern = 'spec/unit/**/*_spec.rb' + end + + desc 'Run integration specs' + RSpec::Core::RakeTask.new(:integration) do |task| + task.pattern = 'spec/integration/**/*_spec.rb' + end + end +rescue LoadError + %w[spec spec:unit spec:integration].each do |name| + task name do + warn "In order to run #{name}, do: gem install rspec" + end + end +end + +task test: :spec diff --git a/devtools/tasks/yard.rake b/devtools/tasks/yard.rake new file mode 100644 index 00000000..588bab60 --- /dev/null +++ b/devtools/tasks/yard.rake @@ -0,0 +1,9 @@ +begin + require 'yard' + + YARD::Rake::YardocTask.new +rescue LoadError + task :yard do + warn 'In order to run yard, you must: gem install yard' + end +end diff --git a/mutant.gemspec b/mutant.gemspec index 25b7e610..2dcdadc5 100644 --- a/mutant.gemspec +++ b/mutant.gemspec @@ -36,6 +36,5 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency('regexp_parser', '~> 1.2') gem.add_runtime_dependency('unparser', '~> 0.4.2') - gem.add_development_dependency('devtools', '~> 0.1.22') gem.add_development_dependency('parallel', '~> 1.3') end