diff --git a/.rubocop.yml b/.rubocop.yml index e1d6acf7e..037ef6a9a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -101,6 +101,7 @@ Style/CharacterLiteral: Style/ClassAndModuleChildren: Enabled: false Style/CollectionMethods: + Enabled: true PreferredMethods: find: detect reduce: inject diff --git a/Appraisals b/Appraisals index e76d3358e..13c3472c6 100644 --- a/Appraisals +++ b/Appraisals @@ -111,24 +111,26 @@ appraise 'rails_5_2' do gem 'pg', '~> 1.1', platform: :ruby end -appraise 'rails_6_0' do - instance_eval(&shared_dependencies) - - gem 'rails', '~> 6.0.0.beta3' - gem 'puma', '~> 3.11' - gem 'bootsnap', '>= 1.4.1', require: false - gem 'sass-rails', '~> 5.0' - gem 'webpacker', '>= 4.0.0.rc3' - gem 'turbolinks', '~> 5' - gem 'jbuilder', '~> 2.5' - gem 'bcrypt', '~> 3.1.7' - gem 'capybara', '>= 2.15' - gem 'listen', '>= 3.0.5', '< 3.2' - gem 'spring-watcher-listen', '~> 2.0.0' - gem 'selenium-webdriver' - gem 'chromedriver-helper' - - # Other dependencies - gem 'rails-controller-testing', '>= 1.0.1' - gem 'pg', '~> 1.1', platform: :ruby +if Gem::Requirement.new('>= 2.5.0').satisfied_by?(Gem::Version.new(RUBY_VERSION)) + appraise 'rails_6_0' do + instance_eval(&shared_dependencies) + + gem 'rails', '~> 6.0.0.beta3' + gem 'puma', '~> 3.11' + gem 'bootsnap', '>= 1.4.1', require: false + gem 'sass-rails', '~> 5.0' + gem 'webpacker', '>= 4.0.0.rc3' + gem 'turbolinks', '~> 5' + gem 'jbuilder', '~> 2.5' + gem 'bcrypt', '~> 3.1.7' + gem 'capybara', '>= 2.15' + gem 'listen', '>= 3.0.5', '< 3.2' + gem 'spring-watcher-listen', '~> 2.0.0' + gem 'selenium-webdriver' + gem 'chromedriver-helper' + + # Other dependencies + gem 'rails-controller-testing', '>= 1.0.1' + gem 'pg', '~> 1.1', platform: :ruby + end end diff --git a/bin/setup b/bin/setup index f88f866bc..96b9c181f 100755 --- a/bin/setup +++ b/bin/setup @@ -3,6 +3,7 @@ set -euo pipefail RUBY_VERSION=$(script/supported_ruby_versions | xargs -n 1 echo | sort -V | tail -n 1) +required_ruby_version=$(cat .ruby-version) cd "$(dirname "$(dirname "$0")")" @@ -30,6 +31,10 @@ error() { echo -e "\033[31m$@\033[0m" } +echo-wrapped() { + echo "$@" | fmt -w 80 | cat +} + has-executable() { type "$1" &>/dev/null } @@ -38,6 +43,14 @@ is-running() { pgrep "$1" >/dev/null } +start() { + if has-executable brew; then + brew services start "$1" + else + sudo service "${2:-$1}" start + fi +} + install() { local apt_package="" local rpm_package="" @@ -90,9 +103,10 @@ check-for-build-tools() { if [[ $platform == "linux" ]]; then if ! has-executable apt-get; then error "You don't seem to have a package manager installed." - echo "The setup script assumes you're using Debian or a Debian-derived flavor of Linux" - echo "(i.e. something with Apt). If this is not the case, then we would gladly take a" - echo "PR fixing this!" + echo-wrapped "\ +The setup script assumes you're using Debian or a Debian-derived flavor of +Linux (i.e. something with Apt). If this is not the case, then we would +gladly take a PR fixing this!" exit 1 fi @@ -100,10 +114,12 @@ check-for-build-tools() { else if ! has-executable brew; then error "You don't seem to have Homebrew installed." - echo - echo "Follow the instructions here to do this:" - echo - echo "http://brew.sh" + echo-wrapped "\ +Follow the instructions here to do this: + + http://brew.sh + +Then re-run this script." exit 1 fi @@ -145,21 +161,22 @@ install-dependencies() { rbenv install --skip-existing "$RUBY_VERSION" fi elif has-executable rvm; then - if ! (rvm ls | grep $RUBY_VERSION'\>' &>/dev/null); then - banner "Installing Ruby $RUBY_VERSION with rvm" - error "You don't seem to have Ruby $RUBY_VERSION installed." - echo - echo "Use RVM to do so, and then re-run this command." - echo + if ! (rvm list | grep $required_ruby_version'\>' &>/dev/null); then + banner "Installing Ruby $required_ruby_version with rvm" + rvm install $required_ruby_version + rvm use $required_ruby_version fi else error "You don't seem to have a Ruby manager installed." - echo - echo 'We recommend using rbenv. You can find installation instructions here:' - echo - echo 'http://github.com/rbenv/rbenv' - echo - echo "When you're done, simply re-run this script!" + echo-wrapped "\ +We recommend using rbenv. You can find instructions to install it here: + + https://github.com/rbenv/rbenv#installation + +Make sure to follow the instructions to configure your shell so that rbenv is +automatically loaded. + +When you're done, open up a new terminal tab and re-run this script." exit 1 fi @@ -167,22 +184,6 @@ install-dependencies() { gem install bundler -v '~> 1.0' --conservative bundle check || bundle install bundle exec appraisal install - - if ! has-executable node; then - banner 'Installing Node' - - if [[ $platform == 'linux' ]]; then - curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - - - install nodejs - - if ! has-executable npm; then - install npm - fi - else - install nodejs - fi - fi } check-for-build-tools diff --git a/lib/shoulda/matchers/active_record/have_db_index_matcher.rb b/lib/shoulda/matchers/active_record/have_db_index_matcher.rb index 24b8edf2b..6cfe5d85b 100644 --- a/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +++ b/lib/shoulda/matchers/active_record/have_db_index_matcher.rb @@ -49,6 +49,30 @@ module ActiveRecord # should have_db_index([:user_id, :name]) # end # + # Finally, if you're using Rails 5 and PostgreSQL, you can also specify an + # expression: + # + # class CreateLoggedErrors < ActiveRecord::Migration + # def change + # create_table :logged_errors do |t| + # t.string :code + # t.jsonb :content + # end + # + # add_index :logged_errors, 'lower(code)::text' + # end + # end + # + # # RSpec + # RSpec.describe LoggedError, type: :model do + # it { should have_db_index('lower(code)::text') } + # end + # + # # Minitest (Shoulda) + # class LoggedErrorTest < ActiveSupport::TestCase + # should have_db_index('lower(code)::text') + # end + # # #### Qualifiers # # ##### unique @@ -171,9 +195,16 @@ def correct_unique? end def matched_index - @_matched_index ||= actual_indexes.find do |index| - index.columns == expected_columns - end + @_matched_index ||= + if expected_columns.one? + actual_indexes.detect do |index| + Array.wrap(index.columns) == expected_columns + end + else + actual_indexes.detect do |index| + index.columns == expected_columns + end + end end def actual_indexes diff --git a/spec/support/unit/helpers/active_record_versions.rb b/spec/support/unit/helpers/active_record_versions.rb index 44eee019b..1d35db635 100644 --- a/spec/support/unit/helpers/active_record_versions.rb +++ b/spec/support/unit/helpers/active_record_versions.rb @@ -40,5 +40,9 @@ def active_record_uniqueness_supports_array_columns? def active_record_supports_optional_for_associations? active_record_version >= 5 end + + def active_record_supports_expression_indexes? + active_record_version >= 5 + end end end diff --git a/spec/support/unit/helpers/database_helpers.rb b/spec/support/unit/helpers/database_helpers.rb index f7a267b95..df7c9f4c9 100644 --- a/spec/support/unit/helpers/database_helpers.rb +++ b/spec/support/unit/helpers/database_helpers.rb @@ -16,5 +16,6 @@ def postgresql? alias_method :database_supports_array_columns?, :postgresql? alias_method :database_supports_uuid_columns?, :postgresql? alias_method :database_supports_money_columns?, :postgresql? + alias_method :database_supports_expression_indexes?, :postgresql? end end diff --git a/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb index f19eaad46..2c1025c50 100644 --- a/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb @@ -1,6 +1,11 @@ require 'unit_spec_helper' describe Shoulda::Matchers::ActiveRecord::HaveDbIndexMatcher, type: :model do + def self.can_test_expression_indexes? + active_record_supports_expression_indexes? && + database_supports_expression_indexes? + end + describe 'the matcher' do # rubocop:disable Layout/MultilineBlockLayout # rubocop:disable Layout/SpaceAroundBlockParameters @@ -237,6 +242,99 @@ end end end + + if can_test_expression_indexes? + context 'when given an expression' do + context 'qualified with nothing' do + context 'when the table has the given index' do + it 'matches when used in the positive' do + record = record_with_index_on( + 'lower((code)::text)', + columns: { code: :string }, + ) + expect(record).to have_db_index('lower((code)::text)') + end + + it 'does not match when used in the negative' do + record = record_with_index_on( + 'lower((code)::text)', + model_name: 'Example', + columns: { code: :string }, + ) + + assertion = lambda do + expect(record).not_to have_db_index('lower((code)::text)') + end + + expect(&assertion).to fail_with_message(<<-MESSAGE, wrap: true) +Expected the examples table not to have an index on "lower((code)::text)", but +it does. + MESSAGE + end + end + + context 'when the table does not have the given index' do + it 'matches when used in the negative' do + record = record_with_index_on( + 'code', + columns: { code: :string }, + ) + expect(record).not_to have_db_index('lower((code)::text)') + end + + it 'does not match when used in the positive' do + record = record_with_index_on( + 'code', + model_name: 'Example', + columns: { code: :string }, + ) + + assertion = lambda do + expect(record).to have_db_index('lower((code)::text)') + end + + expect(&assertion).to fail_with_message(<<-MESSAGE, wrap: true) +Expected the examples table to have an index on "lower((code)::text)", but it +does not. + MESSAGE + end + end + end + + context 'when qualified with unique' do + include_examples( + 'for when the matcher is qualified', + index: 'lower((code)::text)', + other_index: 'code', + columns: { code: :string }, + unique: true, + qualifier_args: [], + ) + end + + context 'when qualified with unique: true' do + include_examples( + 'for when the matcher is qualified', + index: 'lower((code)::text)', + other_index: 'code', + columns: { code: :string }, + unique: true, + qualifier_args: [true], + ) + end + + context 'when qualified with unique: false' do + include_examples( + 'for when the matcher is qualified', + index: 'lower((code)::text)', + other_index: 'code', + columns: { code: :string }, + unique: false, + qualifier_args: [false], + ) + end + end + end end context 'when not all models are connected to the same database' do @@ -352,6 +450,44 @@ ) end end + + if can_test_expression_indexes? + context 'when given an expression' do + context 'when not qualified with anything' do + it 'returns the correct description' do + matcher = have_db_index('lower(code)') + expect(matcher.description).to eq('have an index on "lower(code)"') + end + end + + context 'when qualified with unique' do + include_examples( + 'for when the matcher is qualified', + index: 'lower(code)', + index_type: 'unique', + qualifier_args: [], + ) + end + + context 'when qualified with unique: true' do + include_examples( + 'for when the matcher is qualified', + index: 'lower(code)', + index_type: 'unique', + qualifier_args: [true], + ) + end + + context 'when qualified with unique: false' do + include_examples( + 'for when the matcher is qualified', + index: 'lower(code)', + index_type: 'non-unique', + qualifier_args: [false], + ) + end + end + end end def record_with_index_on(